Skip to content

markdown: harden the parser's remaining 32-bit limits - #33078

Merged
Jarred-Sumner merged 7 commits into
mainfrom
farm/fbc702e4/md-internal-limits
Jun 30, 2026
Merged

markdown: harden the parser's remaining 32-bit limits#33078
Jarred-Sumner merged 7 commits into
mainfrom
farm/fbc702e4/md-internal-limits

Conversation

@robobun

@robobun robobun commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #32752, requested by @alii after it merged (#32752 (comment) has the plan). That fix only guarded the input length that Parser::init reads; three sibling limits in the same path were still fatal in release builds.

What

  1. Block metadata overflow. A block's offset into block_bytes is stored as a u32 (Container.block_byte_off, and the casts in push_container / enter_child_containers that produce it), but nothing bounded block_bytes, which grows by one 16-byte header per block plus 12 bytes per accumulated line. A few hundred MB of nested > or # (16x amplification) therefore still aborted in release:

    panic: int cast: TryFromIntError(PosOverflow)   # containers.rs, push_container
    

    block_bytes is now capped at MAX_BLOCK_BYTES through one shared parser::check_block_bytes_len, called by the only two growers: Parser::append_block_header (the align/check/grow/write sequence that start_new_block and push_container_bytes used to duplicate) and end_current_block's line append. A const _: () = assert!(..) next to the constant encodes the headroom proof, so enlarging BlockHeader is a compile error instead of a silently shrunk margin, and the surviving u32 casts are annotated with the invariant that makes them safe. JS sees a RangeError (ERR_OUT_OF_RANGE) saying the input requires more block metadata than the parser can address, because an input.byteLength bound would be wrong for a 300 MB input.

  2. Lookahead overflow at the accepted boundary. input_size accepted lengths up to u32::MAX, but is_html_block_start_condition probes 9 bytes past an in-bounds offset in OFF (u32) arithmetic, so off + 9 could wrap at the very end of a maximum-length input, pass the <= size test, and index out of bounds. The accepted maximum is now MAX_INPUT_LEN = u32::MAX - MAX_LOOKAHEAD = 4294967286, where MAX_LOOKAHEAD is derived from the probe's own constant (line_analysis::CDATA_OPEN) rather than restated as a literal, so the two cannot drift. Its doc states the real invariant: the longest OFF-typed fixed addition (longer probes such as match_html_tag's add in usize and cannot wrap).

  3. Uncatchable OOM in the ANSI renderer. AnsiRenderer::init reserved 1.5x the input with the aborting Vec::reserve, and its output path used plain extend_from_slice/push, so a failed allocation aborted the process even though the renderer already carries an oom flag that render_to_ansi converts into an error. The two renderers' byte-identical OutputBuffer structs are now one shared type in src/md/output.rs using try_reserve, and the ANSI staging buffers that hold output-scale data before it reaches the sink (code_buf, heading_buf, image_alt, table_cell_buf, plus the table_rows/table_cells vectors) record their failure on the same flag, matching what the HTML renderer already did for its heading buffer. Scope, to be explicit: that flag covers the output sink and those staging buffers; the parser proper and the renderer's remaining per-node scratch keep the runtime's global abort-on-OOM policy, as before this PR.

Tests

test/js/bun/md/md-edge-cases.test.ts, in subprocesses so the bun:internal-for-testing knob and the huge virtual buffers cannot leak into other tests:

  • The oversized-input test also covers the largest allocatable length that must still be rejected (2^32 - 1, the accepted-boundary side) and asserts the new maximum in the message. On the previous code that length is accepted and the child grinds through a 4 GiB scan until the test times out.
  • New setMaxMarkdownBlockBytesForTesting(n) in bun:internal-for-testing (modeled on setSyntheticAllocationLimitForTesting): lowers the block-metadata cap, never raises it past the real one, returns the previous value. The new test shrinks it to exactly 40 single-line paragraphs of metadata (28 bytes each) and proves the boundary: 40 paragraphs render, 41 throw, a nested blockquote throws through the container-header path, and both render again once the cap is restored. The ERR_OUT_OF_RANGE code and message are asserted verbatim, so the guard, the error mapping, and the ? propagation through the growers are all load-bearing.
  • Also raises the child budget of the neighboring reference-definition perf test from 30s to 75s: its 220k-line debug+ASAN child needs 24-45s on a loaded runner (observed SIGKILLed at 30.1s), which made it the flakiest test in this file; workload and assertions unchanged. The oversized-input test's redundant inner 20s child timeout is gone for the same reason: the outer test timeout already bounds and reaps a hung child.

Not covered by a test: the try_reserve conversion itself, since an allocation failure cannot be triggered deterministically in CI.

bun bd test test/js/bun/md/   # 1064 pass
cargo test -p bun_md          # 14 pass

@robobun

robobun commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 3:33 AM PT - Jun 30th, 2026

@robobun, your commit fc66f1f has 1 failures in Build #67192 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 33078

That installs a local version of the PR into your bun-33078 executable, so you can run:

bun-33078 --bun

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The markdown parser gains explicit size limit constants (MAX_INPUT_LEN, MAX_BLOCK_BYTES) and a new TooManyBlocks error variant. Block and container methods switch their error types from AllocError to parser::Error and add MAX_BLOCK_BYTES guards. OutputBuffer write methods become non-panicking via try_reserve. JS error mapping and edge-case tests are updated to reflect the new limits.

Changes

Parser OOM/limit hardening

Layer / File(s) Summary
Parser limit constants and TooManyBlocks error variant
src/md/parser.rs
Adds MAX_LOOKAHEAD, MAX_INPUT_LEN, MAX_BLOCK_BYTES constants and TooManyBlocks variant to ParserError; rewrites input_size to enforce MAX_INPUT_LEN instead of OFF::try_from.
Block/container guards using MAX_BLOCK_BYTES and parser::Error
src/md/blocks.rs, src/md/containers.rs
Updates analyze_line, process_line, start_new_block, push_container_bytes, enter_child_containers, leave_child_containers to return parser::Error and adds TooManyBlocks guards when computed sizes exceed MAX_BLOCK_BYTES.
OutputBuffer non-panicking allocation
src/md/ansi_renderer.rs
Replaces extend_from_slice/push with try_reserve + oom flag in write/write_byte; AnsiRenderer::init uses try_reserve with ignored result.
JS error mapping for new limits
src/runtime/api/MarkdownObject.rs
Imports MAX_INPUT_LEN; updates InputTooLarge range bound to MAX_INPUT_LEN; refactors TooManyBlocks to use ErrCode::OUT_OF_RANGE.
Edge-case tests for addressability limits
test/js/bun/md/md-edge-cases.test.ts
Renames test group, adds boundary = 2 ** 32 - 1 buffer, extends runs list, updates subprocess timeout/kill policy, and updates expected RangeError messages to <= 4294967286.

Possibly related PRs

  • oven-sh/bun#32752: Overlaps directly — modifies the same src/md/parser.rs input-length bounds, src/runtime/api/MarkdownObject.rs error mapping, and test/js/bun/md/md-edge-cases.test.ts oversized-input rejection tests.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: hardening markdown parser 32-bit limits.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description clearly explains the changes and includes verification notes, though it uses different headings than the template.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/md/ansi_renderer.rs`:
- Around line 272-286: The OOM guard in AnsiRenderer only covers self.out, while
side buffers like image_alt, code_buf, heading_buf, table_cell_buf, table_cells,
and table_rows can still abort on allocation failure. Update the renderer paths
that write to these buffers to use fallible helpers that set the same oom state,
or change the relevant callbacks in AnsiRenderer to return Result so allocation
failures flow into to_owned_slice() as AllocError instead of panicking/aborting.

In `@src/md/blocks.rs`:
- Around line 850-854: `end_current_block` and the header-write path only guard
`block_bytes` growth in one place, but `VerbatimLine` appends can still overflow
the u32-addressable range before that check runs. Move the `MAX_BLOCK_BYTES`
validation into a shared helper used by both `end_current_block` and the
container/header write path, so every `block_bytes` growth is checked before
appending. Make sure `end_current_block` returns `parser::Error::TooManyBlocks`
on failure and avoid any panic-prone size arithmetic on external input.

In `@test/js/bun/md/md-edge-cases.test.ts`:
- Around line 1248-1254: The subprocess test in md-edge-cases.test.ts only
checks stdout via JSON.parse(stdout.trim()) and ignores the captured exitCode
from proc.exited, so a failing child process could still pass. Update the test
around the Promise.all(proc.stdout.text(), proc.exited) result to assert the
subprocess exits successfully before validating stdout, and keep the stdout
assertion in place using the existing message helper and SKIP expectation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2597bbe4-daca-4122-bfda-7176cb6cd111

📥 Commits

Reviewing files that changed from the base of the PR and between fb24aac and 33c14bb.

📒 Files selected for processing (6)
  • src/md/ansi_renderer.rs
  • src/md/blocks.rs
  • src/md/containers.rs
  • src/md/parser.rs
  • src/runtime/api/MarkdownObject.rs
  • test/js/bun/md/md-edge-cases.test.ts

Comment thread src/md/ansi_renderer.rs Outdated
Comment thread src/md/blocks.rs Outdated
Comment thread test/js/bun/md/md-edge-cases.test.ts
Comment thread src/md/containers.rs Outdated
Comment thread src/md/parser.rs Outdated
@mintlify

mintlify Bot commented Jun 29, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
bun 🟢 Ready View Preview Jun 29, 2026, 2:09 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

Comment thread src/md/parser.rs Outdated
@robobun

robobun commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author

CI status for build 66810 (latest commit): every failure is on a lane or file this PR does not touch, and test/js/bun/md/md-edge-cases.test.ts passed on every lane that ran it.

  • darwin 26 aarch64 - test-bun: buildkite-agent artifact download timed out after 120s; no tests ran. The same agent has failed this way on every build it picked up for days, across unrelated PRs.
  • ubuntu 25.04 x64 - test-bun: test/js/bun/util/v8-heap-snapshot.test.ts - SIGKILL ("main process killed by SIGKILL but no core file found"); no test assertion failed in the job.
  • alpine 3.23 x64 and x64-baseline - test-bun: test/js/node/test/parallel/test-net-connect-memleak.js - code 1 on all retries (a GC-dependent Node compat test).

Nothing markdown-related is red. All review feedback is addressed and the threads are resolved.

robobun added 3 commits June 30, 2026 01:34
Follow-ups to #32752, requested in review after it merged. The first fix only
guarded the input length read by Parser::init; three sibling limits in the
same path were still fatal in release builds:

- block_bytes growth: every block offset is a u32 (Container.block_byte_off,
  the casts in push_container and enter_child_containers), but nothing
  bounded the buffer, so a few hundred MB of nested blockquotes or headings
  panicked on 'int cast' in push_container. Reject the document at the two
  sites that grow the buffer (push_container_bytes, start_new_block) with a
  new ParserError::TooManyBlocks, reported to JS as a RangeError.
- lookahead margin: input_size accepted lengths up to u32::MAX, but the
  parser probes up to 9 bytes past an offset (the <![CDATA[ check), so
  off + 9 could wrap at the very end of a maximum-length input and panic on
  the slice. The accepted maximum is now u32::MAX - 9 and the RangeError
  reports it.
- the ANSI renderer's OutputBuffer aborted on OOM (plain reserve and
  extend_from_slice); it now uses try_reserve and its existing oom flag,
  exactly like the HTML renderer's OutputBuffer already did, so allocation
  failure surfaces as the catchable error render_to_ansi already maps.
Review follow-ups: end_current_block flushes 12 bytes of VerbatimLine per
accumulated line into block_bytes, so a few hundred MB of two-byte lines
still overflowed the u32 block offsets through a growth site the first
commit missed (and its comment wrongly called the other two the only ones).
Move the cap into one parser::check_block_bytes_len helper used by all three
growth sites, and name the actual function holding the 9-byte lookahead in
the MAX_LOOKAHEAD comment.
The constant bounds headers and the per-line VerbatimLine data since the
end_current_block guard, not just headers.
…er can meet

The 'documents with many reference definitions and references render in
linear time' test spawns a debug+ASAN child over a 220k-line document with a
30s kill timer. On a loaded runner that child needs 24-45s (observed:
SIGKILLed at 30.1s), which made it the flakiest test in this file. Raise the
child's hard stop to 75s, still under the test's own 90s timeout; the
workload and every assertion are unchanged.
@robobun

robobun commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

CI status for build 67052 (latest commit, finished): every failure is in a lane or file this PR does not touch, and test/js/bun/md/md-edge-cases.test.ts passed on every lane that ran it.

  • darwin 26 aarch64: buildkite-agent artifact download timed out after 120s (no tests ran; this agent has failed the same way on every build for days).
  • darwin 14 aarch64: test/regression/issue/20965.test.ts - code 1 on all retries.
  • darwin 14 x64: test/js/bun/terminal/terminal.test.ts - code 1 on all retries.
  • alpine 3.23 x64 and x64-baseline: test/js/node/test/parallel/test-net-connect-memleak.js - code 1 on all retries.

Unrelated to markdown. The last commit also deflakes the one test in this file that was genuinely flaky under load (the reference-definition perf test's 30s child timeout; its debug+ASAN child needs 24-45s on a loaded runner), which is what kept intermittently failing earlier runs of this file.

@alii alii left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Adversarial design-review pass on this PR. The integer-overflow analysis is solid — I went after the MAX_BLOCK_BYTES headroom proof cast-by-cast and the MAX_LOOKAHEAD bound with a crate-wide grep and could not break either. But two of the three headline claims need work before this merges, so: requesting changes (in substance; left as a comment-review).

The 5 inline comments are ordered by severity: #1 (ANSI staging buffers + duplicated OutputBuffer) and #2 (zero coverage on TooManyBlocks when bun:internal-for-testing makes it a 1 KB test) are the request-changes. #3 (MAX_LOOKAHEAD proof is wrong and unenforced) and #4 (the new 20s inner SIGKILL) are should-fix. #5 is non-blocking notes from verifying the headroom proof.

What was probed and held up — no action needed

  • Entry-point coverage is even. All four JS entry points (html / ansi / render / react, MarkdownObject.rs:201/168/318/418) reach the single input_size call in Parser::init (parser.rs:188-189) and route every ParserError through parser_err_to_js. No mode is missing the InputTooLarge/TooManyBlocks mapping.
  • The boundary test is not vacuous — the SKIP branch, the both-allocations-in-one-try shape, and the "fails for the wrong reason" angle were all run to ground.
  • indent accumulation (helpers.rs:123) cannot overflow u32 in release given the now-enforced input bound; [profile.release] never sets overflow-checks.
  • The TooManyBlocks error shape and the hardcoded "(4 GiB)" are fine — the message states a different fact than MAX_BLOCK_BYTES, so it's not a duplicated constant.
  • The CLI / bundler .md consumers (run_command.rs:3565, transpiler.rs:2162, ParseTask.rs:867) only see Err(_), so the new variants change nothing for them.
  • Widening the lookahead arithmetic instead of shrinking the input was considered; shrinking MAX_INPUT_LEN is the right design.

(Separately: I rebased the branch onto main earlier today to drop the stale [autofix.ci] commit that was reverting part of #33112's docs pass — the diff is now src/md + MarkdownObject.rs + the test only.)

Comment thread src/md/ansi_renderer.rs
Comment thread src/md/parser.rs
Comment thread src/md/parser.rs Outdated
Comment thread test/js/bun/md/md-edge-cases.test.ts Outdated
Comment thread src/md/parser.rs
robobun added 3 commits June 30, 2026 09:27
… staging buffers

The HTML and ANSI renderers each carried a byte-identical OutputBuffer;
hoist it (plus try_extend/try_push helpers) into src/md/output.rs. The
ANSI renderer routed code-block, heading, image-alt, and table-cell
bytes through staging Vecs that still aborted on allocation failure one
branch above the guarded output sink; those sites (and the
table_rows/table_cells pushes) now record the failure on the same oom
flag that render_to_ansi already checks, matching how the HTML renderer
guards its heading buffer. resolve_href can no longer fail, so it
returns Box<[u8]> directly.
MAX_LOOKAHEAD is now derived from the CDATA probe it exists for
(line_analysis::CDATA_OPEN), and its doc states the real invariant: the
longest OFF-typed fixed addition, not the longest lookahead. A const
assert next to MAX_BLOCK_BYTES encodes the headroom proof so a larger
BlockHeader fails to compile instead of silently shrinking the margin.

The align/check/grow/write-header sequence that start_new_block and
push_container_bytes duplicated moves into Parser::append_block_header,
so a new header site cannot skip check_block_bytes_len; the remaining
u32 casts of block_bytes.len() are annotated with the invariant that
makes them safe.

check_block_bytes_len now reads an AtomicUsize initialized to
MAX_BLOCK_BYTES and lowerable (never raisable) through
setMaxMarkdownBlockBytesForTesting in bun:internal-for-testing. The new
test shrinks the cap to ~1 KB and proves the exact boundary: a document
whose metadata lands on the cap renders, one more block (and a nested
blockquote's container headers) throws the catchable ERR_OUT_OF_RANGE,
and restoring the cap makes both render again.

Also drops the redundant inner timeout/SIGKILL from the addressable
limit test; the outer test timeout already bounds and reaps the child.
BlockHeader.data carries block-type payloads (heading level, list start,
fenced-code info offset into the source text), never an offset into
block_bytes, so it does not justify the cap.
@robobun

robobun commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

@alii all five taken, in 240c35a and b5b7ffc (plus fc66f1f for one doc line). Per thread:

  • Shared OutputBuffer in src/md/output.rs; the ANSI staging sinks and the table_rows/table_cells pushes set the same oom flag through try_extend/try_push; resolve_href returns Box<[u8]>.
  • setMaxMarkdownBlockBytesForTesting in bun:internal-for-testing, plus a child test asserting the exact at-the-cap / one-past boundary and the verbatim ERR_OUT_OF_RANGE code and message; a const _: () = assert!(..) next to MAX_BLOCK_BYTES encodes the headroom proof.
  • MAX_LOOKAHEAD is derived from line_analysis::CDATA_OPEN, the probe is written against it, and the doc states the OFF-typed invariant (and why match_html_tag is exempt).
  • The inner 20s SIGKILL is gone.
  • Parser::append_block_header is now the only header-append path, the three block_bytes.len() casts are annotated with the invariant, and the PR body is rewritten. You were right about current_block and BlockHeader.data; the latter was also wrong in the MAX_BLOCK_BYTES doc comment, so fc66f1f fixes that too.

bun bd test test/js/bun/md/: 1064 pass. cargo test -p bun_md: 14 pass.

@robobun
robobun requested a review from alii June 30, 2026 09:34
Comment thread src/md/parser.rs Outdated
@robobun

robobun commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

CI status for build 67192 (fc66f1f, finished): 283 jobs passed and 3 failed. test/js/bun/md/md-edge-cases.test.ts, including the new block-metadata cap test, passed on every lane that ran it, and all three failures are in files or steps this PR does not touch:

I am not pushing a retrigger for those, so this stays red until a maintainer re-runs or merges. Review feedback is addressed: all five of @alii's comments plus the later bot nit are replied to and resolved, and the PR body is rewritten.

@Jarred-Sumner
Jarred-Sumner merged commit 52a1ddf into main Jun 30, 2026
77 of 79 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/fbc702e4/md-internal-limits branch June 30, 2026 21:38
@alii

alii commented Jun 30, 2026

Copy link
Copy Markdown
Member

@robobun I left a review with 5 inline comments: #33078 (review) — please address them on this branch.

Required before merge:

  • ansi_renderer.rs:329 — finish the ANSI OOM class: guard write_content's staging buffers (image_alt/code_buf/heading_buf/table_cell_buf, plus the raw-write twin, the styled path, and the table/segment pushes) the same way html_renderer.rs already guards its heading_buf, and hoist the now byte-identical OutputBuffer into one shared definition instead of two copies. Fix resolve_href's vestigial AllocError signature while you're there.
  • parser.rs:184 — give TooManyBlocks real coverage: add setMaxMarkdownBlockBytesForTesting(n) to bun:internal-for-testing modeled on setSyntheticAllocationLimitForTesting (STRING_ALLOCATION_LIMIT in bun_core/string/mod.rs), then a subprocess test that lowers the limit and asserts the exact error code/message plus the at-the-limit/one-past boundary. Also add the const _: () = assert!(...) (or a #[cfg(test)] test — the miri lane already runs cargo miri test -p bun_md) encoding the MAX_BLOCK_BYTES headroom invariant.

Should-fix:

  • parser.rs:162 — the MAX_LOOKAHEAD doc comment's proof is wrong (match_html_tag does an 11+-byte lookahead in usize); state the real invariant (longest OFF-typed fixed addition) and derive the constant from the CDATA probe so they can't drift.
  • md-edge-cases.test.ts:1248 — drop the inner timeout: 20_000, killSignal: "SIGKILL" (the 30s outer already reaps the child via the auto-killer), or rescale it to the file's 30s-inner/90s-outer convention and have the parent distinguish a SIGKILLed child from the regression.

Non-blocking but take it if it's clean:

  • parser.rs:176 — extract the duplicated align/check/grow/write-header block from push_container_bytes + start_new_block into one append_block_header, and annotate the three casts the bound makes safe. Either way, fix the two factual errors in the PR body (current_block is usize not u32; BlockHeader.data never holds a block offset), and don't over-claim item 3 if you don't finish the staging buffers.

When you're done, reply summarizing what you changed vs. deliberately skipped, per comment.

@robobun

robobun commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

@alii all five were addressed this morning in 240c35a, b5b7ffc, and fc66f1f, which are all in the squash Jarred merged (52a1ddf). Each inline thread has a specific reply and is resolved; here is the changed vs. deliberately-skipped summary per comment.

1. ansi_renderer.rs:329 (required)
Changed: OutputBuffer is hoisted into src/md/output.rs as the single definition, with try_extend/try_push helpers; routed through them are write_content's four staging sinks (image_alt, code_buf, heading_buf, table_cell_buf), the raw-write twin, the styled path, and the table_rows/table_cells pushes, all recording onto the same out.oom that render_to_ansi already checks. The HTML renderer's three hand-rolled try_reserve blocks use the same helpers. resolve_href returns Box<[u8]>.
Deliberately skipped: block_stack, the per-column segment vecs, and the highlighter scratch stay on plain Vec growth. They are per-node or per-row scratch rather than the output-scale staging your comment centered on, and ordinary Vec growth elsewhere in the renderer and the whole parser still follows the global abort-on-OOM policy, as you noted. The PR body says that scope explicitly instead of claiming the class is closed. Happy to put up a small follow-up moving those onto the same flag if you want them too.

2. parser.rs:184 (required)
Changed: setMaxMarkdownBlockBytesForTesting(n) in bun:internal-for-testing, modeled on setSyntheticAllocationLimitForTesting (a Relaxed AtomicUsize next to MAX_BLOCK_BYTES that check_block_bytes_len loads); it returns the previous value and can only lower the cap, never raise it past the real one. The new subprocess test shrinks the cap to exactly 40 single-line paragraphs of metadata (28 bytes each: one 16-byte BlockHeader plus one 12-byte VerbatimLine) and proves the boundary: 40 render, 41 throw, a 64-deep nested blockquote throws through the container-header path, and both render again after restoring the previous value. The ERR_OUT_OF_RANGE code and message are asserted verbatim. The headroom invariant is a const _: () = assert!(..) next to MAX_BLOCK_BYTES.
Deliberately skipped: the #[cfg(test)] miri variant of the invariant, since the const assert is the same proof but fails cargo check on every lane, not just the miri one.

3. parser.rs:162 (should-fix)
Changed: the probe's literal is line_analysis::CDATA_OPEN, MAX_LOOKAHEAD is defined as 1 + CDATA_OPEN.len(), and the probe's bounds check is written against parser::MAX_LOOKAHEAD, so they cannot drift. The doc states the real invariant (longest OFF-typed fixed addition) and why match_html_tag is exempt (it adds in usize). Nothing skipped.

4. md-edge-cases.test.ts:1248 (should-fix)
Changed: took the first option and dropped timeout: 20_000, killSignal: "SIGKILL" plus its comment; the pre-existing 30s outer is the only timeout on that test.
Deliberately skipped: the 30s-inner/90s-outer rescale alternative, since removal is strictly simpler once the outer reaps the child.

5. parser.rs:176 (non-blocking)
Took all of it. Parser::append_block_header is now the only header-append path, leaving exactly the two growers (header append and end_current_block's line append); the three block_bytes.len() casts carry an "every grower enforces check_block_bytes_len" comment; and the PR body was rewritten. You were right on both facts, and BlockHeader.data was also wrongly cited in the MAX_BLOCK_BYTES doc comment itself, which fc66f1f fixes.

Verification on the final head: bun bd test test/js/bun/md/ 1064 pass, cargo test -p bun_md 14 pass, and CI build 67192 was green everywhere the diff is exercised (the three red jobs were the alpine test-net-connect-memleak.js flake and a crates.io DNS error in windows verify-baseline, detailed in the status comment above).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants