criu/3043.patch
Adrian Reber 850fe769aa Apply patch to fix RHEL-193276
Resolves: RHEL-193276

Signed-off-by: Adrian Reber <areber@redhat.com>
2026-07-08 14:42:11 +00:00

779 lines
21 KiB
Diff

From 143b1978b47c519c3754bd4139798bc177cddd97 Mon Sep 17 00:00:00 2001
From: Adrian Reber <areber@redhat.com>
Date: Thu, 11 Jun 2026 15:24:46 +0000
Subject: [PATCH 1/3] 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 <areber@redhat.com>
---
criu/fsnotify.c | 183 +++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 182 insertions(+), 1 deletion(-)
diff --git a/criu/fsnotify.c b/criu/fsnotify.c
index eb80dd9615..498e48635d 100644
--- a/criu/fsnotify.c
+++ b/criu/fsnotify.c
@@ -7,6 +7,7 @@
#include <string.h>
#include <utime.h>
#include <limits.h>
+#include <dirent.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/inotify.h>
@@ -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) {
From 53b8358a1f39f9f94844f2c147be472fa0b38da1 Mon Sep 17 00:00:00 2001
From: Adrian Reber <areber@redhat.com>
Date: Thu, 11 Jun 2026 15:41:19 +0000
Subject: [PATCH 2/3] test/others/overlayfs: add inotify C/R test
Add a test that exercises checkpoint/restore of a process with an
inotify watch on a file inside an overlayfs mount.
The test compiles inotify_test.c (a simple program that sets up an
inotify watch and blocks reading events), starts it watching a file
on an overlay mount, dumps and restores via criu, then verifies the
restored process is still alive.
Run with: make -C test/others/overlayfs run-inotify
Assisted-by: Claude Code (claude-opus-4-6):default
Signed-off-by: Adrian Reber <areber@redhat.com>
---
test/others/overlayfs/.gitignore | 1 +
test/others/overlayfs/Makefile | 17 ++++--
test/others/overlayfs/inotify_test.c | 69 ++++++++++++++++++++++++
test/others/overlayfs/run-inotify.sh | 78 ++++++++++++++++++++++++++++
4 files changed, 162 insertions(+), 3 deletions(-)
create mode 100644 test/others/overlayfs/.gitignore
create mode 100644 test/others/overlayfs/inotify_test.c
create mode 100755 test/others/overlayfs/run-inotify.sh
diff --git a/test/others/overlayfs/.gitignore b/test/others/overlayfs/.gitignore
new file mode 100644
index 0000000000..ec554b8965
--- /dev/null
+++ b/test/others/overlayfs/.gitignore
@@ -0,0 +1 @@
+inotify_test
diff --git a/test/others/overlayfs/Makefile b/test/others/overlayfs/Makefile
index 78c246bd88..1ebf139f60 100644
--- a/test/others/overlayfs/Makefile
+++ b/test/others/overlayfs/Makefile
@@ -1,6 +1,17 @@
-run:
+CC ?= cc
+
+inotify_test: inotify_test.c
+ $(CC) -Wall -o $@ $<
+
+run: run-inotify
./run.sh
+run-inotify: inotify_test
+ ./run-inotify.sh
+
clean:
- umount -f overlay_test/z
- rm -rf overlay_test output checkpoint
+ umount -f overlay_test/z 2>/dev/null || true
+ umount -f ovl_inotify/merged 2>/dev/null || true
+ rm -rf overlay_test ovl_inotify output checkpoint inotify_test
+
+.PHONY: run run-inotify clean
diff --git a/test/others/overlayfs/inotify_test.c b/test/others/overlayfs/inotify_test.c
new file mode 100644
index 0000000000..00f1b7f99b
--- /dev/null
+++ b/test/others/overlayfs/inotify_test.c
@@ -0,0 +1,69 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <unistd.h>
+#include <sys/inotify.h>
+
+#define EVENT_SIZE (sizeof(struct inotify_event))
+#define BUF_LEN (1024 * (EVENT_SIZE + 16))
+
+int main(int argc, char **argv)
+{
+ char buf[BUF_LEN];
+ int fd, wd, len, i;
+
+ if (argc < 2) {
+ fprintf(stderr, "Usage: %s <path>\n", argv[0]);
+ return 1;
+ }
+
+ fd = inotify_init();
+ if (fd < 0) {
+ perror("inotify_init");
+ return 1;
+ }
+
+ wd = inotify_add_watch(fd, argv[1],
+ IN_MODIFY | IN_CREATE | IN_DELETE |
+ IN_OPEN | IN_CLOSE);
+ if (wd < 0) {
+ perror("inotify_add_watch");
+ close(fd);
+ return 1;
+ }
+
+ printf("Watching %s for events...\n", argv[1]);
+
+ while (1) {
+ len = read(fd, buf, BUF_LEN);
+ if (len < 0) {
+ perror("read");
+ break;
+ }
+
+ i = 0;
+ while (i < len) {
+ struct inotify_event *event;
+
+ event = (struct inotify_event *)&buf[i];
+
+ if (event->mask & IN_CREATE)
+ printf("CREATE: %s\n", event->name);
+ if (event->mask & IN_DELETE)
+ printf("DELETE: %s\n", event->name);
+ if (event->mask & IN_MODIFY)
+ printf("MODIFY: %s\n", event->name);
+ if (event->mask & IN_OPEN)
+ printf("OPEN: %s\n", event->name);
+ if (event->mask & IN_CLOSE)
+ printf("CLOSE: %s\n", event->name);
+
+ i += EVENT_SIZE + event->len;
+ }
+ }
+
+ inotify_rm_watch(fd, wd);
+ close(fd);
+
+ return 0;
+}
diff --git a/test/others/overlayfs/run-inotify.sh b/test/others/overlayfs/run-inotify.sh
new file mode 100755
index 0000000000..8711797221
--- /dev/null
+++ b/test/others/overlayfs/run-inotify.sh
@@ -0,0 +1,78 @@
+#!/bin/bash
+
+set -eu
+
+SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
+CRIU="${SCRIPT_DIR}/../../../criu/criu"
+ORIG_WD=$(pwd)
+PROC_PID=""
+
+cleanup() {
+ if [ -n "${PROC_PID}" ]; then
+ kill "${PROC_PID}" > /dev/null 2>&1 || true
+ wait "${PROC_PID}" 2>/dev/null || true
+ fi
+ cd "${ORIG_WD}"
+ umount ovl_inotify/merged 2>/dev/null || true
+ rm -rf ovl_inotify
+}
+
+trap cleanup EXIT
+
+setup() {
+ mkdir -p ovl_inotify
+ cd ovl_inotify
+ mkdir -p lower upper work merged checkpoint
+
+ cp "${SCRIPT_DIR}/inotify_test" .
+ touch lower/testfile
+ mount -t overlay overlay \
+ -o lowerdir=lower,upperdir=upper,workdir=work merged
+
+ setsid bash -c 'echo $$ > inotify.pid; exec ./inotify_test "$@"' \
+ -- merged/testfile < /dev/null &> output &
+ sleep 1
+ PROC_PID=$(cat inotify.pid)
+ echo "PROC_PID=$PROC_PID"
+}
+
+check_criu() {
+ echo "Dumping $PROC_PID..."
+ if ! $CRIU dump -D checkpoint -t "${PROC_PID}" --shell-job -v4; then
+ echo "ERROR! dump failed"
+ return 1
+ fi
+
+ echo "Restoring..."
+ if ! $CRIU restore -d -D checkpoint --shell-job -v4; then
+ echo "ERROR! restore failed"
+ return 1
+ fi
+
+ sleep 1
+
+ # Trigger an inotify event on the restored process
+ touch merged/testfile
+ sleep 1
+
+ # The restored process should still be alive
+ if ! kill -0 "${PROC_PID}" 2>/dev/null; then
+ echo "ERROR! restored process is not running"
+ return 1
+ fi
+
+ return 0
+}
+
+main() {
+ setup
+
+ if ! check_criu; then
+ exit 1
+ fi
+
+ echo "OverlayFS inotify C/R successful."
+ exit 0
+}
+
+main
From 082ca25d2280a632e0c19540a52d54460a23cff4 Mon Sep 17 00:00:00 2001
From: Adrian Reber <areber@redhat.com>
Date: Thu, 11 Jun 2026 15:50:54 +0000
Subject: [PATCH 3/3] zdtm: add inotify_overlayfs test
Add a ZDTM test that verifies inotify watches on files inside an
overlayfs mount survive checkpoint/restore.
The test uses the external mount pattern (like mnt_ext_auto): in
the ZDTM_NEWNS=1 phase it creates an overlay mount before
unshare(CLONE_NEWNS), so CRIU treats it as an external mount and
does not attempt to reconstruct the overlay on restore. After
restore the test opens the watched file and reads back the inotify
event to confirm the watch is intact.
This is needed because the test/others/overlayfs test only checks
that the process survives C/R. This ZDTM test validates that the
inotify watch itself is properly dumped and restored by exercising
the overlay directory walk fallback added in fsnotify.c.
The test is restricted to the 'ns' flavor because the overlay
setup requires the ZDTM_NEWNS=1 lifecycle and root privileges
for mounting overlayfs.
Assisted-by: Claude Code (claude-opus-4-6):default
Signed-off-by: Adrian Reber <areber@redhat.com>
---
test/zdtm/static/Makefile | 1 +
test/zdtm/static/inotify_overlayfs.c | 213 ++++++++++++++++++++++++
test/zdtm/static/inotify_overlayfs.desc | 2 +
test/zdtm/static/inotify_overlayfs.hook | 19 +++
4 files changed, 235 insertions(+)
create mode 100644 test/zdtm/static/inotify_overlayfs.c
create mode 100644 test/zdtm/static/inotify_overlayfs.desc
create mode 100755 test/zdtm/static/inotify_overlayfs.hook
diff --git a/test/zdtm/static/Makefile b/test/zdtm/static/Makefile
index 0e2580ed6e..d53eec2e2d 100644
--- a/test/zdtm/static/Makefile
+++ b/test/zdtm/static/Makefile
@@ -481,6 +481,7 @@ TST_DIR = \
symlink \
symlink01 \
unbindable \
+ inotify_overlayfs \
TST_DIR_FILE = \
chroot \
diff --git a/test/zdtm/static/inotify_overlayfs.c b/test/zdtm/static/inotify_overlayfs.c
new file mode 100644
index 0000000000..5044037a9e
--- /dev/null
+++ b/test/zdtm/static/inotify_overlayfs.c
@@ -0,0 +1,213 @@
+#include <sched.h>
+#include <unistd.h>
+#include <limits.h>
+#include <ftw.h>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mount.h>
+#include <fcntl.h>
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <dirent.h>
+#include <sys/inotify.h>
+
+#include "zdtmtst.h"
+
+const char *test_doc = "Check inotify C/R on overlayfs";
+const char *test_author = "CRIU contributors";
+
+char *dirname = "inotify_overlayfs.test";
+TEST_OPTION(dirname, string, "directory name", 1);
+
+#define BUFF_SIZE ((sizeof(struct inotify_event) + PATH_MAX))
+#define TEST_FNAME "testfile"
+
+#define OVL_DIR_SUFFIX "zdtm_inotify_ovl.XXXXXX"
+
+static int rm_entry(const char *path, const struct stat *st,
+ int flag, struct FTW *ftw)
+{
+ return remove(path);
+}
+
+/*
+ * Clean up overlay backing dirs matching the zdtm_inotify_ovl prefix
+ * in the given directory. Called at setup to remove leftovers from
+ * previous runs. Post-test cleanup is handled by
+ * inotify_overlayfs.hook (--clean).
+ */
+static void cleanup_stale_ovl_dirs(const char *parent)
+{
+ DIR *d;
+ struct dirent *de;
+ char path[PATH_MAX];
+
+ d = opendir(parent);
+ if (!d)
+ return;
+
+ while ((de = readdir(d)) != NULL) {
+ if (strncmp(de->d_name, "zdtm_inotify_ovl.",
+ strlen("zdtm_inotify_ovl.")) != 0)
+ continue;
+ snprintf(path, sizeof(path), "%s/%s", parent, de->d_name);
+ nftw(path, rm_entry, 16, FTW_DEPTH | FTW_PHYS);
+ }
+ closedir(d);
+}
+
+int main(int argc, char *argv[])
+{
+ char merged[PATH_MAX], watch_path[PATH_MAX];
+ char buf[BUFF_SIZE];
+ int inotify_fd, wd, fd, dir_fd;
+ char *zdtm_newns = getenv("ZDTM_NEWNS");
+ cleanup_free char *cwd = NULL;
+
+ cwd = get_current_dir_name();
+ snprintf(merged, sizeof(merged), "%s/%s", cwd, dirname);
+
+ if (zdtm_newns && !strcmp(zdtm_newns, "1")) {
+ char ovl_dir[PATH_MAX - 2];
+ char ovl_lower[PATH_MAX], ovl_upper[PATH_MAX];
+ char ovl_work[PATH_MAX];
+ char ovl_mnt[] = "/tmp/zdtm_ino_mnt.XXXXXX";
+ char opts[3 * PATH_MAX + 32];
+
+ /*
+ * Phase 1: running before namespace creation.
+ *
+ * Overlay backing dirs (lower/upper/work) are placed
+ * in cwd so they live on a real filesystem — tmpfs
+ * does not support overlayfs upperdir on all kernels.
+ *
+ * The overlay is mounted at a temporary directory
+ * under /tmp, which is outside ZDTM_ROOT and will
+ * not be visible after pivot_root. After unshare
+ * we bind-mount it into the test directory; the bind
+ * creates a shared/master relationship that
+ * --external mnt[]:s can detect.
+ */
+ cleanup_stale_ovl_dirs(cwd);
+
+ snprintf(ovl_dir, sizeof(ovl_dir), "%s/" OVL_DIR_SUFFIX, cwd);
+ if (!mkdtemp(ovl_dir)) {
+ pr_perror("Can't create overlay tmpdir");
+ return 1;
+ }
+
+ snprintf(ovl_lower, sizeof(ovl_lower), "%s/l", ovl_dir);
+ snprintf(ovl_upper, sizeof(ovl_upper), "%s/u", ovl_dir);
+ snprintf(ovl_work, sizeof(ovl_work), "%s/w", ovl_dir);
+
+ if (mkdir(ovl_lower, 0700) < 0) {
+ pr_perror("Can't mkdir %s", ovl_lower);
+ return 1;
+ }
+ if (mkdir(ovl_upper, 0700) < 0) {
+ pr_perror("Can't mkdir %s", ovl_upper);
+ return 1;
+ }
+ if (mkdir(ovl_work, 0700) < 0) {
+ pr_perror("Can't mkdir %s", ovl_work);
+ return 1;
+ }
+
+ /* Create a test file in the lower layer */
+ dir_fd = openat(AT_FDCWD, ovl_lower, O_RDONLY | O_DIRECTORY);
+ if (dir_fd < 0) {
+ pr_perror("Can't open lower dir %s", ovl_lower);
+ return 1;
+ }
+ fd = openat(dir_fd, TEST_FNAME, O_CREAT | O_WRONLY, 0644);
+ close(dir_fd);
+ if (fd < 0) {
+ pr_perror("Can't create test file in lower");
+ return 1;
+ }
+ close(fd);
+
+ /*
+ * Mount overlay at a path outside ZDTM_ROOT so it
+ * does not appear in mountinfo after pivot_root.
+ */
+ if (!mkdtemp(ovl_mnt)) {
+ pr_perror("Can't create overlay mount dir");
+ return 1;
+ }
+ snprintf(opts, sizeof(opts),
+ "lowerdir=%s,upperdir=%s,workdir=%s",
+ ovl_lower, ovl_upper, ovl_work);
+
+ if (mount("overlay", ovl_mnt, "overlay", 0, opts) < 0) {
+ pr_perror("Can't mount overlayfs at %s", ovl_mnt);
+ return 1;
+ }
+
+ if (unshare(CLONE_NEWNS)) {
+ pr_perror("unshare");
+ return 1;
+ }
+
+ /* Bind-mount into test directory inside new mntns */
+ if (mkdir(merged, 0700) < 0) {
+ pr_perror("Can't mkdir %s", merged);
+ return 1;
+ }
+ if (mount(ovl_mnt, merged, NULL, MS_BIND, NULL) < 0) {
+ pr_perror("Can't bind-mount overlay to %s", merged);
+ return 1;
+ }
+ }
+
+ /*
+ * test_init handles ZDTM_NEWNS internally:
+ * "1" -> ns_create() re-execs with "2" (never returns)
+ * "2" -> ns_init() forks, child continues
+ * unset -> normal execution
+ */
+ test_init(argc, argv);
+
+ snprintf(watch_path, sizeof(watch_path),
+ "%s/" TEST_FNAME, dirname);
+
+ inotify_fd = inotify_init1(IN_NONBLOCK);
+ if (inotify_fd < 0) {
+ pr_perror("inotify_init1 failed");
+ return 1;
+ }
+
+ wd = inotify_add_watch(inotify_fd, watch_path, IN_OPEN);
+ if (wd < 0) {
+ pr_perror("inotify_add_watch failed for %s", watch_path);
+ close(inotify_fd);
+ return 1;
+ }
+
+ test_daemon();
+ test_waitsig();
+
+ /* After restore, trigger an inotify event */
+ fd = open(watch_path, O_RDONLY);
+ if (fd < 0) {
+ fail("Can't open %s after restore", watch_path);
+ close(inotify_fd);
+ return 1;
+ }
+ close(fd);
+
+ /* Check that we got the event */
+ memset(buf, 0, sizeof(buf));
+ if (read(inotify_fd, buf, sizeof(buf)) <= 0) {
+ fail("No inotify events after restore");
+ close(inotify_fd);
+ return 1;
+ }
+
+ close(inotify_fd);
+ pass();
+ return 0;
+}
diff --git a/test/zdtm/static/inotify_overlayfs.desc b/test/zdtm/static/inotify_overlayfs.desc
new file mode 100644
index 0000000000..f1c3a17143
--- /dev/null
+++ b/test/zdtm/static/inotify_overlayfs.desc
@@ -0,0 +1,2 @@
+{'flags': 'suid', 'flavor': 'ns', 'feature': 'mnt_id',
+ 'opts': '--external mnt[]:s'}
diff --git a/test/zdtm/static/inotify_overlayfs.hook b/test/zdtm/static/inotify_overlayfs.hook
new file mode 100755
index 0000000000..06d128755e
--- /dev/null
+++ b/test/zdtm/static/inotify_overlayfs.hook
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+[ "$1" == "--clean" ] || exit 0
+
+# Remove overlay backing dirs left by the ZDTM_NEWNS=1 phase.
+# The test creates them in its working directory, which is the
+# same directory as this hook script.
+test_dir=$(dirname "$(readlink -f "$0")")
+for dir in "$test_dir"/zdtm_inotify_ovl.* ; do
+ [ -d "$dir" ] && rm -rf "$dir"
+done
+
+# Remove overlay mount point dirs from /tmp.
+for dir in /tmp/zdtm_ino_mnt.* ; do
+ umount "$dir" 2>/dev/null || true
+ [ -d "$dir" ] && rmdir "$dir"
+done
+
+exit 0