Skip to content

orderfile: stop a traced process's children from truncating its trace - #33347

Merged
Jarred-Sumner merged 1 commit into
mainfrom
farm/31f1380d/pagetrace-child-truncate
Jul 5, 2026
Merged

Jarred-Sumner merged 1 commit into
mainfrom
farm/31f1380d/pagetrace-child-truncate

Conversation

@robobun

@robobun robobun commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #33302, per this review comment.

The bug

pagetrace.c's constructor opened BUN_PAGETRACE_OUT with O_CREAT|O_TRUNC and ftruncated it before reading /proc/self/maps to find out whether this process is even the binary under trace:

int fd = open(out, O_CREAT | O_TRUNC | O_RDWR, 0644);   // ← truncates
...
record = mmap(NULL, bytes, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
...
fclose(maps);
if (!region_count) return;                              // ← too late

LD_PRELOAD and both BUN_PAGETRACE_* variables are inherited across exec, so any dynamically linked child a workload spawns — a lifecycle script, /bin/sh, another bun — re-ran the constructor, zeroed the MAP_SHARED trace its parent was still writing into, and only then bailed. The pages lost are the earliest-touched ones, which is to say the hottest.

The fix

Find the regions first and leave the file alone when there are none. Then drop LD_PRELOAD and the two BUN_PAGETRACE_* variables from the environment once loaded, so a child that does map the same binary (another bun) never arms itself either — the region check alone can't catch that one. ptyrun already hands the preload down explicitly via PTYRUN_PRELOAD rather than relying on inheritance, so nothing depended on it; its comment is updated to say so.

Impact today

None, which is why it was still there. No workload currently spawns, and the order file comes out byte-for-byte the same work:

                      before fix            after fix
bun -e                2108 pages  +21636    2108 pages  +21636
bun hello.ts          2168 pages    +631    2168 pages    +631
bun server.js         2611 pages   +4809    2611 pages   +4809
bun test              3057 pages   +5174    3057 pages   +5174
bun install            630 pages   +1440     630 pages   +1440
bun install (cached)   540 pages     +83     540 pages     +83
bun cli.js (pipe)     3075 pages   +2024    3075 pages   +2024
bun cli.js (tty)      3348 pages   +2003    3369 pages   +2137   (pty jitter)

But bun install is one package.json away from running a lifecycle script, and the failure is silent: a truncated trace still produces an order file, just one missing the hot core.

Test

test/js/bun/perf/pagetrace-fixture.c reads one byte from each of 32 pages of its own .rodata, execs a dynamically linked child, then reads one more. Under the tracer each read is a recorded fault, so the count is the signal:

before:  Expected: >= 33   Received: 1
after:   9 pass, 1 skip, 0 fail

The stride is 64 KB so the page count is the same on kernels with 16 KB or 64 KB pages. Skipped where there's no C compiler, alongside the existing ptyrun test.

pagetrace.c's constructor opened BUN_PAGETRACE_OUT with O_CREAT|O_TRUNC and
ftruncated it before reading /proc/self/maps to find out whether this process
is the binary under trace. LD_PRELOAD and both BUN_PAGETRACE_* variables are
inherited across exec, so any dynamically linked child a workload spawns — a
lifecycle script, /bin/sh, another bun — re-ran the constructor, zeroed the
MAP_SHARED trace its parent was still writing into, and only then bailed. The
pages lost are the earliest-touched ones, which is to say the hottest.

Find the regions first and leave the file alone when there are none, and drop
LD_PRELOAD and the two BUN_PAGETRACE_* variables from the environment once
loaded, so a child that does map the same binary never arms itself either.
ptyrun already hands the preload down explicitly rather than relying on
inheritance, so nothing depended on it.

Nothing in today's workloads spawns, and the order file comes out the same
(37.9k functions, same per-workload page counts), but `bun install` is one
package.json away from running a lifecycle script. The new test traces a
fixture that reads 32 pages, execs a child, then reads one more: before this
it recorded 1 page, now it records all 33.

Reported by claude[bot] on #33302.
@github-actions github-actions Bot added the claude label Jul 5, 2026
@robobun

robobun commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:35 PM PT - Jul 4th, 2026

@robobun, your commit 6d171fa is building: #68433

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: dc3d7ed9-cd7b-43ed-93c0-46f5f1de7d9a

📥 Commits

Reviewing files that changed from the base of the PR and between bb49700 and 6d171fa.

📒 Files selected for processing (4)
  • scripts/orderfile/pagetrace.c
  • scripts/orderfile/ptyrun.c
  • test/js/bun/perf/linker-order.test.ts
  • test/js/bun/perf/pagetrace-fixture.c

Walkthrough

The pagetrace.c constructor now reads and unsets environment variables (LD_PRELOAD, BUN_PAGETRACE_BIN/OUT) before scanning /proc/self/maps, deferring output-file creation/mmap until after region discovery. ptyrun.c's comment is updated accordingly. A new test suite and C fixture validate page-tracing behavior across fork/exec.

Changes

Pagetrace init reordering and tests

Layer / File(s) Summary
Pagetrace init env handling and deferred output setup
scripts/orderfile/pagetrace.c
Environment variables are read/unset earlier, and output-file creation/mmap is deferred until after region scanning.
ptyrun.c preload comment update
scripts/orderfile/ptyrun.c
Comment updated to reflect that PTYRUN_PRELOAD is passed down as the child's LD_PRELOAD, with the tracer removing itself once loaded.
Shared compile helper and page tracer test suite
test/js/bun/perf/linker-order.test.ts, test/js/bun/perf/pagetrace-fixture.c
A shared compile() helper replaces a duplicated local function; a new Linux-only test builds tracer/fixture/child binaries, runs them, and validates the trace output header; a new C fixture performs deterministic page touches and fork/execl.

Sequence Diagram(s)

sequenceDiagram
  participant Test as linker-order.test.ts
  participant Fixture as pagetrace-fixture
  participant Tracer as pagetrace.c (LD_PRELOAD)
  participant Child

  Test->>Fixture: spawn with LD_PRELOAD=tracer, BUN_PAGETRACE_BIN/OUT
  Fixture->>Fixture: touch .rodata pages (STRIDE reads)
  Fixture->>Child: fork + execl(child)
  Child-->>Fixture: exit
  Fixture->>Fixture: final touched-page read, print sum
  Test->>Test: read trace output, parse header, assert results
Loading

Estimated code review effort: Medium

Suggested labels: testing, linux, orderfile

Suggested reviewers: none identified from provided context

Poem
A rabbit traced each page it found,
Env vars hushed without a sound,
Fork, then exec, the child runs free,
A header counts what pages be,
Hop, hop — the trace is safe and sound. 🐇

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: preventing traced children from truncating the parent trace.
Description check ✅ Passed The description explains the bug, the fix, impact, and verification, covering the template’s required content.
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.

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

@Jarred-Sumner
Jarred-Sumner merged commit 455efaa into main Jul 5, 2026
40 of 55 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/31f1380d/pagetrace-child-truncate branch July 5, 2026 04:45

@claude claude 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.

LGTM — focused build-tooling fix with a solid regression test.

Extended reasoning...

Overview

This PR reorders scripts/orderfile/pagetrace.c's constructor so it checks /proc/self/maps for the traced binary's regions before opening/truncating the output file, and additionally unsetenvs LD_PRELOAD + the two BUN_PAGETRACE_* vars once loaded so exec'd children never re-arm the tracer. The env values are correctly copied to stack buffers before unsetenv (which may invalidate getenv returns). ptyrun.c gets a comment-only update. A new C fixture and test in test/js/bun/perf/linker-order.test.ts verify a child exec no longer wipes the parent's trace.

Security risks

None. This is a Linux-only, build-time LD_PRELOAD shim used exclusively by scripts/orderfile/generate.ts during release builds to produce the linker symbol-ordering file. It is not shipped in the bun binary and has no user-facing surface.

Level of scrutiny

Low-to-medium. The changed code is developer tooling under scripts/, not runtime code — worst case is a suboptimal .text layout, which the existing verifyOrderFileApplied() CI check would still catch. The core change is a mechanical reordering of two existing blocks (region discovery ↔ file creation) plus three unsetenv calls, and the PR description confirms the generated order file is byte-identical for all current workloads.

Other factors

  • Follow-up to a specific review comment on #33302; scope is exactly what was asked for.
  • The regression test is well-constructed: compiles the real tracer, uses a fixture that touches 32 distinct 64 KB-strided .rodata pages, fork+execs a dynamically linked child, then touches one more — asserting ≥33 recorded faults. PR shows it fails (count=1) before the fix and passes after. Skipped on non-Linux / no compiler, uses tempDir/bunEnv/await using, drains pipes concurrently.
  • The compile() helper was hoisted unchanged from the existing pty runner describe block to share it — no behavior change there.
  • Minor: the test asserts stderr: "" on the fixture, which CLAUDE.md discourages for bun-debug spawns, but this is a plain cc -O1 C binary with no sanitizers, so it's fine here.

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.

2 participants