Skip to content

Keep the Windows VM boundary probe off the host's mounts - #1

Merged
therahul-yo merged 18 commits into
quattrofrom
claude/fork-audit-pr-bgz552
Sep 14, 2026
Merged

therahul-yo merged 18 commits into
quattrofrom
claude/fork-audit-pr-bgz552

Conversation

@therahul-yo

@therahul-yo therahul-yo commented Sep 14, 2026 •

Copy link
Copy Markdown
Owner

Fork audit

Compared therahul-yo/omarchy against upstream (basecamp/omarchy, which now redirects to omacom/omarchy — both serve identical SHAs on every branch).

Fork default branch quattro @ 692c02c
Upstream quattro @ b679363b
Divergence 17 behind, 0 ahead — a clean fork with no local commits

This branch is therefore based on upstream/quattro, so merging it both brings the fork current (the 17 upstream commits, including the Panther Lake kernel migration, the kyber I/O scheduler rule, the 1Password window class fix, and the basecamp → omacom URL move) and lands the fix below.

The bug

test/shell.d/windows-vm-mount-boundary-test.sh mounts tmpfs over /run, /var and /home to fake a system for the privileged omarchy-windows-vm helper. That is only safe inside its own mount namespace, and the file re-execs itself into one — but only when it is not already root:

if ((EUID != 0)); then
  if unshare --user --map-auto --map-root-user --mount true 2>/dev/null; then
    exec unshare --user --map-auto --map-root-user --mount --propagation private bash "$0"
  fi
  ...
fi

A root test runner — a container, a root CI job, sudo ./test/all — arrives with EUID already 0, so the whole unshare is skipped and the mount -t tmpfs ... calls land on the caller's own namespace. Nothing unmounts them when the file exits, so for the rest of the machine's uptime:

  • /run is an empty 8 MB tmpfs — the session's runtime state, sockets and $XDG_RUNTIME_DIR are gone
  • /var is an empty 16 MB tmpfs — the pacman database and logs are hidden
  • /home is an empty 16 MB tmpfs — every home directory disappears, the checkout with it

Being root is not isolation, which is exactly the assumption the EUID check encodes. The file's own comment shows the hazard was known but assumed handled: "The checkout may live under /home, which the tmpfs below hides, so take a mount-safe copy of the helper before the mounts land."

Observed, not theorized

This reproduced while auditing the fork: ./test/all was run as root, and the repository checkout vanished mid-session. The tmpfs mounts the test creates were still sitting on the host afterwards:

home-parent on /home type tmpfs (rw,relatime,size=16384k,mode=755)
home-alice on /home/alice type tmpfs (rw,relatime,size=1048576k,mode=710)
storage-test on /home/storage-target type tmpfs (...)
shared-test on /home/shared-target type tmpfs (...)

Those names come only from this file. Every test that ran after it, and everything else on the machine that reads /run, /var or /home, was working against empty tmpfs.

The fix

Gate the re-exec on a marker variable rather than on uid — the pattern the sibling windows-vm-compose-test.sh already uses — and give uid 0 a plain mount namespace, since it needs no user namespace to get one:

  • root → unshare --mount --propagation private
  • non-root → unchanged (--user --map-auto --map-root-user --mount)
  • neither available → skip, as before

Then record the caller's mount namespace before the re-exec and refuse to mount while still in it, so any future lapse in isolation fails closed instead of destructively.

Coverage improves rather than shrinks: a root runner previously ran the probe and wrecked the host; it now runs the probe in full and leaves the host alone.

Verification

Run as root in a container, on the fixed file:

$ wc -l </proc/self/mounts          # 31
$ bash test/shell.d/windows-vm-mount-boundary-test.sh
ok - root dispatch rejects missing/invalid uid, passwd, owner, symlink, and writable-parent boundaries without mutation
ok - cross-filesystem symlink sources bind by identity and migrated 0700 leaves deny another account
ok - root rejects wrong-owned and group-writable production mount boundaries without mutation
ok - disk-space accounting measures the actual storage filesystem, not home
ok - root writer and final pre-Docker guard revalidate the pinned production mounts
ok - previous sibling-anchor installs upgrade in place to the fixed /var/lib boundary
ok - existing fixed-anchor compose gains web-console authentication
ok - root preflights both sources before mounting either and preserves rejection evidence
ok - final guard rejects a symlink even when it resolves to the expected mounted source
$ wc -l </proc/self/mounts          # 31 — host untouched

All nine assertions pass and the host mount table is unchanged. Before the fix the same command replaced /run, /var and /home.

New regression test, test/shell.d/mount-namespace-isolation-test.sh, which stubs mount(8) so a regression records a call instead of landing one:

ok - windows-vm-mount-boundary-test.sh enters its namespace before mounting anything
ok - windows-vm-compose-test.sh enters its namespace before mounting anything
ok - boundary probe fails closed in the caller's mount namespace instead of mounting
ok - boundary probe fails closed when the caller namespace was never recorded

It fails (exit 1) against the pre-fix file and passes after, and it passes inside a full ./test/shell run. windows-vm-test.sh and bin-style-test.sh also pass.

docs/testing.md gains the convention under "Conventions worth copying" so the next mounting test does not re-derive it.

Not duplicate

No upstream issue, pull request, or branch covers this. Searches across omacom/omarchy for the test file name, unshare, mount-namespace and root-runner phrasings return nothing; the nine windows-vm mount hits are all about the runtime 0700/setgid mode check in bin/omarchy-windows-vm (omacom#9874, omacom#10004, omacom#10035, omacom#10114, omacom#10149, omacom#10256, omacom#10321, omacom#10399, PRs omacom#9783 and omacom#10113), a different file and a different failure. No upstream branch touches test-namespace isolation.

Scope note

One pre-existing, unrelated failure was observed and deliberately left alone: windows-vm-compose-test.sh exits 2 under a root runner (cannot identify the user who authorized this action, since --map-current-user maps root to root and PKEXEC_UID is unset). It fails safely inside its own namespace and is a separate change.

dhh and others added 18 commits September 13, 2026 08:56
The battery service ran `powerprofilesctl get` every two seconds to keep the
active profile visible to the wallpaper and lock services. That command is a
PyGObject script, so the shell spawned a Python interpreter for it tens of
thousands of times a day. Roughly once a day one of those exits into a CPython
3.14 finalization race (python/cpython#124619): the GLib D-Bus worker thread
calls PyGILState_Ensure after the interpreter is torn down and the process
dies with SIGSEGV, leaving a core dump and a crash notification behind.

Read the ActiveProfile property straight from power-profiles-daemon with
busctl, the same way omarchy-powerprofiles-set already reads UPower. The
output is JSON, so an empty or malformed reply when the daemon is not running
still reads as no active profile, matching the previous behaviour.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011gbfh4Mi9dK6SAd1P2xMTi
The kernel leaves NVMe on none and everything else on mq-deadline. Neither
bounds latency once the device queue fills, so a large build, copy, or
package upgrade can make the desktop sluggish while reads wait behind a
wall of writes.

Kyber keeps separate read and sync-write queues and throttles the depth it
submits to hit a 2ms read target, which keeps interactive reads flowing
under heavy writes at negligible CPU cost. The trade is a small ceiling on
peak throughput on very fast devices, which matters for a storage server
chasing IOPS but not for a desktop.

Ships as a package-owned udev rule in /etc so it applies at boot and on
hot-plug. Existing installs pick it up at the next boot. zram is left alone
since it has nothing to schedule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
udev patterns are shell globs, so nvme[0-9]*n[0-9]* also matched every
partition. Partitions have no queue/scheduler, and udev logged a "Could not
chase sysfs attribute" for each one at boot. Restrict the match to
SUBSYSTEM block with DEVTYPE disk, which also keeps mmcblk boot areas and
NVMe multipath nodes out.

Reword the comment: kyber targets a read latency rather than bounding it,
and the kernel's default choice depends on the device rather than being a
fixed NVMe versus everything-else split.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Use the kyber I/O scheduler on real disks
1Password 8.12 changed its app id from "1Password" to
"com.onepassword.OnePassword" (its .desktop file now declares that as
StartupWMClass). Our window rule only matched the old name, so the main
window came up tiled and lost the no_screen_share protection.

Match both forms so older installs keep working.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSFKDatumRZHB8zqk5CP5C
Match 1Password's new window class so it floats again
The repo moved to the omacom org. GitHub redirects the old URLs, but
`omarchy channel set dev` was still cloning from basecamp/omarchy, which
left every dev checkout with a stale origin remote that confuses gh
(pr create fails with "No commits between omacom:quattro and
basecamp:<branch>"). Update the clone URL, the quattro upgrade tarball,
the update-confirm release link, the systemd Documentation link, and
the manual.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSFKDatumRZHB8zqk5CP5C
Point GitHub URLs at omacom/omarchy instead of basecamp/omarchy
No branch on the repo is protected and there are no rulesets, so the
file never enforced owner review. Its only effect was auto-requesting a
review from the other owner on every PR, which nobody acted on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…edumps

Poll the active power profile over D-Bus instead of powerprofilesctl
…gration

Migrate Panther Lake systems to the Omarchy PTL kernel
The probe re-execs into an isolated namespace only when EUID is not 0, so a
root test runner -- a container, a root CI job, sudo ./test/all -- walks past
the unshare and lands its tmpfs mounts on the live system. /run, /var and
/home are replaced in the caller's own namespace, hiding the running session's
runtime state and every home directory, the checkout included, for as long as
the machine stays up. Nothing unmounts them when the file exits.

Gate the re-exec on a marker variable instead, the way the sibling compose
probe already does, and give uid 0 a plain mount namespace since it needs no
user namespace to get one. Root now runs the probe in full rather than
wrecking the machine that started it, and a runner that can make no namespace
at all skips as before.

Record the caller's mount namespace before the re-exec and refuse to mount
when the probe still sits in it, so this fails closed rather than destructively
if the isolation ever lapses again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUNbTymYv46cHfVS6gz7dW
@coderabbitai

coderabbitai Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 53ed5c1e-7fbf-4df0-b9f1-6aa4701a5a3e


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@therahul-yo
therahul-yo marked this pull request as ready for review September 14, 2026 05:59
@therahul-yo
therahul-yo merged commit 8630f37 into quattro Sep 14, 2026
1 check passed
therahul-yo added a commit that referenced this pull request Oct 2, 2026
Keep the Windows VM boundary probe off the host's mounts
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.

5 participants