Reject a retention duration longer than about 292 years (closes #289)
check / check (push) Failing after 3m36s

parseDuration multiplied each number by the length of its unit with no
limit, so from 293y (or 15251w, 3559mo, 106752d, or a sum such as
200y200y) the result wrapped to a negative duration. The purge cutoff
then lay in the future and every snapshot of the name was selected, for
--older-than and --keep-newer-than alike. Each part is now checked
before it is added, and a total past the largest time.Duration is
rejected as an invalid duration, so nothing is deleted. Go units were
already safe: time.ParseDuration rejects an overflow.

With --prune the backup still runs before the duration is read; only
the prune step fails.

Model: opus-5-5
This commit was merged in pull request #328.
This commit is contained in:
2026-10-13 01:51:24 +02:00
parent fa56165fe4
commit c9a519c233
4 changed files with 68 additions and 8 deletions
+24 -8
View File
@@ -3,6 +3,7 @@ package vaultik
import (
"errors"
"fmt"
"math"
"regexp"
"strconv"
"strings"
@@ -47,6 +48,10 @@ const (
year = 365 * day
)
// maxDuration is the largest value a time.Duration can hold, about 292
// years.
const maxDuration = time.Duration(math.MaxInt64)
// A snapshot ID split on "_" has at least a hostname and a trailing
// timestamp.
const minSnapshotIDParts = 2
@@ -145,10 +150,10 @@ func parseSnapshotName(snapshotID, hostname string) string {
// parseDuration parses a duration string with support for human-friendly units:
// d/day/days, w/week/weeks, mo/month/months, y/year/years, plus standard Go
// duration units. Following Go, m is minutes and mo is months. A bare number,
// an unknown unit, and a negative value are all rejected. Outside the Go
// units the input must be whole numbers each followed directly by a unit
// (2w3d), so 1.0y and 30 days are errors; decimals work only in Go units
// (1.5h).
// an unknown unit, a negative value, and a total longer than maxDuration are
// all rejected. Outside the Go units the input must be whole numbers each
// followed directly by a unit (2w3d), so 1.0y and 30 days are errors;
// decimals work only in Go units (1.5h).
func parseDuration(s string) (time.Duration, error) {
if strings.HasPrefix(strings.TrimSpace(s), "-") {
return 0, errNegativeDuration
@@ -181,19 +186,30 @@ func parseDuration(s string) (time.Duration, error) {
return 0, fmt.Errorf("invalid number %q: %w", match[1], err)
}
var unitLength time.Duration
unit := strings.ToLower(match[2])
switch unit {
case "d", "day", "days":
total += time.Duration(n) * day
unitLength = day
case "w", "week", "weeks":
total += time.Duration(n) * week
unitLength = week
case "mo", "month", "months":
total += time.Duration(n) * month
unitLength = month
case "y", "year", "years":
total += time.Duration(n) * year
unitLength = year
default:
return 0, fmt.Errorf("%w %q", errUnknownTimeUnit, unit)
}
// Past maxDuration the sum wraps to a negative value, which would
// put the purge cutoff in the future.
if time.Duration(n) > (maxDuration-total)/unitLength {
return 0, fmt.Errorf("%w: %q is longer than about 292 years",
errInvalidDuration, s)
}
total += time.Duration(n) * unitLength
}
return total, nil