Skip to content

[RSI, security] fix(kernel): keep the kernel stderr log owner-only - #2425

Open
sethkarten wants to merge 2 commits into
mainfrom
rsi/sec-band-85
Open

sethkarten wants to merge 2 commits into
mainfrom
rsi/sec-band-85

Conversation

@sethkarten

@sethkarten sethkarten commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

What

The kernel stderr log (kernel-stderr.log in the session artifact directory) was created world-readable (openSync(path, "a") default mode -> 0644) while every sibling artifact in the same private directory is owner-only: kernel-state.dill/kernel-state.json 0600, semantic-edges.jsonl 0600, rlm-subagent.json 0600, and the daemon journals use 0700 dirs + 0600 files. Kernel stderr can carry Python exception payloads and library warnings, so it belongs to the private set.

This creates the log owner-only (0600) and its directory owner-only (0700) when the kernel manager itself creates it, with fchmodSync on the opened descriptor so the exact bits hold despite the umask (and a pre-existing loose log is tightened on the next kernel start).

Why this shape

  • mkdirSync(dirname(path), { recursive: true, mode: 0o700 }) mirrors command-recovery-journal.ts, worker-recovery-journal.ts, and rlm-ledger.ts.
  • openSync(path, "a", 0o600) + fchmodSync mirrors auth-storage.ts's fchmod idiom; the stderr log is a persistent append fd with a byte budget, so the writeFileAtomicSync temp+rename pattern does not apply. The fd is closed if the fchmod fails (no leak window).
  • Rotation (renameSync to .old) preserves the mode, so rotated logs stay owner-only.

Tests

Extends the existing teardown test in test/repl-kernel-startup.test.ts (fake runtime that exits before ready, log path in a nested dir the manager creates) with two assertions: file mode 0600 and directory mode 0700. Both fail pre-fix (0644/0755) and pass post-fix; proven pre-fix by stashing the source change and rerunning (received 0o644 vs expected 0o600).

Validation

  • npm run check clean at the tip: biome, check:test-policy (vs origin/main), tsgo --noEmit, check:installer, check:browser-smoke.
  • npx vitest --run test/repl-kernel-startup.test.ts: 5/5.
  • Test-line budget: net test additions 2 vs 21 meaningful source additions.

Dedup

Complementary, not overlapping: #1249 (private-files.ts utility; covers session-manager/config/state-snapshot/agent-session storage; does not touch this log) and #2174 (kernel-env allowlist hunks in the same file, disjoint from openStderrLog; its docs explicitly keep the stderr log as-is). No bash.py guard-class overlap with #2373/#2384/#2390/#2395/#2413/#2415.


Note

Low Risk
Narrow filesystem permission change with tests; opening may fail if the process cannot chmod a log owned by another user.

Overview
Hardens permissions on kernel-stderr.log so it matches other private session artifacts (kernel state, semantic edges, etc.), since stderr can include Python exception payloads.

ReplKernelManager.openStderrLog now creates the log directory at 0700 and opens the log at 0600, with fchmodSync on the descriptor so mode holds under umask and existing loose files are tightened on the next open. Before rotation to .old, chmodSync runs so historical rotated logs are not left world-readable.

Startup tests gain shared fakeRuntime / withManager helpers plus assertions on file and directory modes, including a case that seeds an oversized 0644 log and expects rotation to produce 0600 on both the new file and .old.

Reviewed by Cursor Bugbot for commit fee75f1. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix kernel stderr log permissions to be owner-only in ReplKernelManager

Sets the kernel stderr log directory to mode 0700 when newly created and keeps current and rotated log files at mode 0600 in repl-manager.ts. Before rotating an oversized existing log, the current file is tightened to 0600 so the renamed .old file retains restricted permissions. If fchmod fails on a log descriptor, the descriptor is closed and the operation fails.

  • Tests in repl-kernel-startup.test.ts verify the directory is 0700 and log files are 0600, including a case that tightens a pre-existing world-readable oversized log before rotation.
  • Behavioral Change: pre-existing stderr logs with looser permissions are now tightened to 0600 on open and before rotation; callers relying on group/world readability of these files will lose access.

Macroscope summarized fee75f1.

The kernel stderr log (session-artifacts/<id>/kernel-stderr.log) was created
world-readable (0644) while every sibling artifact in the same private
directory is owner-only (0600 files, 0700 dirs). Kernel stderr can carry
exception payloads and library warnings, so the log and its directory must
match the other private session artifacts.

Create both with owner-only bits: mode 0o700 for the directory and 0o600 for
the file, with fchmodSync on the opened descriptor to enforce the exact bits
despite the umask (and tighten a pre-existing loose log on next kernel start).
@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown

Prime Agent performance — completed

PR fee75f11 compared with main b6ac5d01.

Overall: 0 regressed · 1 improved · 39 no clear change.

Metric Main This PR Change
Cold startup 1,479.1 ms 1,379.2 ms ≈ -99.9 ms (-6.76%)
Warm startup 883.9 ms 794.9 ms ≈ -89.0 ms (-10.06%)
Installation 13.62 s 13.00 s ≈ -0.62 s (-4.56%)
Compressed release artifacts 72.48 MB 71.13 MB $\textcolor{#65816c}{\textsf{↓ -1.35 MB (-1.87\%)}}$
Installed footprint 577.63 MB 574.25 MB ≈ -3.38 MB (-0.59%)
Idle memory, summed RSS 1,169.97 MB 1,137.01 MB ≈ -32.96 MB (-2.82%)

Python runtime

Metric Main This PR Change
Python kernel startup 134.8 ms 134.9 ms ≈ +0.1 ms (+0.08%)
Python cell round trip 0.529 ms 0.496 ms ≈ -0.033 ms (-6.23%)
Empty bash command 11.7 ms 10.9 ms ≈ -0.8 ms (-6.48%)
Bash git status 18.1 ms 16.3 ms ≈ -1.8 ms (-9.93%)
Bash 32 KiB output 11.8 ms 11.2 ms ≈ -0.7 ms (-5.52%)
35 cells / 9 shell calls 169.0 ms 163.3 ms ≈ -5.7 ms (-3.36%)
Python interrupt to done 1.601 ms 1.513 ms ≈ -0.088 ms (-5.48%)
Python state snapshot 25.7 ms 25.5 ms ≈ -0.2 ms (-0.62%)
Python state restore 360.0 ms 356.9 ms ≈ -3.1 ms (-0.86%)
Python idle RSS 34.52 MB 35.95 MB ≈ +1.43 MB (+4.15%)
Python RSS after pandas workload 96.59 MB 97.99 MB ≈ +1.40 MB (+1.45%)

UI interactions

Metric Main This PR Change
Resume large session (cold) 4,012.5 ms 3,602.6 ms ≈ -409.9 ms (-10.21%)
CPU, resume large session 6,520.0 ms 5,970.0 ms ≈ -550.0 ms (-8.44%)
Switch into large session 5,396.0 ms 5,329.2 ms ≈ -66.8 ms (-1.24%)
CPU, switch into large session 9,550.0 ms 9,530.0 ms ≈ -20.0 ms (-0.21%)
Open agents view from a session 165.5 ms 160.4 ms ≈ -5.2 ms (-3.12%)
CPU, open agents view 330.0 ms 360.0 ms ≈ +30.0 ms (+9.09%)
Full agents roster, many sessions 4.46 s 4.45 s ≈ -0.0043 s (-0.10%)
CPU, full agents roster 3.19 s 2.77 s ≈ -0.42 s (-13.17%)
Open another session from agents view 2,688.1 ms 2,640.5 ms ≈ -47.6 ms (-1.77%)
CPU, open from agents view 2,860.0 ms 2,680.0 ms ≈ -180.0 ms (-6.29%)
Reopen resident large session 615.7 ms 566.5 ms ≈ -49.2 ms (-7.99%)
CPU, reopen resident session 1,040.0 ms 960.0 ms ≈ -80.0 ms (-7.69%)
Open subagent session at depth 6 21,187.3 ms 20,777.1 ms ≈ -410.2 ms (-1.94%)
CPU, open subagent at depth 6 10,760.0 ms 9,700.0 ms ≈ -1,060.0 ms (-9.85%)
Open chain parent from agents view 4,047.9 ms 3,817.3 ms ≈ -230.7 ms (-5.70%)
CPU, open chain parent 4,060.0 ms 3,590.0 ms ≈ -470.0 ms (-11.58%)
Scheduled catalog, first request 2,029.3 ms 1,964.0 ms ≈ -65.2 ms (-3.21%)
CPU, scheduled catalog 3,340.0 ms 3,350.0 ms ≈ +10.0 ms (+0.30%)
Scheduled catalog, repeated request 1,243.1 ms 1,210.1 ms ≈ -33.0 ms (-2.65%)
CPU, repeated catalog 1,680.0 ms 1,680.0 ms ≈ +0.0 ms (+0.00%)
Cold worker with three catalog scans 1,629.2 ms 1,666.5 ms ≈ +37.3 ms (+2.29%)
CPU, cold worker and scans 4,470.0 ms 4,360.0 ms ≈ -110.0 ms (-2.46%)
UI memory after interactions 2,678.68 MB 2,585.61 MB ≈ -93.07 MB (-3.47%)

Sandbox cost: ~$0.1419 — no inference calls.
Run, logs, and downloadable raw results

Methodology and samples

Main resolved at 2026-09-16T23:46:40.168225+00:00. Harness b6ac5d01.
Linux x64, 4 vCPU, 8 GB RAM, 20 GB disk; region us.
Image: node:24-bookworm@sha256:be23f54a88d34e8824c741b19b91064094f92c1c97b194144bfc8b50d67258e2.
Stock tools, skills, daemon, and Python bootstrap enabled; fresh homes and a fixed Git fixture.
Onboarding is dismissed; the editor starts without a selected model or submitted prompt.
Medians shown. Arrows require a 20% timing/memory change plus absolute floors and IQR.
These practical noise floors are not a statistical significance test.
Cold means stopped Prime processes; OS filesystem caches are not flushed.
No model requests or credentials. Installation excludes build/setup time.
Installer tarballs use loopback; npm/Python downloads use the network with fresh caches.
Artifact size counts release tarballs; footprint after first use includes registry packages.
MB is decimal. Summed RSS can double-count shared pages; PSS is recorded when available.
Provisioning, setup, and build durations are recorded separately in the raw results.
Kernel probes use the installed JSONL runtime, outside the TUI/TypeScript host.
Per trial: 50 Python cells, 5 calls per shell case, and one 35-cell mix (9 git status calls).
Cell/shell values are batch means; other runtime timings are single operations.
State fixture: a 10,000-row × 8-column integer DataFrame and a 10,000-integer list.
Restore runs in a fresh kernel, including pandas imports; kernel startup is excluded.
Kernel RSS covers the isolated Python process; loaded RSS follows the pandas workload.
UI trials use a fresh fixture set: 194 top-level sessions including one ~40 MB transcript,
40 ledger fan-out children, and a 6-deep subagent chain (~46 spawn edges).
Large fixtures hold 1,999 complete triples (~5 MB JSONL); medium 119; subagents 399 each.
Interactions: cold --resume of a large session, warm /resume switch, left-arrow to agents view,
roster settle with many saved sessions, search-and-open of another large session,
reattaching to that resident session, opening the chain parent, and drilling to depth 6.
Readiness is the rendered transcript tail plus a confirmed editor echo.
CPU metrics sum utime+stime across the whole benchmark-user process tree per interaction.
UI memory sums RSS after the interactions; PTY byte counts are in the raw results.
A separate catalog fixture has 2,300 sessions, 2,298 edges, and 13 paused scheduled-job owners.
Catalog timings cover first/repeated reads and cold worker creation under three pending scans.
All expected jobs and owner metadata are checked; worker readiness excludes TUI rendering.
Costs estimate full sandbox lifetimes at configured rates, including setup and build.
Budget target: $1; not a billing cap. Performance changes are informational.
Failed or incomplete execution fails the workflow; saved artifacts remain available.
Each side stops a phase after 2 identical consecutive failures.
Skipped trials are not attempted samples. Warm startup requires a successful cold launch.

Metric Main successful/attempted PR successful/attempted Main spread PR spread
Cold startup 10/10 10/10 IQR 43.3 ms IQR 90.5 ms
Warm startup 10/10 10/10 IQR 31.7 ms IQR 16.9 ms
Installation 3/3 3/3 range 0.12 s range 0.26 s
Compressed release artifacts 1/1 1/1
Installed footprint 1/1 1/1
Idle memory, summed RSS 10/10 10/10 IQR 65.87 MB IQR 134.09 MB
Python kernel startup 10/10 10/10 IQR 8.3 ms IQR 6.6 ms
Python cell round trip 10/10 10/10 IQR 0.060 ms IQR 0.028 ms
Empty bash command 10/10 10/10 IQR 1.5 ms IQR 0.4 ms
Bash git status 10/10 10/10 IQR 2.4 ms IQR 0.7 ms
Bash 32 KiB output 10/10 10/10 IQR 0.9 ms IQR 0.4 ms
35 cells / 9 shell calls 10/10 10/10 IQR 16.9 ms IQR 8.5 ms
Python interrupt to done 10/10 10/10 IQR 0.111 ms IQR 0.062 ms
Python state snapshot 10/10 10/10 IQR 3.4 ms IQR 2.1 ms
Python state restore 10/10 10/10 IQR 37.6 ms IQR 16.3 ms
Python idle RSS 10/10 10/10 IQR 4.65 MB IQR 2.27 MB
Python RSS after pandas workload 10/10 10/10 IQR 5.51 MB IQR 2.31 MB
Resume large session (cold) 3/3 3/3 range 940.0 ms range 454.3 ms
CPU, resume large session 3/3 3/3 range 930.0 ms range 810.0 ms
Switch into large session 3/3 3/3 range 516.8 ms range 327.5 ms
CPU, switch into large session 3/3 3/3 range 760.0 ms range 1,060.0 ms
Open agents view from a session 3/3 3/3 range 17.4 ms range 11.4 ms
CPU, open agents view 3/3 3/3 range 20.0 ms range 70.0 ms
Full agents roster, many sessions 3/3 3/3 range 0.0097 s range 0.009 s
CPU, full agents roster 3/3 3/3 range 0.55 s range 0.55 s
Open another session from agents view 3/3 3/3 range 195.3 ms range 169.1 ms
CPU, open from agents view 3/3 3/3 range 580.0 ms range 360.0 ms
Reopen resident large session 3/3 3/3 range 112.6 ms range 74.8 ms
CPU, reopen resident session 3/3 3/3 range 340.0 ms range 180.0 ms
Open subagent session at depth 6 3/3 3/3 range 732.1 ms range 2,322.5 ms
CPU, open subagent at depth 6 3/3 3/3 range 560.0 ms range 140.0 ms
Open chain parent from agents view 3/3 3/3 range 69.7 ms range 130.1 ms
CPU, open chain parent 3/3 3/3 range 110.0 ms range 350.0 ms
Scheduled catalog, first request 3/3 3/3 range 117.2 ms range 414.5 ms
CPU, scheduled catalog 3/3 3/3 range 250.0 ms range 930.0 ms
Scheduled catalog, repeated request 3/3 3/3 range 134.8 ms range 97.0 ms
CPU, repeated catalog 3/3 3/3 range 180.0 ms range 120.0 ms
Cold worker with three catalog scans 3/3 3/3 range 66.7 ms range 226.1 ms
CPU, cold worker and scans 3/3 3/3 range 330.0 ms range 600.0 ms
UI memory after interactions 3/3 3/3 range 85.50 MB range 89.09 MB

Comment thread packages/coding-agent/src/core/kernel/repl-manager.ts

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c023913. Configure here.

Comment thread packages/coding-agent/src/core/kernel/repl-manager.ts
Rotating an oversized kernel stderr log renamed it to `.old` before any
chmod, so a pre-existing world-readable (0644) log kept those bits and its
historical exception payloads stayed readable by other users. Only the new
current file was tightened.

chmod the log to KERNEL_STDERR_LOG_MODE immediately before the rename. The
path is known to exist (statSync just succeeded), a chmod failure lands in
the existing rotation catch and keeps appending instead, and on Windows the
chmod clears the read-only bit so the rename still proceeds.

Cover it with a regression test that pre-creates an oversized 0644 log and
asserts the rotated file is 0600. The failing-start tests now share
fakeRuntime/withManager helpers to keep the added coverage line-neutral.
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