Refuse a returned tree too large for the C queue

QU
1d ago
+68-1

json_tree in the C harness checked tail < 4096 once per dequeued node and then wrote both children. tail starts at 1 and grows by two per node, so it is always odd and the guard never stops the walk in time: the 2048th node of any returned tree writes queue[4095] and queue[4096], cycle or not, and the stack overrun takes down the whole C run instead of failing one case. The walk now checks that both child slots fit before writing them and refuses the case with "Returned tree must not have more than 2047 nodes.", reported as the case's error the way json_list reports a revisited node. Truncating instead would print part of the tree as if it were the answer, as noted in the review of #388.

The limit is exact, since n nodes take 2n + 1 slots, and the problem contracts cap trees at 2000 nodes, so no valid answer reaches it. The other four tree serializers grow their queues and need no bound. This does not depend on the visited check in #388. Until that lands, a short cycle in C also stops at this limit with this message instead of crashing, and once it lands the visited check names the repeat first. #388 edits the same line of json_tree, so whichever merges second needs a one-line rebase.

tests/browser/compiler-explorer.test.js compiles the C harness locally with cc and returns a 2048-node right chain with and without a link back to the root, expecting the message on every case with a root and [] on the empty case, plus a 2047-node chain that must still print all 2047 values.

Validation on macOS 26.5 over 3198a99:

  • node --test --test-name-pattern='returned 204[78]-node' tests/browser/compiler-explorer.test.js: 3 pass. Without the change both 2048-node runs abort with SIGABRT. Stopping the walk instead of refusing fails both 2048-node tests, and a check that gives up one node early fails the 2047-node test.
  • Built with -fsanitize=address, the new json_tree reports no error for 2047-, 2048- and 3000-node chains and cycles.
  • node --test tests/browser/compiler-explorer.test.js: 56 pass. ./scripts/test.sh passes. Not checked by this run: commentflow, ruff, shfmt, shellcheck, cargo-audit and actionlint are not installed, and the pinned Clang lane runs only on Linux x86_64.

Closes #408


Summary by cubic

Fixes a buffer overflow in the C harness's json_tree so an oversized returned tree fails its case instead of crashing the whole run.

The old tail < 4096 check never fired in time: tail starts at 1 and grows by two per node, so the 2048th node always wrote past the 4096-slot queue. The walk now checks that both child slots fit before writing them and reports "Returned tree must not have more than 2047 nodes." as the case's error, matching how json_list reports a revisited node. The limit is exact (n nodes take 2n + 1 slots) and no valid answer reaches it, so truncation is never used. #388 edits the same line, so whichever merges second needs a one-line rebase.