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