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;
}
}
}
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.confwhere/etc/sysusers.dis absent), the syscall is incorrectly aborted with anELOOP("Too many levels of symbolic links") error instead of returningENOENT.This causes standard Linux tools (like
systemd-sysusersor package post-install scripts) to crash because they expect anENOENTerror (which they can handle by creating the parent directory) but encounterELOOPinstead.Steps to Reproduce
Run any guest command under
elfusethat 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.confENOENT.ELOOP.Root Cause
In proc-state.c,
sysroot_path_is_containedcallsrealpathon the resolved host path to verify it sits within the guestsysroot:Since the parent directory does not exist,
realpathfails on the host and setserrno = ENOENT. However,sysroot_path_is_containedtreats anyrealpathfailure as a containment violation and returnsfalse.elfusethen maps containment violations toELOOPto abort the syscall, hiding the legitimateENOENTerror.Proposed Solution
If
realpathfails withENOENTorENOTDIR, 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:And update
sysroot_path_is_containedto fallback to this helper whenrealpathreturnsENOENTorENOTDIR: