Skip to content

Fix Windows VM stop/launch cycle breaking on btrfs due to stale bind anchors and setgid bits - #12046

Open
Hercaster wants to merge 2 commits into
omacom:quattrofrom
Hercaster:fix/windows-vm-stale-bind-mounts
Open

Hercaster wants to merge 2 commits into
omacom:quattrofrom
Hercaster:fix/windows-vm-stale-bind-mounts

Conversation

@Hercaster

@Hercaster Hercaster commented Sep 16, 2026 •

Copy link
Copy Markdown

Running omarchy windows vm stop followed by omarchy windows vm launch fails on btrfs because the identity check fails on stale bind anchors left behind, and because directories with btrfs-inherited setgid bits (mode 2700) fail the mounted_leaf_matches check.

This PR:

  1. Ensures down unmounts those anchors, and launch recovers gracefully if a stale unverified bind anchor exists.
  2. Fixes mounted_leaf_matches to compare only the permission bits (using ), correctly handling directories with btrfs-inherited setgid bits (mode 2700).

@Hercaster Hercaster changed the title Fix Windows VM stop/launch cycle breaking on btrfs due to stale bind anchors Fix Windows VM stop/launch cycle breaking on btrfs due to stale bind anchors and setgid bits Sep 16, 2026
@Chessing234

Copy link
Copy Markdown
Contributor

hey, the setgid / launch-survives-first-run part is already covered in #12264. mind closing this as a duplicate (or trimming to whatever btrfs/bind-anchor bits are still unique)?

@omarchybot omarchybot added the bug Something isn't working label Sep 27, 2026
@omarchybot

Copy link
Copy Markdown
Collaborator

Thanks for chasing this down. The setgid bug is real, but this change doesn't fix it, and the open #12264 does, so that's the one I'd point the maintainer at.

What was checked. I ran a stop → launch reproduction on an Omarchy ISO worker (btrfs home), inside a private mount namespace the way test/shell.d/windows-vm-compose-test.sh does it. I ran it against quattro (2fbac0c), this head (b8f282d) and #12264 (799827f). The three windows-vm-* shell tests also pass on this head.

Case quattro this PR #12264
Plain stop → launch passes passes passes
~/Windows turns 2700, then stop → launch fails fails passes
Sources created under a setgid parent, then stop → launch fails fails passes
~/Windows replaced while its anchor is still bound refuses rebinds refuses

Why the setgid case still fails here. chmod 0700 on a directory keeps an existing setgid bit (that's how GNU chmod treats numeric modes on directories), so the directory stays 2700. prepare_caller_mounts then checks for exactly 700 (bin/omarchy-windows-vm:593 on this head) and returns 1 without printing anything, which is where the generic failure comes from. That happens before bind_mount_leaf, so relaxing mounted_leaf_matches never comes into play. #12264 clears the bit with chmod a-s,u=rwx,go= and keeps the strict check.

Other findings in this diff:

  • A plain stop → launch already works on quattro: the anchors are meant to survive down, and the next launch reuses them. The "no longer matches its home source" error only shows up when a source was replaced or its mode or owner changed.
  • __priv_down never calls resolve_caller. On the pkexec path it runs in a fresh root process with EXPECTED_STORAGE and EXPECTED_SHARED unset, so the new unmounts never happen. For a sudoless-Docker user, down runs without root, so umount fails and a warning prints on every stop.
  • "${mode: -3}" == 700 also accepts 4700 and 1700, and prepare_caller_mounts still rejects all of those.
  • Unmounting and rebinding any anchor that fails validation also changes what remove does. __priv_remove goes through the same path, so if ~/.windows has been redirected to a different directory, it rebinds to that directory and deletes its contents. quattro refuses in that case.

Second opinion. Codex Medium reviewed both pull requests. It agreed with the setgid trace and with preferring #12264 (independence not guaranteed, since it can read this session). The __priv_down initialisation gap and the remove rebinding hazard came from Codex, and I checked both against the source.

Reviewed by Claude Opus 5.5 and Codex Medium. This is waiting on the maintainer to choose between this and #12264. Nothing was pushed to your branch.

@omarchybot

Copy link
Copy Markdown
Collaborator

Following the merge of #12323, The setgid launch case is fixed without masking special permission bits. This PR also changes stale-bind replacement and unmount-on-stop behavior, which the merged PR did not change. Keep that separate work open. Keeping this open does not establish that its replacement and unmount paths are safe or ready.

Scope checked by GPT-6 in Codex and Codex Medium against the discussion and landed change; review independence is not guaranteed. No tests were rerun for this cleanup.

Omabot on behalf of DHH

@omarchybot

Copy link
Copy Markdown
Collaborator

Since the last review, #12323 has landed on quattro. It clears setgid while it hardens the directories, so I re-ran the reproduction against current quattro (d9b970d) and against this branch merged into it, on an Omarchy ISO worker in a private mount namespace.

Case quattro now this PR merged in
Plain stop → launch passes passes
~/Windows turns 2700, then stop → launch passes passes
Sources created under a setgid parent, then stop → launch passes passes
~/Windows replaced while its anchor is still bound refuses rebinds
mounted_leaf_matches given mode 4700 or 1700 rejects accepts

The three windows-vm-* shell tests and ./test/cli pass on both.

What that means for this change. Nothing in this PR now fixes a case that fails on quattro. What is left changes behaviour in three ways:

  • The mode check. Looking only at the last three digits is no longer needed for the setgid case, because hardening now leaves the directory at exactly 700. It does let a docker-group launch accept an existing 2700, 4700 or 1700 anchor as ready, which skips the elevated step where hardening runs.
  • Unmounting on stop. Nothing gets unmounted on the normal path. __priv_down runs in a fresh root process, and the __priv dispatcher (bin/omarchy-windows-vm:1592 after the merge) never calls resolve_caller, so EXPECTED_STORAGE and EXPECTED_SHARED are empty. On the worker, a fresh-process stop returned 0 and left the storage anchor mounted.
  • Rebinding a mismatched anchor. remove goes through the same prepare_caller_mounts. Suppose ~/.windows is pointed at a different caller-owned directory after install. quattro refuses to remove anything, because the anchor still pins the original. With this change, remove unbinds the original, binds the new directory, and deletes its contents.

Second opinion. Codex Medium reviewed it separately and reached the same conclusion on all three. The point about the docker-group readiness path came from Codex, and I checked it against the source. Its independence is not guaranteed. Reviewed by Claude Opus 5.5 and Codex Medium.

Nothing was pushed to your branch. This waits on the maintainer to decide whether it stays open. If a stale-source recovery is still needed, it would want a reproduction that fails on current quattro, and a guard so remove keeps deleting only through the identity it pinned.

@jandrusk

Copy link
Copy Markdown

Setgid hardening for the Windows VM shared mount is already on quattro via #12323, and #12264 covers the related launch path. If the only remaining unique work here is stale bind-anchor replace + unmount-on-stop, please trim the PR to that (or close if those paths aren't still needed after #12323).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants