From b34e2fe4810f222bf17c8cfaa908e99b575385e0 Mon Sep 17 00:00:00 2001 From: Ramiro Rivera Date: Fri, 28 Aug 2026 12:15:22 +0200 Subject: [PATCH] feat(vm): add opt-in QEMU guest agent channel - Pin qemu-guest-agent in the ARM64 guest transaction - Add a private per-boot virtio-serial QGA socket behind an explicit flag - Validate and test the disabled-by-default root command channel Assisted-by: GPT-5.6 Luna via OMP Assisted-by: GPT-5.6 Sol via Hermes Agent --- Makefile | 1 + docs/architecture.md | 6 +++++ guest/packages.lock.json | 6 +++-- guest/packages.txt | 1 + guest/spec.json | 4 +++ macos/Tests/qemu-guest-agent.test.sh | 39 ++++++++++++++++++++++++++++ macos/run-qemu-gpu.sh | 33 ++++++++++++++++++++++- 7 files changed, 87 insertions(+), 3 deletions(-) create mode 100755 macos/Tests/qemu-guest-agent.test.sh diff --git a/Makefile b/Makefile index 042a7d70..c1dc6680 100644 --- a/Makefile +++ b/Makefile @@ -58,6 +58,7 @@ test: @mkdir -p $(ROOT)/macos/.build/module-cache/swift $(ROOT)/macos/.build/module-cache/clang @cd $(ROOT)/macos && SWIFT_MODULECACHE_PATH=$(ROOT)/macos/.build/module-cache/swift CLANG_MODULE_CACHE_PATH=$(ROOT)/macos/.build/module-cache/clang swift test --disable-sandbox @$(ROOT)/macos/Tests/qemu-persistent-storage.test.sh + @$(ROOT)/macos/Tests/qemu-guest-agent.test.sh guest: @OMARCHY_FORCE_BUILD="$(FORCE)" "$(BUILD_CACHE)" \ diff --git a/docs/architecture.md b/docs/architecture.md index 53cd3852..e2cc8dee 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -42,6 +42,12 @@ fingerprint of what it last wrote so the immediate echo is dropped. The marker is cleared as soon as the other side moves on to new content, and expires after a couple of seconds regardless, so a genuine repeat of the same content still flows. +Developer launches can set `OMARCHY_QEMU_GUEST_AGENT=1` to add a second private +Unix-socket channel named `org.qemu.guest_agent.0`. The guest package is present, +and systemd starts its static service when that virtio port appears. This channel +can execute commands as root, so release launches leave it disabled and the host +socket exists only inside the launcher's mode-0700 per-boot runtime directory. + When a folder is chosen on the start menu, QEMU exports it over virtio-9p with `security_model=none`, so every host file operation runs as the Mac user and the Mac keeps real modes and ownership. A small QEMU patch adds diff --git a/guest/packages.lock.json b/guest/packages.lock.json index a9edc5bf..0dcfeea5 100644 --- a/guest/packages.lock.json +++ b/guest/packages.lock.json @@ -130,7 +130,7 @@ "gupnp-igd": "1.6.0-2", "gvfs": "1.60.2-3", "gzip": "1.14-2", - "harfbuzz": "14.3.1-1", + "harfbuzz": "14.4.0-1", "hicolor-icon-theme": "0.18-1", "hidapi": "0.15.0-1", "highway": "1.4.0-1", @@ -423,6 +423,7 @@ "npth": "1.8-1", "nspr": "4.40-1", "nss": "3.128-1", + "numactl": "2.0.19-1", "ocl-icd": "2.3.5-1", "omarchy-keyring": "20251027-1", "onetbb": "2023.1.0-1", @@ -472,6 +473,7 @@ "python-gobject": "3.56.3-1", "python-pyxdg": "0.28-7", "python-yaml": "6.0.3-2", + "qemu-guest-agent": "11.1.0-1", "qoi": "r350.4461cc3-1", "qt6-base": "6.11.2-2", "qt6-declarative": "6.11.2-1", @@ -595,6 +597,6 @@ "zram-generator": "1.2.1-1", "zstd": "1.5.7-3" }, - "requestedFileSha256": "c63acbf1a06b04b494bd65b38f1ee10847786fcbc3ff5cab4e8e0039c0582f30", + "requestedFileSha256": "8230ecd7405f07bf2c8e09246f426cc3397c2a05145197a03ffccfd3f4c62605", "schemaVersion": 1 } diff --git a/guest/packages.txt b/guest/packages.txt index 8846a6b6..8da632c2 100644 --- a/guest/packages.txt +++ b/guest/packages.txt @@ -50,6 +50,7 @@ polkit python qt6-imageformats qt6-wayland +qemu-guest-agent quickshell ripgrep sddm diff --git a/guest/spec.json b/guest/spec.json index bb5ffcb4..c5fdf336 100644 --- a/guest/spec.json +++ b/guest/spec.json @@ -210,6 +210,10 @@ "image/png" ] }, + "guestAgent": { + "device": "virtserialport", + "port": "org.qemu.guest_agent.0" + }, "sharedFolder": { "device": "virtio-9p-pci", "fsdriver": "local", diff --git a/macos/Tests/qemu-guest-agent.test.sh b/macos/Tests/qemu-guest-agent.test.sh new file mode 100755 index 00000000..1646d51e --- /dev/null +++ b/macos/Tests/qemu-guest-agent.test.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +set -euo pipefail + +test_dir=$(cd "$(dirname "$0")" && pwd -P) +repo_dir=$(cd "$test_dir/../.." && pwd -P) + +python3 - "$repo_dir" <<'PY' +import json +from pathlib import Path +import sys + +repo = Path(sys.argv[1]) +launcher = (repo / "macos/run-qemu-gpu.sh").read_text() +packages = (repo / "guest/packages.txt").read_text().splitlines() +package_lock = json.loads((repo / "guest/packages.lock.json").read_text()) +spec = json.loads((repo / "guest/spec.json").read_text()) + +assert packages.count("qemu-guest-agent") == 1, "qemu-guest-agent must be a direct guest package" +assert "qemu-guest-agent" in package_lock["packages"], "qemu-guest-agent must be pinned in the package lock" +assert spec["runtime"]["guestAgent"] == { + "device": "virtserialport", + "port": "org.qemu.guest_agent.0", +} +assert 'case ${OMARCHY_QEMU_GUEST_AGENT:-0} in' in launcher +assert '*) fail "OMARCHY_QEMU_GUEST_AGENT must be 0 or 1" ;;' in launcher +assert launcher.count("socket,id=omarchy-guest-agent,path=$guest_agent_socket,server=on,wait=off") == 1 +assert launcher.count( + "virtserialport,bus=omarchy-serial.0,nr=3,chardev=omarchy-guest-agent,name=org.qemu.guest_agent.0" +) == 1 +assert "if ((guest_agent_enabled)); then\n qemu_args+=(" in launcher +assert "guest agent: disabled" in launcher +assert "guest agent socket: %q" in launcher +assert "[[ -S $guest_agent_socket ]] || fail \"QEMU did not create its private guest-agent socket\"" in launcher +assert 'guest_agent_socket="/tmp/${work_dir##*/}/qga.sock"' in launcher +assert "umask 077\nwork_dir=$(mktemp -d '/private/tmp/omarchy-qemu-gpu.XXXXXX')" in launcher +PY + +printf 'qemu-guest-agent.test: PASS\n' diff --git a/macos/run-qemu-gpu.sh b/macos/run-qemu-gpu.sh index e466b24b..58ca6022 100755 --- a/macos/run-qemu-gpu.sh +++ b/macos/run-qemu-gpu.sh @@ -347,6 +347,7 @@ runtime = exact_keys( "devices", "disk", "graphics", + "guestAgent", "hypervisor", "initramfs", "initramfsSource", @@ -383,6 +384,10 @@ clipboard = { "port": "dev.tryomarchy.clipboard", "formats": ["text/plain;charset=utf-8", "image/png"], } +guest_agent = { + "device": "virtserialport", + "port": "org.qemu.guest_agent.0", +} shared_folder = { "device": "virtio-9p-pci", "fsdriver": "local", @@ -432,6 +437,7 @@ if ( or runtime.get("audio") != audio or runtime.get("storage") != storage or runtime.get("clipboard") != clipboard + or runtime.get("guestAgent") != guest_agent or runtime.get("sharedFolder") != shared_folder or runtime.get("devices") != expected_devices or runtime.get("minimumMemoryMiB") != 2048 @@ -750,6 +756,14 @@ if [[ -n $shared_folder ]]; then shared_folder_kernel_argument=" omarchy.shared_folder_name=$shared_folder_name_encoded" fi +# QGA is a root-capable guest command channel. Keep it disabled unless an +# explicit developer launch requests the private per-boot Unix socket. +case ${OMARCHY_QEMU_GUEST_AGENT:-0} in + 1) guest_agent_enabled=1 ;; + 0) guest_agent_enabled=0 ;; + *) fail "OMARCHY_QEMU_GUEST_AGENT must be 0 or 1" ;; +esac + work_dir="" owner_marker="" owner_token="" @@ -880,6 +894,7 @@ chmod 600 "$owner_marker" qmp_socket="/tmp/${work_dir##*/}/qmp.sock" audio_bridge_socket="/tmp/${work_dir##*/}/audio.sock" clipboard_bridge_socket="/tmp/${work_dir##*/}/clipboard.sock" +guest_agent_socket="/tmp/${work_dir##*/}/qga.sock" audio_route_dir="/tmp/${work_dir##*/}/audio-routes" mkdir -m 700 "$work_dir/audio-routes" @@ -963,6 +978,13 @@ qemu_args=( -device 'virtserialport,bus=omarchy-serial.0,nr=2,chardev=omarchy-clipboard-bridge,name=dev.tryomarchy.clipboard' ) +if ((guest_agent_enabled)); then + qemu_args+=( + -chardev "socket,id=omarchy-guest-agent,path=$guest_agent_socket,server=on,wait=off" + -device 'virtserialport,bus=omarchy-serial.0,nr=3,chardev=omarchy-guest-agent,name=org.qemu.guest_agent.0' + ) +fi + if [[ -n $shared_folder ]]; then # security_model=none performs every host operation as this Mac user and # ignores guest chown requests, so the Mac keeps real modes and ownership. @@ -998,6 +1020,11 @@ if [[ ${OMARCHY_QEMU_GPU_DRY_RUN:-0} == 1 ]]; then else printf '\n[qemu-gpu] shared folder: disabled' >&2 fi + if ((guest_agent_enabled)); then + printf '\n[qemu-gpu] guest agent socket: %q' "$guest_agent_socket" >&2 + else + printf '\n[qemu-gpu] guest agent: disabled' >&2 + fi printf '\n' >&2 exit 0 fi @@ -1020,13 +1047,17 @@ printf '%s\n' "$qemu_pid" >"$work_dir/.qemu.pid" chmod 600 "$work_dir/.qemu.pid" for ((attempt = 0; attempt < 100; attempt++)); do - [[ -S $qmp_socket && -S $audio_bridge_socket && -S $clipboard_bridge_socket ]] && break + [[ -S $qmp_socket && -S $audio_bridge_socket && -S $clipboard_bridge_socket ]] && + { ((!guest_agent_enabled)) || [[ -S $guest_agent_socket ]]; } && break kill -0 "$qemu_pid" 2>/dev/null || fail "QEMU exited before creating its private QMP socket" sleep 0.05 done [[ -S $qmp_socket ]] || fail "QEMU did not create its private QMP socket" [[ -S $audio_bridge_socket ]] || fail "QEMU did not create its private audio bridge socket" [[ -S $clipboard_bridge_socket ]] || fail "QEMU did not create its private clipboard bridge socket" +if ((guest_agent_enabled)); then + [[ -S $guest_agent_socket ]] || fail "QEMU did not create its private guest-agent socket" +fi echo "[qemu-gpu] Ready." >&2 # FD 9 deliberately remains open only in QEMU. Letting the sibling audio