Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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...",
Expand Down
63 changes: 63 additions & 0 deletions qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down
24 changes: 24 additions & 0 deletions testdata/nixos-path.txt
Original file line number Diff line number Diff line change
@@ -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