From 7d602fc87e2cf3028b59016466a5d7c1bb88b235 Mon Sep 17 00:00:00 2001 From: Adrian Reber Date: Thu, 11 Jun 2026 15:24:46 +0000 Subject: [PATCH] fsnotify: add overlay directory walk fallback When a process has inotify (or fanotify) watches on files inside an overlayfs mount, criu dump fails because open_by_handle_at() does not work for overlay file handles (type OVL_FILEID_V1 = 0xf8). The dump log shows: fsnotify: wd 0x000001 s_dev 0x00002d i_ino 0x101ad2 mask 0x00033a fsnotify: [fhandle] bytes 0x000020 type 0x0000f8 ... fsnotify: Handle 0x2d:0x101ad2 cannot be opened Error (criu/fsnotify.c:284): fsnotify: Can't dump that handle To reproduce manually: mkdir -p /tmp/{lower,upper,work,merged} touch /tmp/lower/testfile mount -t overlay overlay \ -o lowerdir=/tmp/lower,upperdir=/tmp/upper,workdir=/tmp/work \ /tmp/merged inotifywait -m /tmp/merged/testfile & PID=$! criu dump -t $PID -D /tmp/imgs --shell-job --ext-unix-sk alloc_openable() iterates mounts matching s_dev and tries open_by_handle_at() for each one. Overlay file handles use the private OVL_FILEID_V1 type which open_by_handle_at() does not support, so the call always fails and the dump aborts. Add a fallback for overlay mounts: when open_by_handle_at() fails and the mount is identified as FSTYPE__OVERLAYFS, walk the overlay mount's directory tree with opendir/readdir/fstatat to find the file matching the (s_dev, i_ino) pair from the handle. Store the discovered absolute path in f_handle->path so that get_mark_path() uses the path-based openat() strategy on restore, which works on overlay mounts. The fallback only triggers when open_by_handle_at() fails AND the mount type is FSTYPE__OVERLAYFS, so there is no impact on other filesystems. Assisted-by: Claude Code (claude-opus-4-6):default Signed-off-by: Adrian Reber --- criu/fsnotify.c | 183 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 182 insertions(+), 1 deletion(-) diff --git a/criu/fsnotify.c b/criu/fsnotify.c index eb80dd961..498e48635 100644 --- a/criu/fsnotify.c +++ b/criu/fsnotify.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -108,6 +109,163 @@ enum { ERR_GENERIC = -3 }; +#define OVL_WALK_MAX_DEPTH 64 +#define OVL_WALK_WARN_THRESHOLD 10000 + +/* + * Recursively walk a directory tree looking for an inode matching + * (s_dev, i_ino). Returns an allocated path string on success, + * NULL when not found. The caller is responsible for freeing the + * returned string via xfree(). + * + * Note: this function takes ownership of @dirfd and closes it + * (via fdopendir/closedir) whether it succeeds or fails. + * + * @visited is incremented for each inode examined and is used to + * emit a one-time warning when the walk becomes expensive. + */ +static char *__walk_overlay_dir(int dirfd, const char *base, + unsigned int s_dev, + unsigned long i_ino, int depth, + unsigned long *visited) +{ + DIR *dfd; + struct dirent *de; + char *found = NULL; + + if (depth <= 0) { + close(dirfd); + return NULL; + } + + dfd = fdopendir(dirfd); + if (!dfd) { + pr_perror("Can't fdopendir for overlay walk"); + close(dirfd); + return NULL; + } + + while (1) { + struct stat st; + + errno = 0; + de = readdir(dfd); + if (!de) + break; + + if (dir_dots(de)) + continue; + + if (fstatat(dirfd, de->d_name, &st, + AT_SYMLINK_NOFOLLOW) < 0) { + pr_debug("overlay walk: fstatat(%s/%s) failed: %s\n", + base, de->d_name, strerror(errno)); + continue; + } + + (*visited)++; + if (*visited == OVL_WALK_WARN_THRESHOLD) + pr_warn("overlay walk: examined %lu entries so far " + "looking for ino %lx, mount may be large\n", + *visited, i_ino); + + if (MKKDEV(major(st.st_dev), minor(st.st_dev)) == s_dev && + st.st_ino == i_ino) { + found = xsprintf("%s/%s", base, de->d_name); + if (!found) + pr_err("OOM building overlay path for ino %lx\n", + i_ino); + break; + } + + if (S_ISDIR(st.st_mode)) { + int subfd; + char *subbase; + + subfd = openat(dirfd, de->d_name, + O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (subfd < 0) + continue; + + subbase = xsprintf("%s/%s", base, de->d_name); + if (!subbase) { + close(subfd); + continue; + } + + found = __walk_overlay_dir(subfd, subbase, + s_dev, i_ino, + depth - 1, + visited); + xfree(subbase); + if (found) + break; + } + } + + if (!de && errno) + pr_perror("overlay walk: readdir failed on %s", base); + + closedir(dfd); + return found; +} + +/* + * Try to locate a file on an overlay mount by walking the directory + * tree and matching (s_dev, i_ino). Returns an allocated absolute + * path (e.g. "/tmp/merged/subdir/file") on success, NULL on failure. + */ +static char *find_path_on_overlay(struct mount_info *m, + unsigned int s_dev, + unsigned long i_ino) +{ + int root_fd, mount_fd; + unsigned long visited = 0; + char *base, *found; + struct stat st; + + root_fd = mntns_get_root_fd(m->nsid); + if (root_fd < 0) + return NULL; + + /* + * ns_mountpoint has a leading dot, e.g. "./tmp/merged", + * which makes it relative to mntns root for openat. + */ + mount_fd = openat(root_fd, m->ns_mountpoint, O_RDONLY | O_DIRECTORY); + if (mount_fd < 0) { + pr_perror("Can't open overlay mountpoint %s", + m->ns_mountpoint); + return NULL; + } + + /* + * The path stored in f_handle->path must be absolute so + * that get_mark_path() can strip the leading "/" and use + * openat(mntns_root, path + 1, O_PATH) on restore. + * ns_mountpoint + 1 gives us e.g. "/tmp/merged". + */ + base = m->ns_mountpoint + 1; + + /* Check if the mountpoint directory itself is the target */ + if (fstat(mount_fd, &st) == 0 && + MKKDEV(major(st.st_dev), minor(st.st_dev)) == s_dev && + st.st_ino == i_ino) { + close(mount_fd); + return xstrdup(base); + } + + found = __walk_overlay_dir(mount_fd, base, s_dev, i_ino, + OVL_WALK_MAX_DEPTH, &visited); + /* mount_fd is consumed by fdopendir inside __walk_overlay_dir */ + + if (found) + pr_debug("overlay walk: found ino %lx after %lu entries\n", + i_ino, visited); + + return found; +} + static char *alloc_openable(unsigned int s_dev, unsigned long i_ino, FhEntry *f_handle) { struct mount_info *m; @@ -144,8 +302,31 @@ static char *alloc_openable(unsigned int s_dev, unsigned long i_ino, FhEntry *f_ fd = userns_call(open_by_handle, UNS_FDOUT, &handle, sizeof(handle), mntfd); close(mntfd); - if (fd < 0) + if (fd < 0) { + if (m->fstype->code == FSTYPE__OVERLAYFS) { + char *ovl_path; + + pr_debug("\t\tHandle open failed on overlay," + " trying dir walk for %lx\n", + i_ino); + ovl_path = find_path_on_overlay(m, s_dev, + i_ino); + if (ovl_path) { + if (root_ns_mask & CLONE_NEWNS) { + f_handle->has_mnt_id = true; + f_handle->mnt_id = m->mnt_id; + } + return ovl_path; + } + /* + * Mark as found so the caller gets + * ERR_NO_PATH_IN_MOUNT and falls + * through to irmap lookup. + */ + suitable_mount_found = 1; + } continue; + } suitable_mount_found = 1; if (read_fd_link(fd, buf, sizeof(buf)) < 0) {