Skip to content

feat(system): persist kernel.yama.ptrace_scope=0 so the keylog spin capture can attach - #334

Open
ZacxDev wants to merge 1 commit into
mainfrom
feat/persist-ptrace-scope
Open

feat(system): persist kernel.yama.ptrace_scope=0 so the keylog spin capture can attach#334
ZacxDev wants to merge 1 commit into
mainfrom
feat/persist-ptrace-scope

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Staged /etc/nixos change (Claude can't sudo nixos-rebuild), run with:

sudo bash nix/system/apply-ptrace-scope.sh

Why

keylog-spin-capture uses py-spy to dump keylog.service — a sibling systemd --user unit, never a descendant of the capture. Yama's default scope=1 ("descendants only") blocks both PTRACE_ATTACH and process_vm_readv, so the watcher hits its sysctl gate and exits silently every 5 minutes. It is enabled and structurally incapable of ever succeeding.

The workbench read 0 for most of 2026-07-30 only because of a manual echo 0 | sudo tee during the investigation. That was never persisted, so the 2026-08-04 reboot reset it to 1 and the watcher went inert.

An earlier revision of the perf script had this as a prompted step. The audit removed it — correctly, since the live value was already 0 and it would have bought nothing. That reasoning is now stale; the reboot is what changed the facts.

The trade, stated rather than buried

scope=0 is the traditional Linux default: any process may ptrace another running as the same UID. It grants no cross-user or root access. But on a box routinely running agent-spawned code as this user, it widens what a compromised same-user process can read from other processes' memory — browser sessions, kubeconfigs loaded into a running tool, ssh-agent.

Two narrower alternatives, rejected with reasons in the script header:

  • CAP_SYS_PTRACE on the capture unitsystemd --user services can't gain capabilities. It would have to become a root system unit, which is a larger privilege grant, not a smaller one.
  • Run py-spy as root ad hoc — works, but defeats the point: the mechanism exists to catch a spin that appears unpredictably over 24h+, precisely when you aren't there to run something.

If you'd rather not take the trade, the honest alternative is to drop the watcher — it degrades safely (detects scope!=0, exits quietly, no dump/toast/retry), but it will never fire.

Includes the systemd-sysctl restart — and here it's warranted

nixos-rebuild switch does not restart systemd-sysctl (nixpkgs#289174), so a new boot.kernel.sysctl edit stays inert on the running kernel. The earlier perf script deliberately dropped this restart because nothing was pending. Here something is.

Verification

Tested against a scratch copy of the live configuration.nix:

non-TTY run          rc=0, completes (no mid-edit abort)
second run           "[1/3] ptrace_scope already declared — skipping"
resulting diff       exactly one attribute, inside boot.kernel.sysctl
nix-instantiate      parses clean after two runs

Backs up first, restores on any failure before validation, prompts before nixos-rebuild (skipped safely on non-TTY), prints a rollback line — including if the rebuild itself fails.

🤖 Generated with Claude Code

…ture works

keylog-spin-capture uses py-spy to dump keylog.service — a SIBLING
`systemd --user` unit, never a descendant — so Yama's default scope=1
("descendants only") blocks both PTRACE_ATTACH and process_vm_readv. The
watcher then hits its sysctl gate and exits silently every 5 minutes:
enabled, but structurally incapable of succeeding.

The workbench read 0 for most of 2026-07-30 only because of a manual
`echo 0 | sudo tee` during the investigation. It was never persisted, so
the 2026-08-04 reboot reset it to 1 and the watcher went inert. An
earlier revision of the perf script had this as a prompted step; the
audit correctly removed it as buying nothing WHEN the live value was
already 0. That reasoning is now stale — the reboot is what changed it.

The trade is documented in the script header rather than buried: scope=0
is the traditional Linux default (same-UID only, no cross-user or root
grant), but on a box routinely running agent-spawned code as this user it
widens what a compromised same-user process can read from other processes'
memory. Two narrower alternatives are rejected with reasons — a
`systemd --user` service cannot gain CAP_SYS_PTRACE, and running py-spy as
root ad hoc defeats a mechanism whose whole point is catching an
unpredictable spin while you are not watching.

Includes the systemd-sysctl restart, and this is the case where it is
genuinely warranted: `nixos-rebuild switch` does not restart that unit
(nixpkgs#289174), so a NEW boot.kernel.sysctl edit stays inert on the
running kernel. The earlier script dropped the restart because nothing
was pending; here something is.

Verified against a scratch copy of the live configuration.nix: non-TTY
run completes rc=0, second run is a clean no-op, resulting diff is
exactly one attribute inside boot.kernel.sysctl, parses clean.
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