Skip to content

backlog: file #1222 -- a fail-closed contract pinned through a C-stack-dependent trigger - #331

Merged
wshallwshall merged 1 commit into
mainfrom
w3-file-recursion
Aug 11, 2026
Merged

backlog: file #1222 -- a fail-closed contract pinned through a C-stack-dependent trigger#331
wshallwshall merged 1 commit into
mainfrom
w3-file-recursion

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Files BACKLOG #1222 -- a fail-closed contract pinned through a C-stack-dependent trigger.

tests/test_sandbox_codec.py::test_recursion_error_is_not_a_value_error pins a real contract:
decode_frame catches RecursionError explicitly because it is a RuntimeError, so an
except ValueError would let a deep-nesting rejection escape the fail-closed path -- exactly as the old
bare KeyError did. The contract deserves a test. The trigger does not test it portably.

with pytest.raises(RecursionError):
    json.loads(_deep_json(100000))          # "[" * 100000 + "]" * 100000

Measured on two environments, and the spread is the finding

json.loads first raises at 100,000 raises?
a local box (CPython 3.14.6, win32) 16,913 yes -- 5.9x margin
the failing CI runner above 100,000 no

Same bytes, same trigger, thresholds differing by more than six times. A binary "it passes here" would
have established almost nothing; the spread is what establishes that the trigger is environment-dependent.

The mechanism, which rules out the fix people will reach for first

sys.getrecursionlimit() is 1000, and json.loads reaches 16,913. The C accelerator does not
consult Python's recursion limit -- it consumes the C stack
, which no Python-level knob controls. So
sys.setrecursionlimit(...) in a conftest is not an available fix, and no repo-side setting could
have caused or cured this.

Ruled out, so nobody re-derives it

  • 48f8712d..8077a033: 908 added lines, and zero matches for setrecursionlimit,
    sys.setrecursion, threading.stack_size, stack_size, or RecursionError.
  • tests/test_sandbox_codec.py unchanged across that range -- blob 24807e73e152 byte-identical at
    both ends. The failing test is literally the bytes that passed.
  • No recursion or stack knob in conftest.py, pyproject.toml, or the sandbox modules.

Cause: UNKNOWN, and recorded as unknown

main at 8077a033 passes this test (11,199 passed, 0 failed, full suite) while two unrelated PRs
fail it. The runner-image hypothesis does not cleanly explain main passing, and no mechanism is
invented here to reconcile that
. An honest unknown is worth more than a plausible story -- and a
plausible story here would be believed.

The named anti-fix

Do not raise the depth until it fails again. That buys a green on today's image, re-fires on the next
roll, and makes the test more environment-coupled rather than less.

The direct alternative: assert the type relationship, and drive decode_frame's handler with a raised
RecursionError rather than manufacturing one from real recursion. Acceptance criterion: the
replacement must still fail when the handler is narrowed to except ValueError, and must pass on
both
a box that raises at 16,913 and one that does not raise at 100,000. Covering only one of those is
what produced this item.

Ledger-only. Verified: #1222 free in the live ledger and the archive; merges clean onto current main.

…-dependent trigger

Live: test_recursion_error_is_not_a_value_error reds #302 and #328 while
origin/main 8077a03 -- the commit they are cut from -- passes the full
suite 11199/0. Two unrelated changes, one shared failing node id.

THE CONTRACT IS REAL AND MUST SURVIVE ANY FIX: decode_frame catches
RecursionError explicitly because RecursionError is a RuntimeError and
NOT a ValueError, so an 'except ValueError' would let a deep-nesting
rejection escape the fail-closed path. The defect is the instrument, not
the intent.

MEASURED THE TRIGGER'S ENVIRONMENT SENSITIVITY, framed as that question
and not as an attribution of the CI red -- a local pass or fail cannot
attribute a runner failure, and reporting it as though it could is the
adjacent-question error:

  this box (CPython 3.14.6, AMD64, win32): json.loads first raises at
    depth 16,914, so the test's 100,000 carries 5.9x margin
  the failing runner: 100,000 does NOT raise, so its threshold exceeds
    100,000
  same bytes, same trigger, thresholds differing by more than 6x

The mechanism is visible in those numbers: sys.getrecursionlimit() is
1000, yet json.loads reaches 16,914. The C accelerator does not use
Python's recursion limit -- it consumes the C STACK, which no
Python-level knob controls. Interpreter build, thread stack size and
runner image all move it; nothing in this repo does.

RULED OUT so nobody re-derives it: across 48f8712..8077a03 there are
908 added lines and ZERO occurrences of setrecursionlimit,
sys.setrecursion, threading.stack_size, stack_size or RecursionError; the
test file is unchanged with blob 24807e7 byte-identical at both
ends; and no recursion or stack knob exists in conftest, pyproject or the
sandbox modules. Cause UNKNOWN and recorded as unknown rather than
guessed -- the runner image is the remaining candidate, it is outside
this repo, and it does not cleanly explain main passing. No mechanism is
invented to reconcile that.

The item names the fix NOT to make: raising the depth buys a green on
today's image, re-fires on the next roll, and makes the test MORE
environment-coupled. The contract can be pinned directly -- assert the
type relationship and drive decode_frame's handler with a raised
RecursionError rather than manufacturing one from real recursion.

And it names how to prove a fix: the replacement must still FAIL when
decode_frame's except RecursionError is narrowed to except ValueError,
and must pass BOTH on a box that raises at 16,914 and on one that does
not raise at 100,000 -- covering only one of those is what produced this.
@wshallwshall
wshallwshall merged commit 00a2b28 into main Aug 11, 2026
33 checks passed
@wshallwshall
wshallwshall deleted the w3-file-recursion branch August 11, 2026 18:31
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