Skip to content

qemu: findQemuDataDir returns uncanonicalized relative paths for symlinks #1046

Description

@slash-init

Problem Description

In pkg/unikontainers/utils.go, findQemuDataDir() checks whether /usr/local/share/<name> is a symbolic link. When it is, the function uses os.Readlink() and directly returns the symlink target.
os.Readlink() returns the target exactly as stored in the symlink. If the target is relative, the returned value is therefore also relative to the symlink's parent directory rather than an absolute path.
For example, if:

/usr/local/share/qemu -> ../share/qemu

then os.Readlink("/usr/local/share/qemu") returns:

../share/qemu

findQemuDataDir() currently returns that value without resolving it relative to /usr/local/share/.
The returned path is subsequently used by mountsForMonitor() in pkg/unikontainers/rootfs.go as the source of a bind mount:

mounts = append(mounts, bindMount(qDataPath, "/usr/share/qemu", true, true))

The relative path can then be interpreted relative to the process's current working directory rather than the directory containing the symlink. This can cause the subsequent filesystem or mount operation to fail with ENOENT.

Affected Code

File: pkg/unikontainers/utils.go

func findQemuDataDir(basename string) (string, error) {
	// First check if the file exists under /usr/local/share
	qdPath := filepath.Join("/usr/local/share/", basename)
	info, err := os.Lstat(qdPath)
	if err != nil {
		if !os.IsNotExist(err) {
			return "", fmt.Errorf("failed to get info of %s: %w", qdPath, err)
		}
		// The file does not exist under /usr/local/share
		// fallback to the usual path /usr/share/
		qdPath = filepath.Join("/usr/share/", basename)
	} else {
		// The file exists under /usr/local/share, but check if it is a link
		if info.Mode()&os.ModeSymlink != 0 {
			// It is a link, get the target
			qdPath, err = os.Readlink(qdPath)
			if err != nil {
				return "", fmt.Errorf("failed to get target of %s %w", qdPath, err)
			}
		}

		return qdPath, nil
	}

	return qdPath, nil
}

The issue is specifically the os.Readlink() path: the returned target is not resolved relative to /usr/local/share/<name> before being returned.

Failure Scenarios

1. Relative symlink target

Given:

/usr/local/share/qemu -> ../share/qemu

findQemuDataDir("qemu") returns:

../share/qemu

instead of the path represented by the symlink.
When this value is later used as a filesystem path, its interpretation depends on the process's current working directory.

2. Chained symlinks

os.Readlink() resolves only the first symlink itself. If the target is another symlink, findQemuDataDir() does not resolve the remaining chain.

3. Broken symlink

os.Lstat() succeeds for a dangling symlink because the link itself exists. os.Readlink() also succeeds and returns its target.
As a result, the function does not detect that the target is missing and does not fall back to /usr/share/<name>.

Expected Behavior

findQemuDataDir() should return a path that correctly represents the target of a symlink under /usr/local/share/, independent of the process's current working directory.
Symlink resolution should also correctly handle chained symlinks and detect a broken target so that the existing /usr/share/<name> fallback can be used where appropriate.

Impact

On systems where the QEMU data directory under /usr/local/share/ is represented by a relative symlink, the current behavior can result in an invalid source path being passed to the monitor filesystem setup.
This can prevent the container/unikernel from being created even though the expected QEMU data directory exists and is reachable through the symlink.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions