Environment
- Linux arm64, kernel 4.14.190 (no
openat2 syscall; openat2 was added in Linux 5.6)
- crun 1.21 (Debian); code verified identical in tags 1.22–1.28; device creation was refactored in
main but the fallback below is unchanged there
Symptom
podman run fails at container start:
Error: crun: cannot resolve `null` under rootfs: No such file or directory: OCI runtime attempted to invoke a command that was not found
strace shows crun resolving <rootfs>/null (ENOENT) while creating the default device /dev/null — the correct target <rootfs>/dev/null exists. Same error has been reported in the wild with "switch to runc" as the only workaround.
Root cause
safe_openat() (src/libcrun/utils.c) first tries openat2(dirfd, path, RESOLVE_IN_ROOT). On kernels without openat2 this returns ENOSYS (also EINVAL/EPERM, e.g. when seccomp filters it), and crun falls back to safe_openat_fallback(dirfd, rootfs, path, ...):
path_in_chroot = chroot_realpath (rootfs, path, buffer);
...
ret = openat (dirfd, path_in_chroot, flags, mode);
The fallback resolves the path relative to rootfs only, ignoring dirfd; dirfd is used solely for the final openat(). That is only correct when dirfd == rootfsfd. libcrun_create_dev() calls safe_openat(devfd, rootfs, rel_dev, ...) with devfd = fd of <rootfs>/dev (a subdirectory), so the fallback resolves <rootfs>/null instead of <rootfs>/dev/null → ENOENT → container start fails. Any caller passing a non-rootfs dirfd (crun_safe_ensure_at etc.) is affected the same way.
Impact
On any kernel without openat2 (pre-5.6, or sandboxes where seccomp blocks it), crun cannot create the default devices and no container can start. A/B verified locally: unpatched crun fails with the error above; patched crun creates the container successfully.
Status in main
main has refactored device creation to mknod_and_set_attrs (mknodat/fchmodat/fchownat), which sidesteps this instance — but safe_openat_fallback itself is still dirfd-blind in main, so the bug class remains for any caller passing a subdirectory fd.
Suggested fix
Make the fallback dirfd-aware: resolve dirfd via /proc/self/fd/<dirfd>, strip the rootfs prefix, join with path, verify with chroot_realpath, then openat(dirfd, path). Patch against 1.21:
diff --git a/src/libcrun/utils.c b/src/libcrun/utils.c
index 53f3643..1145d5c 100644
--- a/src/libcrun/utils.c
+++ b/src/libcrun/utils.c
@@ -334,11 +334,33 @@ safe_openat_fallback (int dirfd, const char *rootfs, const char *path, int flags
int mode, libcrun_error_t *err)
{
const char *path_in_chroot;
+ const char *original_path = path;
cleanup_close int fd = -1;
char buffer[PATH_MAX];
+ char dirfd_path[PATH_MAX];
+ char joined[PATH_MAX];
size_t rootfs_len = strlen (rootfs);
+ bool dirfd_relative = dirfd >= 0 && dirfd != AT_FDCWD && path[0] != '/';
int ret;
+ if (dirfd_relative)
+ {
+ proc_fd_path_t fdpath;
+ ssize_t len;
+
+ get_proc_self_fd_path (fdpath, dirfd);
+ len = TEMP_FAILURE_RETRY (readlink (fdpath, dirfd_path, sizeof (dirfd_path) - 1));
+ if (UNLIKELY (len < 0))
+ return crun_make_error (err, errno, "readlink `%s`", fdpath);
+ dirfd_path[len] = '\0';
+
+ if (strncmp (dirfd_path, rootfs, rootfs_len) == 0 && dirfd_path[rootfs_len] == '/')
+ {
+ snprintf (joined, sizeof (joined), "%s/%s", dirfd_path + rootfs_len + 1, path);
+ path = joined;
+ }
+ }
+
path_in_chroot = chroot_realpath (rootfs, path, buffer);
if (path_in_chroot == NULL)
return crun_make_error (err, errno, "cannot resolve `%s` under rootfs", path);
@@ -355,9 +377,9 @@ safe_openat_fallback (int dirfd, const char *rootfs, const char *path, int flags
return ret;
}
- ret = openat (dirfd, path_in_chroot, flags, mode);
+ ret = openat (dirfd, dirfd_relative ? original_path : path_in_chroot, flags, mode);
if (UNLIKELY (ret < 0))
- return crun_make_error (err, errno, "open `%s`", path);
+ return crun_make_error (err, errno, "open `%s`", original_path);
fd = ret;
Environment
openat2syscall;openat2was added in Linux 5.6)mainbut the fallback below is unchanged thereSymptom
podman runfails at container start:strace shows crun resolving
<rootfs>/null(ENOENT) while creating the default device/dev/null— the correct target<rootfs>/dev/nullexists. Same error has been reported in the wild with "switch to runc" as the only workaround.Root cause
safe_openat()(src/libcrun/utils.c) first triesopenat2(dirfd, path, RESOLVE_IN_ROOT). On kernels withoutopenat2this returnsENOSYS(alsoEINVAL/EPERM, e.g. when seccomp filters it), and crun falls back tosafe_openat_fallback(dirfd, rootfs, path, ...):The fallback resolves the path relative to
rootfsonly, ignoringdirfd;dirfdis used solely for the finalopenat(). That is only correct whendirfd == rootfsfd.libcrun_create_dev()callssafe_openat(devfd, rootfs, rel_dev, ...)withdevfd= fd of<rootfs>/dev(a subdirectory), so the fallback resolves<rootfs>/nullinstead of<rootfs>/dev/null→ ENOENT → container start fails. Any caller passing a non-rootfs dirfd (crun_safe_ensure_atetc.) is affected the same way.Impact
On any kernel without
openat2(pre-5.6, or sandboxes where seccomp blocks it), crun cannot create the default devices and no container can start. A/B verified locally: unpatched crun fails with the error above; patched crun creates the container successfully.Status in main
mainhas refactored device creation tomknod_and_set_attrs(mknodat/fchmodat/fchownat), which sidesteps this instance — butsafe_openat_fallbackitself is still dirfd-blind inmain, so the bug class remains for any caller passing a subdirectory fd.Suggested fix
Make the fallback dirfd-aware: resolve
dirfdvia/proc/self/fd/<dirfd>, strip therootfsprefix, join withpath, verify withchroot_realpath, thenopenat(dirfd, path). Patch against 1.21: