Skip to content

Fix codec and parser bugs found by LLM fuzzing - #69

Merged
baanish merged 3 commits into
mainfrom
codex/fuzz-found-fixes
Jun 19, 2026
Merged

Fix codec and parser bugs found by LLM fuzzing#69
baanish merged 3 commits into
mainfrom
codex/fuzz-found-fixes

Conversation

@baanish

@baanish baanish commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Summary

Bugs found by running a fleet of bespoke property-based fuzzers ("LLM fuzzing") against the codec and parser surfaces — hammering the round-trip (decode(encode(x)) == x) and no-throw invariants with seeded, adversarial generators — then adversarially confirming each hit with a minimized repro + root cause. Stacked on #67 (base codex/optimize-codebase).

8 violations confirmed across 5 distinct root causes; each fix is pinned by a regression test.

Bugs fixed

Severity Surface Bug
High arx2/arx3 codec A literal U+007F (DEL) anywhere in artifact content silently corrupted the fragment — 0x7F is the one overlay substitution code JSON.stringify leaves unescaped, so a raw DEL was expanded back into a dictionary pattern on decode → invalid-json. Any shared URL with a DEL char that auto-selected arx2/arx3 was undecodable for the recipient.
High (DoS) base-N decoders A ~27-char fragment drove decodeBaseBMP to compute byteLen ≈ 3.8e9 and attempt a multi-GB allocation + billions of iterations, pegging a core for ~55s — the decoded-size budget only ran after decompression.
High base76 codec Payloads > 5928 bytes overflowed the fixed 2-char length prefix; ALPHABET[77] is undefined, so the wire got an "undefined" prefix and silently round-tripped to a wrong-sized array.
Medium JSON tree renderer Deeply nested JSON (~a few KB, parses fine) overflowed the React client render stack with RangeError — the recursive <JsonNode> walk had no depth bound and no error boundary (unlike DiffRenderer).
Low git-patch parser rename to a/ stripped to an empty displayPath and a degenerate -0 id, defeating the file-N fallback.

Fixes

  • DEL: escape 0x7F at the tuple-JSON boundary in compressTupleEnvelope. A no-op for DEL-free payloads (wire stays byte-identical), and JSON.parse restores the DEL on decode — works for both old and new readers, no migration.
  • DoS: assertWireByteLen() rejects an implausible declared length (> MAX_BROTLI_OUTPUT_BYTES) before allocating, in decodeBaseBMP / decodeBase1k / decodeBase76. Maps to the existing decoded-too-large result. Rejects in ~68 ms instead of ~55 s.
  • base76: =-marked variable-width extended length prefix for payloads beyond the 2-char ceiling. The legacy 2-char shape is preserved byte-for-byte for every length ≤ 5928, so existing links decode unchanged.
  • JSON renderer: MAX_JSON_TREE_DEPTH cap (collapses deep nodes to a leaf) plus a JsonTreeBoundary that degrades to the raw view, matching the DiffRenderer convention.
  • git-patch: stripDiffPrefix returns null for an empty result so the displayPath/id fallback chain applies.

Verification

npm run check (lint + 34 test files + bench + typecheck + build + budgets) is green. The DEL/base76 fixes are wire-compatible; the DoS guard and depth cap reject only provably-implausible inputs.

🤖 Generated with Claude Code

A fleet of property-based fuzzers (round-trip + no-throw invariants over the
codec, fragment, base-N, git-patch, and JSON-tree surfaces) surfaced these,
each now pinned by a regression test:

- arx2/arx3 silently corrupted any content containing U+007F (DEL): 0x7F is the
  one overlay substitution code JSON.stringify leaves unescaped, so a literal
  DEL was expanded back into a dictionary pattern on decode -> invalid JSON.
  Escape it to � at the tuple-JSON boundary (a no-op for DEL-free payloads,
  so the wire stays compatible).
- A ~27-char fragment could peg a core for ~55s / attempt a ~3.8 GB allocation:
  the base76/base1k/baseBMP decoders trusted an attacker-controlled length
  prefix before the decoded-size budget ran. Bound the wire byte length up front.
- base76 silently corrupted payloads > 5928 bytes (its fixed 2-char length
  prefix overflowed to the string "undefined"). Add an "="-marked extended
  prefix; the legacy 2-char shape is preserved byte-for-byte for every length it
  can represent.
- git-patch turned `rename to a/` into an empty displayPath and a degenerate
  "-0" id. stripDiffPrefix now returns null for an empty result so the file-N
  fallback applies.
- The JSON tree renderer overflowed the React render stack on deeply nested
  input (a few KB). Cap render depth and add an error boundary that degrades to
  the raw view, matching DiffRenderer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d53db6be-394c-4974-bf69-455efb435ff6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fuzz-found-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 19, 2026

Copy link
Copy Markdown

Deploying agent-render with  Cloudflare Pages  Cloudflare Pages

Latest commit: 21ea9ff
Status: ✅  Deploy successful!
Preview URL: https://0c01683c.agent-render.pages.dev
Branch Preview URL: https://codex-fuzz-found-fixes.agent-render.pages.dev

View logs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 005d62abf7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// tuple JSON. Escaping it to a \u007F JSON escape (which JSON.parse restores
// on decode) keeps DEL out of the substitution alphabet. This is a no-op for
// DEL-free payloads, so the wire form is byte-identical for existing content.
const tupleJson = JSON.stringify(envelopeToArx2Tuple(envelope)).replace(/\x7f/g, "\\u007f");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid rejecting DEL-heavy arx2 payloads

When an artifact contains many literal DEL bytes, this expands each one from 1 character to the six-character \u007f escape in the internal tuple JSON. The decoder still enforces MAX_DECODED_PAYLOAD_LENGTH on that escaped tuple before JSON.parse, so a valid payload well under the 200k decoded envelope budget (for example, ~40k DEL characters in markdown: standard JSON is ~40k chars, escaped tuple is ~240k) can be encoded into a tiny arx2/arx3 fragment but then fail to decode as decoded-too-large.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and 👍 — correct and a real edge, though a narrow one. The escape trades exactly this for wire-compatibility: each literal DEL becomes the 6-char in the internal tuple JSON, and assertDecodedTextBudget runs on that escaped string, so an artifact with ≳33k DEL bytes (escaped tuple > 200k) is rejected as decoded-too-large even though the real envelope is well under budget.

Two things make it lower-stakes than it reads: (1) it only bites content that is mostly DEL bytes (terminal/control dumps), and (2) it is strictly better than main, where the same payload silently corrupted — now it fails loudly and never returns wrong data.

The clean close that keeps wire-compat: run the decoded-size budget against the parsed envelope rather than the escaped tuple string. The tuple is already bounded by MAX_BROTLI_OUTPUT_BYTES (= 4× the 200k budget = 800k), so parsing first is safe, and the inflation no longer counts against the budget. Happy to fold that into this PR — say the word.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 21ea9ff. The decoded-size budget now runs on the parsed tuple (re-serialized, where each `` escape collapses back to one character) instead of the escaped tuple string, so a DEL-heavy payload is budgeted by its real size. The intermediate dict/overlay strings are still bounded (by the brotli output budget) to keep the expansion-bomb guard. Wire format is unchanged. Pinned by a regression test that round-trips a 40k-DEL artifact through arx2 and arx3.

@greptile-apps

greptile-apps Bot commented Jun 19, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes 8 confirmed fuzzer-found bugs across 5 root causes in the codec and parser surfaces, each pinned by a regression test.

  • DEL corruption (arx2/arx3): compressTupleEnvelope now escapes 0x7F to \u007f before the overlay encode; decode budgets are moved to the parsed tuple (not the \u007f-inflated intermediate string) to prevent false decoded-too-large rejections on DEL-heavy payloads.
  • DoS guard: assertWireByteLen rejects implausible declared lengths (including NaN from out-of-range charCodeAt) before any allocation in decodeBaseBMP, decodeBase1k, and decodeBase76; the truncated extended-prefix NaN case flagged in the previous review thread is also closed.
  • base76 overflow: an =-marked variable-width extended length prefix handles payloads > 5928 bytes; the legacy 2-char shape is preserved byte-for-byte for all existing links.
  • JSON renderer stack overflow: MAX_JSON_TREE_DEPTH (200) collapses deep nodes to a leaf, backed by a JsonTreeBoundary error boundary with componentDidCatch logging matching DiffRendererBoundary parity.
  • git-patch empty path: stripDiffPrefix returns null for an empty result so the displayPath/id fallback chain applies.

Confidence Score: 5/5

All five root causes are addressed with well-scoped changes and regression tests; the wire format is backward-compatible for all previously-valid links.

Each fix is narrowly targeted and verifiable: the DEL escape is a no-op for DEL-free payloads, the DoS guard rejects only lengths above the existing brotli output budget, the base76 extended prefix preserves the legacy 2-char shape byte-for-byte, the depth cap only collapses nodes beyond 200 levels, and the git-patch change is a one-liner. Regression tests cover all five cases including timing assertions for the DoS guard. Previously-flagged issues from the review thread (NaN from truncated extended prefix, missing componentDidCatch) are both addressed.

docs/payload-format.md — the base76 description does not yet reflect the new = extended-prefix marker.

Important Files Changed

Filename Overview
src/lib/payload/arx-codec.ts Five targeted fixes: DEL escape in compressTupleEnvelope, assertWireByteLen guard for all base-N decoders before allocation, extended "=" length prefix for base76 payloads > 5928 bytes, and budget logic fixed to measure the parsed tuple rather than the \u007f-inflated intermediate string. The truncated-extended-prefix NaN guard flagged in the previous review thread is present and correct.
src/components/renderers/json-renderer.tsx Added MAX_JSON_TREE_DEPTH cap (200) in JsonNode and a JsonTreeBoundary error boundary with componentDidCatch logging, matching DiffRendererBoundary parity as noted in the previous thread. The boundary is correctly keyed by artifact.id so switching artifacts resets error state.
src/lib/diff/git-patch.ts stripDiffPrefix now returns null for an empty-string result so the displayPath/id fallback chain applies correctly; the one-liner fix is minimal and correct.
tests/arx-codec-robustness.test.ts New regression file covering DEL round-trip (arx2/arx3/fragment paths), false-rejection of DEL-heavy payloads, and DoS-rejection timing for decodeBaseBMP and decodeFragmentAsync.
tests/arx-codec.test.ts Extended with tests for the legacy ceiling (5928 bytes), one-past-ceiling (5929 bytes), large payload (60 000 bytes, raised timeout), leading-zero preservation, truncated-extended-prefix NaN guard, and full 0x00–0x9F control-byte round-trips.
tests/components/json-renderer.test.tsx One new test exercises 3000-deep nesting and asserts no throw, the renderer mounts, and at least one "max depth reached" label is visible.
tests/git-patch.test.ts New test case for a bare "rename to a/" path that strips to empty, confirming the fallback label "file-1" and id "file-1-0" are produced instead of an empty displayPath.

Fix All in Codex

Reviews (3): Last reviewed commit: "Budget the decoded arx2/arx3 payload by ..." | Re-trigger Greptile

Comment thread src/lib/payload/arx-codec.ts
Comment thread src/components/renderers/json-renderer.tsx
baanish and others added 2 commits June 19, 2026 14:38
…follow-up)

Addresses automated review on the fuzz-fix PR:
- decodeBase76: a truncated extended length prefix (e.g. "=B" claims a length
  digit but carries none) read charCodeAt past the end -> NaN byteLen, and
  `NaN > MAX_BROTLI_OUTPUT_BYTES` is false, so the guard missed it and the
  decode silently returned an empty array. Guard the digit count against the
  string length, and make assertWireByteLen reject non-integer lengths so a NaN
  can never slip past any base-N decoder.
- JsonTreeBoundary: add componentDidCatch to log the swallowed throw (parity
  with DiffRendererBoundary) instead of falling back to raw with no trace.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ped tuple

Follow-up to the DEL-escape fix: literal DEL bytes are escaped to the 6-char
� JSON escape in the internal tuple JSON, so the decoded-size budget run on
that escaped string over-counts DEL-heavy content ~6x — an artifact with ≳33k
DEL bytes (real content well under the 200k limit) was wrongly rejected as
decoded-too-large.

Run the decoded-payload budget on the parsed tuple (re-serialized, where each
escape collapses back to one character), and bound the intermediate
dict/overlay-decoded strings by the brotli output budget so the expansion-bomb
guard is preserved. Wire format and existing payloads are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kilo-code-bot

kilo-code-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (5 files)
  • src/components/renderers/json-renderer.tsx
  • src/lib/diff/git-patch.ts
  • src/lib/payload/arx-codec.ts
  • tests/arx-codec-robustness.test.ts
  • tests/arx-codec.test.ts
  • tests/components/json-renderer.test.tsx
  • tests/git-patch.test.ts

Reviewed by gpt-5.4-mini-2026-03-17 · 260,932 tokens

@baanish
baanish changed the base branch from codex/optimize-codebase to main June 19, 2026 16:47
@baanish
baanish merged commit 2f7935c into main Jun 19, 2026
12 checks passed
@baanish
baanish deleted the codex/fuzz-found-fixes branch June 19, 2026 16:49
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