From 2621335e77ddc208565e735abd3df7abff722851 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 30 Jul 2026 06:28:19 -0500 Subject: [PATCH] =?UTF-8?q?fix(wake):=20#942=20follow-up=20=E2=80=94=20who?= =?UTF-8?q?le-string=20knob=20validation;=20grep=20line=20anchors=20admit?= =?UTF-8?q?=20multi-line=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mos-dt's post-verdict residual (non-blocking, comments 19484/19487): grep is LINE-oriented, so ^[0-9]{1,9}$ binds per line and a knob with an embedded newline ($'300\n8', $'300\nabc') passes the regex whole yet is FATAL inside $((...)) under set -u — the same poll-killing class #942 fixed, reachable only via the env-config operand (snap_ts is immune: jq's number type-check cannot emit an embedded newline). Fix (mos-dt-verified pattern): replace grep with a whole-string case pattern — '' and *[!0-9]* reject, length <= 9 — identical accept/reject to the regex on every single-line value, rejects both multi-line cases. 10# radix force unchanged. Two new D13 sub-cases (SHA-CC2/SHA-CC3) cover exactly the repros; det_seq assertion 6 -> 8. Manifest states the string-extent rule: the validator and the consumer must agree on STRING EXTENT, not just shape and radix. Suite: 13 groups, D13 green 4/4 local runs; sole intermittent failure is the known pre-existing D4 fd-9 flake (carried separately by mos-dt). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01NsKce8iZuSuRnu3gVMCBKB --- .../mosaic/framework/tools/wake/detector.sh | 18 ++++++++++----- .../mosaic/framework/tools/wake/manifest.txt | 18 ++++++++++----- .../tools/wake/test-wake-detector.sh | 22 ++++++++++++++++++- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/packages/mosaic/framework/tools/wake/detector.sh b/packages/mosaic/framework/tools/wake/detector.sh index 4eea6248..d0c33175 100755 --- a/packages/mosaic/framework/tools/wake/detector.sh +++ b/packages/mosaic/framework/tools/wake/detector.sh @@ -295,16 +295,24 @@ _poll_source() { # is interpolated into $((...)) under set -u, so a non-numeric value # ('300s', '5m', 'abc') would be FATAL to the poll — the one thing this # block must never be. Resolve it ONCE, validate, fall back loudly. - local now_s slack + local now_s slack slack_ok slack="${WAKE_SNAPSHOT_TS_FUTURE_SLACK:-300}" - if ! printf '%s' "$slack" | grep -Eq '^[0-9]{1,9}$'; then - echo "detector.sh: WAKE_SNAPSHOT_TS_FUTURE_SLACK='$slack' is not a plain non-negative integer (seconds) — falling back to 300, poll continues (#940)." >&2 + # NOT grep: grep is LINE-oriented, so ^...$ anchors bind per line and a + # multi-line value ($'300\n8') passes the regex yet is FATAL in $((...)). + # The case pattern matches the WHOLE string, newlines included. (snap_ts + # is immune: jq's number type-check above cannot emit an embedded newline.) + case "$slack" in + '' | *[!0-9]*) slack_ok=1 ;; + *) [ "${#slack}" -le 9 ] && slack_ok=0 || slack_ok=1 ;; + esac + if [ "$slack_ok" -ne 0 ]; then + echo "detector.sh: WAKE_SNAPSHOT_TS_FUTURE_SLACK='$slack' is not a plain non-negative integer of at most 9 digits (seconds) — falling back to 300, poll continues (#940)." >&2 slack=300 fi # Shape validation is not radix validation: bash reads a leading zero as - # OCTAL, so '08'/'09' pass the regex yet are FATAL in $((...)), and + # OCTAL, so '08'/'09' pass the shape check yet are FATAL in $((...)), and # '0300' silently means 192. Force base-10 so the knob means what the - # operator wrote (safe: the regex above guarantees pure digits). + # operator wrote (safe: the case pattern above guarantees pure digits). slack=$((10#$slack)) now_s="$(date +%s)" if [ "$snap_ts" -gt $((now_s + slack)) ]; then diff --git a/packages/mosaic/framework/tools/wake/manifest.txt b/packages/mosaic/framework/tools/wake/manifest.txt index 02830656..217b9100 100644 --- a/packages/mosaic/framework/tools/wake/manifest.txt +++ b/packages/mosaic/framework/tools/wake/manifest.txt @@ -276,15 +276,21 @@ # yields a NEGATIVE age — stale-reads-fresher-than-fresh, the # exact failure class #940 fixes. The SLACK knob itself is # operator input interpolated into arithmetic under set -u, so it -# gets the same discipline (#941 §2 round 2): validated -# ^[0-9]{1,9}$, else LOUD fallback to 300 — a malformed knob +# gets the same discipline (#941 §2 round 2): shape-validated as +# a plain non-negative integer of at most 9 digits, else LOUD +# fallback to 300 — a malformed knob # ('300s', '5m', 'abc') must never kill the poll, and a negative # one must never invert the guard into deny-all. Shape validation # is NOT radix validation (#942 review): bash reads leading zeros -# as OCTAL, so '08'/'09' pass the regex yet are fatal in $((...)) -# and '0300' silently means 192 — the knob is therefore forced -# base-10 (10#) after validation, so it means what the operator -# wrote. SKEW GUARANTEE +# as OCTAL, so '08'/'09' pass the shape check yet are fatal in +# $((...)) and '0300' silently means 192 — the knob is therefore +# forced base-10 (10#) after validation, so it means what the +# operator wrote. The validator and the consumer must also agree +# on STRING EXTENT (#942 follow-up): grep's ^...$ anchors bind +# per LINE, so a multi-line value ($'300\n8') passed the regex +# whole yet was fatal in $((...)) — validation is a whole-string +# case pattern, not grep, so an embedded newline rejects. +# SKEW GUARANTEE # (stated, not implied): the future-skew check runs against the # DETECTOR's clock; consumer-side age arithmetic runs on the # consumer's. A surviving snapshot_ts is therefore attested only diff --git a/packages/mosaic/framework/tools/wake/test-wake-detector.sh b/packages/mosaic/framework/tools/wake/test-wake-detector.sh index 9ec80e68..176464a9 100755 --- a/packages/mosaic/framework/tools/wake/test-wake-detector.sh +++ b/packages/mosaic/framework/tools/wake/test-wake-detector.sh @@ -644,6 +644,26 @@ EOF entry="$("$STORE" drain | tail -1)" echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts")' >/dev/null 2>&1 \ || fail_msg "D13: negative slack must NOT invert the guard into deny-all [$entry]" + # (c2) MULTI-LINE knobs — grep's ^...$ anchors bind PER LINE, so a value with + # an embedded newline ($'300\n8') passed the old regex whole yet is FATAL in + # \$((...)) ('error token is "8"'; $'300\nabc' dies as unbound variable). The + # case pattern matches the WHOLE string: both must fall back loudly, keep the ts. + printf '{"snapshot_sha":"%s","snapshot_ts":%s}\n' "$goodsha" "$(date +%s)" >"$stub/repo_r1.meta" + printf 'SHA-CC2\n' >"$stub/repo_r1" + err="$(WAKE_SNAPSHOT_TS_FUTURE_SLACK=$'300\n8' "$DET" poll-once 2>&1 >/dev/null)"; rc=$? + [ "$rc" -eq 0 ] || fail_msg "D13: multi-line SLACK (300\\n8) must NEVER fail the poll (rc=$rc) [$err]" + echo "$err" | grep -qi 'falling back to 300' || fail_msg "D13: multi-line slack must be LOUD on stderr [$err]" + entry="$("$STORE" drain | tail -1)" + echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts")' >/dev/null 2>&1 \ + || fail_msg "D13: valid metadata must SURVIVE a multi-line knob [$entry]" + printf '{"snapshot_sha":"%s","snapshot_ts":%s}\n' "$goodsha" "$(date +%s)" >"$stub/repo_r1.meta" + printf 'SHA-CC3\n' >"$stub/repo_r1" + err="$(WAKE_SNAPSHOT_TS_FUTURE_SLACK=$'300\nabc' "$DET" poll-once 2>&1 >/dev/null)"; rc=$? + [ "$rc" -eq 0 ] || fail_msg "D13: multi-line SLACK (300\\nabc) must NEVER fail the poll (rc=$rc) [$err]" + echo "$err" | grep -qi 'falling back to 300' || fail_msg "D13: multi-line non-numeric slack must be LOUD on stderr [$err]" + entry="$("$STORE" drain | tail -1)" + echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts")' >/dev/null 2>&1 \ + || fail_msg "D13: valid metadata must SURVIVE a multi-line non-numeric knob [$entry]" # (d2-pre) ZERO-PADDED knobs — shape-valid, radix-hostile. '08' passes the # regex but is fatal octal in \$((...)) without the 10# normalization; '0300' # silently means 192 (octal), so a ts +250s ahead would be WRONGLY dropped. @@ -671,7 +691,7 @@ EOF entry="$("$STORE" drain | tail -1)" echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts") | not' >/dev/null 2>&1 \ || fail_msg "D13: valid SLACK=0 must drop the future ts but keep the sha [$entry]" - [ "$(det_seq)" = "6" ] || fail_msg "D13: all six real deltas must still have enqueued, got seq $(det_seq)" + [ "$(det_seq)" = "8" ] || fail_msg "D13: all eight real deltas must still have enqueued, got seq $(det_seq)" ) && ok echo