safe_openat fallback ignores dirfd: container device creation fails on kernels without openat2
- Dominant language
- C
- Stars
- 4.1k
- Forks
- 444
- Avg merge
- 1d 18h
- Merged PRs (30d)
- 43
Description
## 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 `/null` (ENOENT) while creating the default device `/dev/null` — the correct target `/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, ...)`:
```c
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 `/dev` (a subdirectory), so the fallback resolves `/null` instead of `/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/`, strip the `rootfs` prefix, join with `path`, verify with `chroot_realpath`, then `openat(dirfd, path)`. Patch against 1.21:
```diff
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;
```
Contributor guide
Research direction
Start in src/libcrun/utils.c at safe_openat_fallback and trace callers such as libcrun_create_dev and crun_safe_ensure_at. Verify the fallback behavior on a kernel without openat2, especially with a dirfd for a rootfs subdirectory. Done means non-rootfs dirfds resolve the intended path and default device creation succeeds.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c, linux
- Domain
- operating-systems
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 70/100