vmbox: share host repos by bind mount, not symlink

9p exports a directory TREE and does not follow a symlink pointing out of it.
The share directory held `testsAndMisc -> /home/kuhy/testsAndMisc`, so the
guest mounted /mnt/hostrepo successfully, listed the symlink, and then got
"No such file or directory" following it -- the target path does not exist
inside the guest. Measured in a live sandbox, not assumed.

`vm share <path>` now bind-mounts the directory into the share and remounts it
read-only, so the guest sees real contents while still being unable to write
to the host copy. `vm unshare <name>` reverses it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
This commit is contained in:
Krzysztof Rudnicki 2026-08-22 21:15:57 +02:00
parent 60571adb40
commit a18e0da3a9
3 changed files with 43 additions and 1 deletions

View File

@ -22,6 +22,8 @@ Usage: vm <command> [args]
new <name> [--rtc <ts>] Create a sandbox (instant; thin overlay)
run <name> <command...> Run a command in the sandbox, report the verdict
ssh <name> [args...] Interactive shell in the sandbox
share <host-path> Expose a host dir to sandboxes (read-only bind)
unshare <name> Stop exposing it
repo <name> <path> [--worktree]
Copy a host repo in (clone=HEAD, --worktree=uncommitted)
screenshot <name> [out.png] Capture the guest screen (for locker tests)
@ -78,6 +80,8 @@ main() {
ssh) source "$VMBOX_LIB_DIR/ssh.sh"; vm_ssh_interactive "$@" ;;
screenshot) source "$VMBOX_LIB_DIR/screenshot.sh"; vm_screenshot "$@" ;;
repo) source "$VMBOX_LIB_DIR/repo.sh"; cmd_repo "$@" ;;
share) source "$VMBOX_LIB_DIR/repo.sh"; repo_share "$@" ;;
unshare) source "$VMBOX_LIB_DIR/repo.sh"; repo_unshare "$@" ;;
list|ls) cmd_list ;;
-h|--help|help|'') usage ;;
*) die "unknown command '$cmd' (try: vm --help)" ;;

View File

@ -33,7 +33,10 @@ launch_vm() {
serial_n="$(vm_next_serial "$name")"
serial="$(vm_serial "$name" "$serial_n")"
qmp="$(vm_qmp_sock "$name")"
# Symlink the repos you want testable into here; nothing else is exposed.
# Repos you want testable go here. Use `vm share <path>` -- it BIND-MOUNTS
# rather than symlinks, because 9p exports a directory tree and will not
# follow a symlink pointing outside it (the guest would see a dangling
# link). Nothing outside this directory is ever exposed.
share="${VMBOX_SHARE:-$VMBOX_HOME/share}"
install -d -m 755 "$share"

View File

@ -17,6 +17,41 @@ source "$(dirname "${BASH_SOURCE[0]}")/ssh.sh"
readonly GUEST_MOUNT="/mnt/hostrepo"
# Expose a host directory to sandboxes. Bind-mount rather than symlink: 9p
# exports a directory TREE and does not follow links out of it, so a symlinked
# repo appears in the guest as a dangling link (measured, not assumed).
# The bind is read-only, so the guest cannot write to the real repo.
repo_share() {
local src="${1:-}" name share
[[ -n "$src" ]] || die "usage: vm share <host-path>"
[[ -d "$src" ]] || die "no such directory: $src"
src="$(realpath "$src")"
name="$(basename "$src")"
share="${VMBOX_SHARE:-$VMBOX_HOME/share}"
install -d -m 755 "$share/$name"
if mountpoint -q "$share/$name"; then
ok "$name is already shared"
return 0
fi
# A read-only bind needs two steps: mount, then remount ro.
sudo mount --bind "$src" "$share/$name" ||
die "bind mount failed (needs sudo once per boot)"
sudo mount -o remount,ro,bind "$share/$name" ||
warn "could not remount read-only -- the guest mount is still ro via 9p"
ok "shared $src -> sandboxes see it at $GUEST_MOUNT/$name (read-only)"
}
repo_unshare() {
local name="${1:-}" share
share="${VMBOX_SHARE:-$VMBOX_HOME/share}"
[[ -n "$name" ]] || die "usage: vm unshare <name>"
mountpoint -q "$share/$name" || { ok "$name is not shared"; return 0; }
sudo umount "$share/$name" || die "umount failed"
rmdir "$share/$name" 2>/dev/null || true
ok "unshared $name"
}
# $1 vm name, $2 host repo path, $3 mode (clone|worktree)
repo_sync() {
local name="$1" src="$2" mode="${3:-clone}" repo_name