Skip to content

Lower the untyped one-string process.stdout.write(text) call directly - #2964

Merged
ctate merged 2 commits into
mainfrom
ctate/lower-untyped-one-021a3e
Nov 4, 2026
Merged

ctate merged 2 commits into
mainfrom
ctate/lower-untyped-one-021a3e

Conversation

@ctate

@ctate ctate commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator
  • An untyped single-string process.stdout.write(text) or process.stderr.write(text) now reaches one runtime call instead of a boxed receiver, a method read and a boxed chunk; a replaced write still receives the call with the same arguments, this and result.
  • 10,000,000 writes to /dev/null: 3.13 s to 1.38 s (ratio 0.4413, 95% CI [0.4337, 0.4476], A/A [0.9705, 1.0018]), 313 to 138 ns per call, and the benchmark binary shrinks from 565,048 to 378,640 bytes.

At an untyped call site, process.stdout.write(text) and
process.stderr.write(text) with a single string argument went through
dynamic method dispatch: box the argument, read the write slot, and
invoke through the handle. The lowering now emits
process.stdioWriteStr(fd, text), and the new runtime entry point writes
the string when the slot is still the built-in method and otherwise
makes the same call the site made before, with the string as its only
argument and the stream as `this`.

The write still costs one call and one write per chunk, and a replaced
write keeps its boolean result. The write-slot feature rule lists the new
entry point, so a program that does not use this call form links the
objects it linked before.
@vercel

vercel Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
scriptc Ready Ready Preview, v0 Nov 4, 2026 7:47am UTC

Comment thread packages/runtime/src/scr_events.c Outdated
Comment on lines +1463 to +1464
ScrDyn *result = scr_dyn_call(stream->write, &chunk, 1,
stream->fd == 2 ? "process.stderr.write" : "process.stdout.write");

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.

When a replacement write restores or replaces itself during the call, the unretained function can be freed along with its captured bindings, causing a use-after-free when the callback continues.

Fix on Vercel

The untyped one-string write calls a replacement process.stdout.write
straight from the stdio slot, and borrowed that slot value for the call.
A replacement that assigns process.stdout.write during its own call —
restoring the built-in method, for one — drops the slot's reference to
the function that is running, and its captured bindings go with it while
the body still reads them. Hold a reference for the duration of the call
and drop it after the return, as the other dispatch paths do.

tests/corpus/process-stdout-write-form.js now covers that shape: the
replacement restores the built-in method before it accumulates its
argument and reports the accumulated length. Without the extra
reference the sanitized native run stops at the write.
@ctate
ctate merged commit 80735fb into main Nov 4, 2026
73 of 76 checks passed
@ctate
ctate deleted the ctate/lower-untyped-one-021a3e branch November 4, 2026 08:52

This branch was successfully deployed

1 active deployment
Preview — 3404951f Deployed Nov 4, 2026 by vercel[bot]
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