fix(wake): #942 follow-up — whole-string knob validation; grep line anchors admit multi-line values
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NsKce8iZuSuRnu3gVMCBKB
This commit is contained in:
co-authored by
Claude Fable 5
parent
3e47fc076f
commit
2621335e77
@@ -295,16 +295,24 @@ _poll_source() {
|
|||||||
# is interpolated into $((...)) under set -u, so a non-numeric value
|
# is interpolated into $((...)) under set -u, so a non-numeric value
|
||||||
# ('300s', '5m', 'abc') would be FATAL to the poll — the one thing this
|
# ('300s', '5m', 'abc') would be FATAL to the poll — the one thing this
|
||||||
# block must never be. Resolve it ONCE, validate, fall back loudly.
|
# 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}"
|
slack="${WAKE_SNAPSHOT_TS_FUTURE_SLACK:-300}"
|
||||||
if ! printf '%s' "$slack" | grep -Eq '^[0-9]{1,9}$'; then
|
# NOT grep: grep is LINE-oriented, so ^...$ anchors bind per line and a
|
||||||
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
|
# 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
|
slack=300
|
||||||
fi
|
fi
|
||||||
# Shape validation is not radix validation: bash reads a leading zero as
|
# 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
|
# '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))
|
slack=$((10#$slack))
|
||||||
now_s="$(date +%s)"
|
now_s="$(date +%s)"
|
||||||
if [ "$snap_ts" -gt $((now_s + slack)) ]; then
|
if [ "$snap_ts" -gt $((now_s + slack)) ]; then
|
||||||
|
|||||||
@@ -276,15 +276,21 @@
|
|||||||
# yields a NEGATIVE age — stale-reads-fresher-than-fresh, the
|
# yields a NEGATIVE age — stale-reads-fresher-than-fresh, the
|
||||||
# exact failure class #940 fixes. The SLACK knob itself is
|
# exact failure class #940 fixes. The SLACK knob itself is
|
||||||
# operator input interpolated into arithmetic under set -u, so it
|
# operator input interpolated into arithmetic under set -u, so it
|
||||||
# gets the same discipline (#941 §2 round 2): validated
|
# gets the same discipline (#941 §2 round 2): shape-validated as
|
||||||
# ^[0-9]{1,9}$, else LOUD fallback to 300 — a malformed knob
|
# 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
|
# ('300s', '5m', 'abc') must never kill the poll, and a negative
|
||||||
# one must never invert the guard into deny-all. Shape validation
|
# one must never invert the guard into deny-all. Shape validation
|
||||||
# is NOT radix validation (#942 review): bash reads leading zeros
|
# is NOT radix validation (#942 review): bash reads leading zeros
|
||||||
# as OCTAL, so '08'/'09' pass the regex yet are fatal in $((...))
|
# as OCTAL, so '08'/'09' pass the shape check yet are fatal in
|
||||||
# and '0300' silently means 192 — the knob is therefore forced
|
# $((...)) and '0300' silently means 192 — the knob is therefore
|
||||||
# base-10 (10#) after validation, so it means what the operator
|
# forced base-10 (10#) after validation, so it means what the
|
||||||
# wrote. SKEW GUARANTEE
|
# 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
|
# (stated, not implied): the future-skew check runs against the
|
||||||
# DETECTOR's clock; consumer-side age arithmetic runs on the
|
# DETECTOR's clock; consumer-side age arithmetic runs on the
|
||||||
# consumer's. A surviving snapshot_ts is therefore attested only
|
# consumer's. A surviving snapshot_ts is therefore attested only
|
||||||
|
|||||||
@@ -644,6 +644,26 @@ EOF
|
|||||||
entry="$("$STORE" drain | tail -1)"
|
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 \
|
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]"
|
|| 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
|
# (d2-pre) ZERO-PADDED knobs — shape-valid, radix-hostile. '08' passes the
|
||||||
# regex but is fatal octal in \$((...)) without the 10# normalization; '0300'
|
# regex but is fatal octal in \$((...)) without the 10# normalization; '0300'
|
||||||
# silently means 192 (octal), so a ts +250s ahead would be WRONGLY dropped.
|
# silently means 192 (octal), so a ts +250s ahead would be WRONGLY dropped.
|
||||||
@@ -671,7 +691,7 @@ EOF
|
|||||||
entry="$("$STORE" drain | tail -1)"
|
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 \
|
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]"
|
|| 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
|
) && ok
|
||||||
|
|
||||||
echo
|
echo
|
||||||
|
|||||||
Reference in New Issue
Block a user