Skip to content

process: write process.title through to the OS on Linux and coerce non-string assignments - #34618

Closed
robobun wants to merge 3 commits into
mainfrom
claude/farm/fd3612d1/process-title-os-write
Closed

robobun wants to merge 3 commits into
mainfrom
claude/farm/fd3612d1/process-title-os-write

Conversation

@robobun

@robobun robobun commented Jul 18, 2026 •

Copy link
Copy Markdown
Collaborator

What

process.title = "name" was a JS-only shadow: the setter stored the value in a Rust static so the getter read it back, but never touched the kernel. /proc/self/{comm,cmdline} (and therefore ps, top, pgrep, pkill) kept showing bun. Node writes both via uv_set_process_title.

Separately, the setter used dynamicDowncast<JSString> and silently dropped non-string assignments. Node coerces via ToString: process.title = 42 reads back as "42", and Symbol throws TypeError.

Repro

import fs from "node:fs";
const comm = () => fs.readFileSync("/proc/self/comm", "utf8").trim();
const cmd0 = () => fs.readFileSync("/proc/self/cmdline").toString().split("\0")[0];
process.title = "renamedproc";
const r = { readback: process.title, comm: comm(), cmdline0: cmd0().split("/").pop() };
process.title = 42;
r.numAssign = String(process.title) + "/" + typeof process.title;
console.log(JSON.stringify(r));
// node: {"readback":"renamedproc","comm":"renamedproc","cmdline0":"renamedproc","numAssign":"42/string"}
// bun before: {"readback":"renamedproc","comm":"bun","cmdline0":"bun","numAssign":"renamedproc/string"}
// bun after:  {"readback":"renamedproc","comm":"renamedproc","cmdline0":"renamedproc","numAssign":"42/string"}

Cause

  • Bun__Process__setTitle in src/runtime/node/node_process.rs only stored the bytes into the Bun__Node__ProcessTitle static; no syscall was ever made on POSIX. (On Windows the C++ setter already called uv_set_process_title.)
  • setProcessTitle in src/jsc/bindings/BunProcess.cpp used dynamicDowncast<JSC::JSString> and returned false for any non-string RHS, so the assignment was a no-op instead of coercing.

Fix

  • bun_core::os_argv_title_span() computes (and caches via Once) the contiguous kernel argv byte span starting at argv[0], after forcing argv_storage() so the original strings are already copied into owned ZBoxes and overwriting the kernel block cannot be observed via bun_core::argv() / process.argv.
  • bun_core::set_process_title(title) on Linux performs prctl(PR_SET_NAME) (updates /proc/self/comm, kernel-truncated to 15 bytes) and an in-place NUL-padded overwrite of the argv span (updates /proc/self/cmdline). No-op on other platforms; Windows keeps using uv_set_process_title, and macOS stays JS-readback-only (the TODO referencing libuv's darwin-proctitle.c remains).
  • Bun__Process__setTitle and the --title CLI flag now call set_process_title in addition to updating the static. The setter holds the title mutex across the OS write so concurrent setters serialise.
  • The C++ setter now coerces via JSValue::toString(globalObject) with RETURN_IF_EXCEPTION, matching Node (42 -> "42", object with toString invoked, null -> "null", Symbol throws).

Verification

  • New tests in test/js/node/process/process.test.js cover ToString coercion (all platforms), /proc/self/{comm,cmdline} write-through for process.title = and --title (Linux only via it.skipIf(!isLinux)), and the long-title case (JS readback is full-length, comm is 15-byte, cmdline is capped to the argv span).
  • USE_SYSTEM_BUN=1 bun test test/js/node/process/process.test.js -t title: new tests fail.
  • bun bd test test/js/node/process/process.test.js -t title: 4 pass.
  • process.argv / process.argv0 / process.execPath verified unchanged after a title write (they read from owned copies).
  • BUN_JSC_validateExceptionChecks=1 clean for the Symbol throw path.
  • bun run rust:check-all: 10/10 targets.

no test proof · iteration 3 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/process/process.test.js

…n-string assignments

process.title = "name" was a JS-only shadow: the setter stored the value in
a Rust static so the getter read it back, but never touched the kernel, so
/proc/self/{comm,cmdline} (and thus ps/top/pgrep/pkill) kept showing "bun".
Node writes both via uv_set_process_title.

Separately, the setter used dynamicDowncast<JSString> and silently dropped
non-string assignments where Node coerces via ToString (process.title = 42
should read back as "42", Symbol should throw TypeError).

Fix:
- bun_core: add os_argv_title_span() which computes (once) the contiguous
  kernel argv byte span, after ensuring argv has been copied to owned
  storage, and set_process_title() which on Linux does prctl(PR_SET_NAME)
  plus an in-place NUL-padded overwrite of that span.
- node_process.rs / Arguments.rs: call set_process_title() from the
  process.title setter and the --title flag.
- BunProcess.cpp: coerce the setter RHS via JSValue::toString() instead of
  dynamicDowncast, with RETURN_IF_EXCEPTION for the Symbol case.
@robobun

robobun commented Jul 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: diff is green; ready for maintainer review.

CI build #75447 (re-roll): 282/286 jobs passed. test/js/node/process/process.test.js passed on every lane. The two red jobs are both debian x64-asan main breaks unrelated to this diff:

  • test/js/node/test/parallel/test-worker-message-port-transfer-terminate.js: pre-existing JSC assertion (!scope.exception() || !hasSlot).
  • test/js/node/test/parallel/test-http2-reset-flood.js: intermittent ExceptionScope::assertNoException assertion; also red on unrelated branches (builds 75421, 75414). This diff does not touch http2.

The remaining annotations are retry-passed flakes (terminal, install, repl, webview, etc.). The darwin x64 bake timeouts seen on the previous run (#75338) were a single-box job timeout on darwin-pretzel-x64-1 and did not reproduce on the re-roll; on macOS this PR's Rust changes compile to a let _ = title; no-op.

Reproduce locally:

USE_SYSTEM_BUN=1 bun test test/js/node/process/process.test.js -t title   # fails
bun bd test test/js/node/process/process.test.js -t title                  # passes

@robobun

robobun commented Jul 18, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 9:34 AM PT - Jul 18th, 2026

❌ @robobun, your commit 98e645c has 2 failures in Build #75447 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34618

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

bun-34618 --bun

@coderabbitai

coderabbitai Bot commented Jul 18, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 4 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a8939819-b7dd-45c2-8ff6-f58d43b3effb

📥 Commits

Reviewing files that changed from the base of the PR and between 511caaa and 98e645c.

📒 Files selected for processing (6)
  • src/bun_core/Global.rs
  • src/bun_core/util.rs
  • src/jsc/bindings/BunProcess.cpp
  • src/runtime/cli/Arguments.rs
  • src/runtime/node/node_process.rs
  • test/js/node/process/process.test.js

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

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

Beyond the inline nit, I checked: (1) the --title CLI path passes a slice into the owned argv_storage() ZBox, not the kernel block, so copy_nonoverlapping src/dest don't overlap; (2) os_argv_title_span() forces argv_storage() before returning the span, so bun_core::argv() / process.argv never observe the overwritten bytes; (3) the argv-span write is bounded (n ≤ cap-1, remainder NUL-padded, cap ≥ 1 since end ≥ first + strlen(first)+1); (4) the new toString(globalObject) is followed by RETURN_IF_EXCEPTION for the Symbol/throwing-toString case. Deferring to a human because the change adds unsafe in-place mutation of the kernel argv block.

Extended reasoning...

This PR adds unsafe raw-pointer writes into the process-lifetime kernel argv region and changes C++ JSC exception-path behavior. The bug-hunting pass surfaced only a test-style nit; the concrete memory-safety concerns I traced (source/dest overlap on the CLI path, argv() aliasing, span bounds, ToString exception check) all hold up. But per the approval bar, new unsafe blocks that mutate process-global kernel memory are not the kind of change to auto-approve — a human should sign off on the provenance/aliasing reasoning in os_argv_title_span and set_process_title.

Comment thread test/js/node/process/process.test.js
Per review: fold stderr/stdout/exitCode into a single toEqual so a failing
run surfaces all three in the diff instead of short-circuiting on stderr.

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

No bugs found, but this adds new unsafe code that overwrites the kernel argv block in-place — worth a human look before merging.

What was reviewed:

  • os_argv_title_span() pointer arithmetic and Once caching; argv_storage() is forced first so the overwrite cannot alias bun_core::argv()'s owned ZBoxes or the CLI-parsed --title slice.
  • set_process_title bounds: n = min(len, cap-1) + NUL-pad cap-n; no underflow when cap >= 1 (always true since span includes argv[0]'s NUL).
  • C++ setter: toString() + RETURN_IF_EXCEPTION before use; downstream Bun::toStringRef path unchanged.
  • Mutex held across the OS write in set_title; CLI --title path is single-threaded startup.
Extended reasoning...

Overview

This PR makes process.title = ... write through to the OS on Linux (via prctl(PR_SET_NAME) + in-place rewrite of the kernel argv block, matching libuv's uv_set_process_title) and changes the C++ setter to coerce the RHS via ToString instead of silently dropping non-string assignments. It touches bun_core (two new functions with unsafe blocks), BunProcess.cpp (JSC exception path), the CLI --title handler, and node_process.rs. Four new tests cover coercion (all platforms) and /proc/self/{comm,cmdline} write-through (Linux-only).

Security risks

None identified. The argv-block rewrite is bounded by the cached span length computed from the original kernel argv layout; the title bytes come from an owned Box<[u8]> (JS path) or a ZBox-backed argv slice (CLI path), never from the kernel block itself, so copy_nonoverlapping is sound. PR_SET_NAME is truncated to 15 bytes into a zeroed 16-byte stack buffer.

Level of scrutiny

Moderate-to-high. The change is small and well-motivated (Node compat), but it introduces raw-pointer writes into a process-global memory region, caches a *mut u8 as usize in a Once, and relies on argv_storage() having already copied the original strings before the first overwrite. These invariants all check out on my read, and the technique mirrors libuv exactly, but per REVIEW.md's memory-safety guidance ("Never let a pointer or slice outlive the memory it points into", "exactly one named owner") this is the kind of unsafe code a maintainer should sign off on rather than a bot.

Other factors

The prior inline nit (combined-object assertion for the --title subprocess test) was addressed in 104da5d. Tests pass locally per the PR description with fail-before/pass-after verification and rust:check-all across 10 targets. macOS remains a no-op (existing TODO preserved); Windows keeps its uv_set_process_title path. The one thing I'd flag for the human reviewer to sanity-check is whether any other code path (e.g. crash-handler command-line dumping, bun_crash_handler) reads the raw kernel argv block directly rather than via bun_core::argv() — I found none, but the reviewer will know the codebase better.

@robobun

robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

Re the crash-handler question in the review: verified that nothing reads the raw kernel argv block after startup. OS_ARGV is only loaded inside raw_os_argv(), which is only called from argv_storage() (populates the owned ZBox copies) and the new os_argv_title_span(). The crash handler's command-line dump paths (src/crash_handler/lib.rs:910,1852,2196,2990) all go through bun_core::argv() / Output::argv(), which read the owned copies in ARGV_STORAGE, so a title rewrite cannot affect crash output.

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

No issues found, but this adds new unsafe code that overwrites the kernel argv block in place — worth a human maintainer confirming the aliasing/lifetime story and the choice to leave macOS as JS-readback-only.

What was reviewed:

  • os_argv_title_span caches the span via Once before any overwrite and forces argv_storage() first, so bun_core::argv()/crash-handler dumps read owned copies (robobun traced the only OS_ARGV readers).
  • set_process_title bounds: n ≤ cap-1 so write_bytes(.., cap-n) always writes ≥1 NUL and never overruns; prctl buffer is 16-byte zeroed.
  • C++ setter's toString + RETURN_IF_EXCEPTION is correct for Symbol; toStringRef on the resulting JSString* is the same call path as before.
  • The prior stderr-assertion nit was applied in 104da5d for the --title test.
Extended reasoning...

Overview

This PR makes process.title = ... write through to the OS on Linux (via prctl(PR_SET_NAME) + in-place rewrite of the kernel argv block, matching libuv's uv_set_process_title), and changes the C++ setter to coerce non-string RHS via ToString instead of silently dropping them. Touches: bun_core/Global.rs (new set_process_title), bun_core/util.rs (new os_argv_title_span), BunProcess.cpp (setter coercion), node_process.rs (hold lock across OS write), Arguments.rs (--title flag wires through), plus tests.

Security risks

None user-facing. The new unsafe blocks write to process-owned memory (the CRT/kernel argv block) that the process already has full write access to; no external input controls the write bounds beyond min(title.len(), cap-1). No auth/crypto/permissions.

Level of scrutiny

Medium-high. This is not mechanical: it introduces two new unsafe blocks doing raw copy_nonoverlapping/write_bytes into a kernel-managed byte region, plus a Once-cached raw pointer stored as usize. The approach is standard (it's what libuv/Node do), the SAFETY comments are accurate, and the span computation correctly caches before any rewrite so later strlen calls can't observe NUL-padded results. But REVIEW.md flags memory safety as the most-blocked category, and a human should confirm they're comfortable with (a) the in-place argv rewrite on Linux and (b) leaving macOS as a no-op (Node writes through there too via libuv's darwin-proctitle path; the existing TODO is preserved but not addressed).

Other factors

  • CI green (281/286; the one red job is a pre-existing unrelated JSC assertion on main).
  • Tests cover ToString coercion (all platforms), /proc write-through for both the setter and --title, and the long-title truncation behavior.
  • rust:check-all passed 10/10 targets per the PR description.
  • The one nit I raised previously (combined-object subprocess assertion) was applied in 104da5d for the --title test; the author left the JSON-parsed test as-is with a reasonable justification.
  • I checked that prctl(PR_SET_NAME) from a Worker thread would set the worker thread's comm rather than the main process's — this matches Node/libuv behavior (libuv's uv_set_process_title also calls prctl on the calling thread), so not a divergence.

@robobun

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-18, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main.

@robobun robobun closed this Sep 13, 2026
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.

1 participant