Skip to content

Path containment checks fail with ELOOP on non-existent parent directories #187

Description

@doanbaotrung

Description

When a guest process queries or attempts to create a file in a parent directory that does not exist yet (e.g., calling open(..., O_CREAT) on /etc/sysusers.d/basic.conf where /etc/sysusers.d is absent), the syscall is incorrectly aborted with an ELOOP ("Too many levels of symbolic links") error instead of returning ENOENT.

This causes standard Linux tools (like systemd-sysusers or package post-install scripts) to crash because they expect an ENOENT error (which they can handle by creating the parent directory) but encounter ELOOP instead.

Steps to Reproduce

Run any guest command under elfuse that attempts to open, check, or create a path whose parent directory does not exist, e.g.:

# Assuming /etc/sysusers.d does not exist in the sysroot
touch /etc/sysusers.d/basic.conf
  • Expected behavior: Syscall fails with ENOENT.
  • Actual behavior: Syscall fails with ELOOP.

Root Cause

In proc-state.c, sysroot_path_is_contained calls realpath on the resolved host path to verify it sits within the guest sysroot:

if (follow_final) {
    if (!realpath(resolved_path, real_path))
        return false; // <--- FAILS HERE on ENOENT/ENOTDIR
}

Since the parent directory does not exist, realpath fails on the host and sets errno = ENOENT. However, sysroot_path_is_contained treats any realpath failure as a containment violation and returns false. elfuse then maps containment violations to ELOOP to abort the syscall, hiding the legitimate ENOENT error.


Proposed Solution

If realpath fails with ENOENT or ENOTDIR, the containment check should recursively walk up the directory tree until it finds the closest existing parent directory, verify its containment, and permit the syscall if the parent is contained.

Suggested Patch:

Add a parent-recursive lookup helper in src/syscall/proc-state.c:

static bool realpath_existing_parent(const char *path, char *resolved_path)
{
    char temp[LINUX_PATH_MAX];
    str_copy_trunc(temp, path, sizeof(temp));

    while (1) {
        if (realpath(temp, resolved_path)) {
            return true;
        }
        if (errno != ENOENT && errno != ENOTDIR) {
            return false;
        }
        char *slash = strrchr(temp, '/');
        if (!slash) {
            return false;
        }
        if (slash == temp) {
            str_copy_trunc(temp, "/", sizeof(temp));
            if (realpath(temp, resolved_path)) {
                return true;
            }
            return false;
        }
        *slash = '\0'; // Move up to parent directory
    }
}

And update sysroot_path_is_contained to fallback to this helper when realpath returns ENOENT or ENOTDIR:

if (follow_final) {
    if (!realpath(resolved_path, real_path)) {
        if (errno == ENOENT || errno == ENOTDIR) {
            if (!realpath_existing_parent(resolved_path, real_path))
                return false;
        } else {
            return false;
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions