From ab4f94fb2917e7cdbc0cf3650e73af45b5a4791e Mon Sep 17 00:00:00 2001 From: Lorenz Bauer Date: Sun, 25 Jan 2026 20:38:41 -0500 Subject: [PATCH] fix usage on NixOS We make /run inaccessible in the VM since that folder usually contains machine specific state. On NixOS, this includes the directory with all the symlinks for the current profile. Work around this by resolving symlinks so they point at /nix/store instead of at /run. --- main_test.go | 4 +-- qemu.go | 63 +++++++++++++++++++++++++++++++++++++++++ testdata/nixos-path.txt | 24 ++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 testdata/nixos-path.txt diff --git a/main_test.go b/main_test.go index 5f7600d..1a8ac59 100644 --- a/main_test.go +++ b/main_test.go @@ -43,8 +43,6 @@ func TestExecutable(t *testing.T) { t.Fatal("Failed to compile binary:", err) } - t.Setenv("PATH", fmt.Sprintf("%s:%s", path, os.Getenv("PATH"))) - e := script.NewEngine() e.Cmds["glob-exists"] = globExists e.Cmds["gdb"] = gdbStub @@ -61,7 +59,7 @@ func TestExecutable(t *testing.T) { s.Setenv("TMPDIR", t.TempDir()) return nil, nil }) - e.Cmds["vimto"] = script.Program("vimto", nil, time.Second) + e.Cmds["vimto"] = script.Program(filepath.Join(path, "vimto"), nil, time.Second) e.Cmds["config"] = script.Command(script.CmdUsage{ Summary: "Write to the configuration file", Args: "items...", diff --git a/qemu.go b/qemu.go index 32f4325..6910e81 100644 --- a/qemu.go +++ b/qemu.go @@ -252,6 +252,25 @@ func (cmd *command) Start(ctx context.Context) (err error) { return strings.HasPrefix(env, "TMPDIR=") }) + // Resolve symlinks in PATH entries and warn about opaque directories + for i, e := range env { + if key, value, ok := strings.Cut(e, "="); ok && key == "PATH" { + parts := filepath.SplitList(value) + for j, p := range parts { + resolved := resolvePath(p) + parts[j] = resolved + // Warn if resolved path still under opaque directory + for _, opaque := range opaqueDirectories { + if resolved == opaque || strings.HasPrefix(resolved, opaque+"/") { + fmt.Fprintf(os.Stderr, "warning: PATH entry %q resolves to %q which is shadowed in the VM\n", p, resolved) + break + } + } + } + env[i] = "PATH=" + strings.Join(parts, string(filepath.ListSeparator)) + } + } + execCmd := execCommand{ cmd.Path, cmd.Args, @@ -720,6 +739,50 @@ func (*p9SharedDirectory) KArgs() []string { return nil } +// resolvePath resolves symlinks in a path segment by segment. +// Returns original path if resolution fails at any point. +func resolvePath(path string) string { + if path == "" { + return path + } + + var resolved string + if filepath.IsAbs(path) { + resolved = "/" + } + + segments := strings.Split(path, string(filepath.Separator)) + for _, seg := range segments { + if seg == "" { + continue + } + + current := filepath.Join(resolved, seg) + info, err := os.Lstat(current) + if err != nil { + // Path doesn't exist or can't be accessed, return original + return path + } + + if info.Mode()&os.ModeSymlink != 0 { + target, err := os.Readlink(current) + if err != nil { + return path + } + + if filepath.IsAbs(target) { + resolved = target + } else { + resolved = filepath.Join(resolved, target) + } + } else { + resolved = current + } + } + + return filepath.Clean(resolved) +} + // virtioRandom and arbitraryArgs were copied from u-root, available under BSD-3-Clause. // Copyright (c) 2012-2019, u-root Authors diff --git a/testdata/nixos-path.txt b/testdata/nixos-path.txt new file mode 100644 index 0000000..4c814ed --- /dev/null +++ b/testdata/nixos-path.txt @@ -0,0 +1,24 @@ +# Test PATH symlink resolution +# +# On NixOS, PATH contains entries like /run/current-system/sw/bin which are +# symlinks into /nix/store. Since /run is made opaque in the VM, we resolve +# symlinks in PATH entries before passing them to the guest. +# +# This test verifies symlink resolution works. Since $WORK is under /tmp +# (an opaque directory), we expect a warning but the resolution should +# still transform link-dir -> real-dir in the PATH. + +config kernel="${KERNEL}" + +# Create target directory and symlink to it +mkdir real-dir +symlink link-dir -> real-dir + +# Set PATH through the symlink +env PATH=$PATH:$WORK/link-dir + +# Verify PATH inside VM contains resolved path, not symlink +vimto exec -- sh -c 'echo $PATH' +stderr 'warning: PATH entry' +stdout real-dir +! stdout link-dir