Skip to content

fix(core): correct two defects in _iso_to_epoch - #150

Open
rvalitov wants to merge 1 commit into
SamNet-dev:mainfrom
rvalitov:fix/iso-epoch-timezone
Open

rvalitov wants to merge 1 commit into
SamNet-dev:mainfrom
rvalitov:fix/iso-epoch-timezone

Conversation

@rvalitov

Copy link
Copy Markdown
Contributor

Summary

_iso_to_epoch() parses the ISO 8601 timestamps this project stores for secret expiry, in the format "%Y-%m-%dT%H:%M:%SZ"no fractional seconds. That exact format was broken on both platforms, by two independent defects. Values with fractional seconds happened to work, which is why neither was noticed.

Defect 1 — Z duplication (GNU/Linux: Debian, Ubuntu)

local ts_clean="${ts%%.*}"
[[ "$ts" == *Z ]] && ts_clean="${ts_clean}Z"

${ts%%.*} is a no-op when there is no fractional part, so the trailing Z
survived and a second one was appended:

2026-03-03T10:00:00ZZ      # <- what date is handed

GNU date rejects that, and date -D does not exist on GNU, so the function fell
through to echo 0.

Measured on Debian 12 and Ubuntu 24.04:

_iso_to_epoch '2026-03-03T10:00:00Z'           -> 0            (true: 1772532000)
_iso_to_epoch '2026-03-03T10:00:00.123456789Z' -> 1772532000   (correct)

Impact

Callers read 0 as "no expiry":

  • secret_check_expiry (:3004) — [ "$exp_epoch" -le 0 ] && continueskips the entry, so an expired secret is never disabled
  • secret_extend (:2834) — [ "$base_epoch" -le "$now_epoch" ] && base_epoch=$now_epoch — treats it as already expired and extends from now, silently granting more days than requested

Defect 2 — local-time parsing (Alpine/busybox)

The busybox branch strips the Z and then parses without forcing a timezone: bash local ts_bb="${ts_clean%Z}"epoch=$(date -D '%Y-%m-%dT%H:%M:%S' -d "${ts_bb}" +%s ...) # no TZ
Under -D, busybox ignores a trailing Z entirely, so a UTC value is reinterpreted as local time:

busybox, TZ=UTC+2:  date -D '%Y-%m-%dT%H:%M:%S' -d "2026-03-03T10:00:00Z" +%s  -> 1772524800   (tr1772532000)```                                                                             
The error equals the offset that **that timestamp's own date** carries, so on a DSTzone it is one hour in winter and two in summer — which makes it look intermitterather than constant.

## Fix
```bash                                                                         # Re-attach the Z only if stripping the fraction removed it
[[ "$ts" == *Z && "$ts_clean" != *Z ]] && ts_clean="${ts_clean}Z"

# ... and force UTC for the busybox branch
epoch=$(TZ=UTC date -D '%Y-%m-%dT%H:%M:%S' -d "${ts_bb}" +%s ...)

Both copies are fixed: the manager's, and the comment-stripped copy inside the generated daemon's heredoc (TELEGRAM_SCRIPT), which the daemon uses for its own StartedAt age calculation.

Testing

New tests/test_iso_to_epoch.sh. It has to cover both defects on both platforms, so it runs in two parts:

  • Part A puts a date stub on PATH modelling busybox's real option surface — plain -d <value ending in Z> rejected, -D FMT -d <value> accepted with the Z ignored — and runs under TZ=XXX-2 (a POSIX zone string, so no tzdata is needed). This forces the busybox branch on GNU hosts.
  • Part B restores the real date, which is where the doubled Z surfaces on GNU.

Neither part alone would catch both. Before the change it fails 4/9 on Debian 12 and 5/9 on Alpine 3.20; after, it passes 9/9 on Debian 12, Ubuntu 24.04, Alpine 3.20 and Fedora 41, with no other test result changing.

This parses the timestamps the project stores for secret expiry, in the format
"%Y-%m-%dT%H:%M:%SZ" — no fractional seconds. That exact format was broken on both
platforms, in two independent ways.

1. Z duplication — broke GNU/Linux (Debian, Ubuntu).
   `${ts%%.*}` is a no-op when there is no fractional part, so the trailing Z survived
   and a second one was appended: "2026-03-03T10:00:00ZZ". GNU date rejects that, and
   `date -D` does not exist on GNU, so the function fell through to `echo 0`. Callers
   read 0 as "no expiry":
     - secret_check_expiry skips the entry, so an expired secret is never disabled
     - secret_extend treats it as already expired and extends from now, silently
       granting more days than were asked for
   Measured: on Debian 12 and Ubuntu 24.04 the stored format returned 0 while the same
   value with fractional seconds returned correctly — which is why this went unnoticed.

2. Local-time parsing — broke Alpine/busybox.
   The busybox branch strips the Z and then parses with no TZ. Under `-D`, busybox
   ignores a trailing Z entirely, so a UTC value was reinterpreted as local time. The
   error equals the offset that timestamp's own date carries, so on a DST zone it is an
   hour in winter and two in summer.

Both copies are fixed: the manager's, and the comment-stripped one inside the generated
daemon heredoc.

tests/test_iso_to_epoch.sh covers both — part A forces the busybox branch through a date
stub, part B uses the real date — so each defect is caught on either platform. It fails
4/9 on Debian 12 and 5/9 on Alpine 3.20 before the change, and passes 9/9 on Debian,
Ubuntu, Alpine and Fedora after.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant