From 99404a3068773cebe401f9cfac7e9663ee9aeeed Mon Sep 17 00:00:00 2001 From: "jerry.wang" Date: Thu, 7 May 2026 18:49:53 +0800 Subject: [PATCH] add FUSE_OVFS_IOC_REFRESH_DIR ioctl to refresh directory cache --- src/node.rs | 14 ++ src/overlay.rs | 36 +++++ tests/ioctl-invalidate.c | 35 +++++ tests/test-cache-invalidate.sh | 248 +++++++++++++++++++++++++++++++++ 4 files changed, 333 insertions(+) create mode 100644 tests/ioctl-invalidate.c create mode 100755 tests/test-cache-invalidate.sh diff --git a/src/node.rs b/src/node.rs index 679b3ea..d7a8133 100644 --- a/src/node.rs +++ b/src/node.rs @@ -263,6 +263,20 @@ impl OvlNode { whiteouts.insert(name); } } + + /// Clear all children and whiteouts. Used by cache invalidation to + /// force a full re-scan of the directory. + pub fn clear_children(&mut self) { + if let DirState::Dir { + children, + whiteouts, + .. + } = &mut self.dir_state + { + children.clear(); + whiteouts.clear(); + } + } } impl Drop for OvlNode { diff --git a/src/overlay.rs b/src/overlay.rs index 35421a7..337682c 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -100,6 +100,12 @@ pub struct OverlayFs { notifier: Arc>, } +/// Custom ioctl command: clear and re-scan a directory's cached children. +/// Encoded as _IO('f', 0x66) — direction=none, size=0, type='f'(0x66), nr=0x66. +/// Immediately rebuilds the directory listing, picking up externally created +/// whiteout files and other changes. +const FUSE_OVFS_IOC_REFRESH_DIR: libc::Ioctl = (b'f' as libc::Ioctl) << 8 | 0x66; + struct OverlayInner { layers: Vec, inodes: InodeTable, @@ -3653,6 +3659,36 @@ impl Filesystem for OverlayFs { let is_set = cmd_ioctl == libc::FS_IOC_SETVERSION || cmd_ioctl == libc::FS_IOC_SETFLAGS; let is_get = cmd_ioctl == libc::FS_IOC_GETVERSION || cmd_ioctl == libc::FS_IOC_GETFLAGS; + // Custom command: clear and re-scan a directory's children. + if cmd_ioctl == FUSE_OVFS_IOC_REFRESH_DIR { + let node = match inner.node(&node_id) { + Ok(n) => n, + Err(e) => { + reply.error(Errno::from_i32(e.0)); + return; + } + }; + if !node.is_dir() { + reply.error(Errno::ENOTDIR); + return; + } + let path = inner.node_path(node_id); + let last_layer = match inner.nodes.get(&node_id) { + Some(n) => n.last_layer_idx, + None => { + reply.error(Errno::ENOENT); + return; + } + }; + if let Some(node) = inner.nodes.get_mut(&node_id) { + node.clear_children(); + } + inner.load_dir_impl(node_id, &path, Some(last_layer), &self.config); + debug!("ioctl invalidate: directory ino={}", ino); + reply.ioctl(0, &[]); + return; + } + if !is_set && !is_get { reply.error(Errno::ENOSYS); return; diff --git a/tests/ioctl-invalidate.c b/tests/ioctl-invalidate.c new file mode 100644 index 0000000..0e393ce --- /dev/null +++ b/tests/ioctl-invalidate.c @@ -0,0 +1,35 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +/* _IO('f', 0x66) — same value as FUSE_OVFS_IOC_REFRESH_DIR in overlay.rs */ +#define FUSE_OVFS_IOC_REFRESH_DIR _IO('f', 0x66) + +int main(int argc, char *argv[]) +{ + if (argc != 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + int fd = open(argv[1], O_RDONLY | O_DIRECTORY); + if (fd < 0) { + perror("open"); + return 1; + } + + int ret = ioctl(fd, FUSE_OVFS_IOC_REFRESH_DIR); + if (ret < 0) { + fprintf(stderr, "ioctl failed: %s\n", strerror(errno)); + close(fd); + return 1; + } + + close(fd); + return 0; +} diff --git a/tests/test-cache-invalidate.sh b/tests/test-cache-invalidate.sh new file mode 100755 index 0000000..25ebb09 --- /dev/null +++ b/tests/test-cache-invalidate.sh @@ -0,0 +1,248 @@ +#!/bin/bash +# Test: ioctl cache invalidation for directory children. + +set -xeuo pipefail + +# Build ioctl helper (resolve paths BEFORE cd $TESTDIR) +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +IOCTL_C="$SCRIPT_DIR/ioctl-invalidate.c" +IOCTL_BIN="$SCRIPT_DIR/ioctl-invalidate" +if [ ! -f "$IOCTL_BIN" ]; then + gcc -o "$IOCTL_BIN" "$IOCTL_C" +fi + +cleanup() { + cd / + umount "$MERGED" 2>/dev/null || true + rm -rf "$TESTDIR" +} + +TESTDIR=$(mktemp -d /tmp/test-cache-invalidate.XXXXXX) +trap cleanup EXIT +cd "$TESTDIR" + +MERGED="$TESTDIR/merged" + +# ======================================== +# Test 1: .wh.* whiteout prepared BEFORE mount +# ======================================== +echo "=== Test 1: .wh.* whiteout before mount ===" +mkdir -p lower upper workdir merged + +echo "lower file" > lower/myfile.txt +touch upper/.wh.myfile.txt + +fuse-overlayfs -o lowerdir=lower,upperdir=upper,workdir=workdir merged + +# Whiteout should work at mount time +test ! -e merged/myfile.txt + +umount merged +rm -rf lower upper workdir merged + +# ======================================== +# Test 2: Remove .wh.* after mount + ioctl → file reappears +# ======================================== +echo "=== Test 2: Remove .wh.* then ioctl ===" +mkdir -p lower upper workdir merged + +echo "lower file" > lower/myfile.txt +touch upper/.wh.myfile.txt + +fuse-overlayfs -o lowerdir=lower,upperdir=upper,workdir=workdir merged + +# Whiteout effective at mount time +test ! -e merged/myfile.txt + +# Load cache by listing the directory +ls merged > /dev/null + +# Remove whiteout externally +rm -f upper/.wh.myfile.txt + +# Cache loaded — file should still NOT be visible +test ! -e merged/myfile.txt + +# Invalidate +$IOCTL_BIN merged + +# ks_cache: use ls to verify (avoids stale kernel dentry cache from earlier stat) +ls merged/ > /tmp/cache-test-out.txt +grep myfile.txt /tmp/cache-test-out.txt +grep "lower file" merged/myfile.txt + +umount merged +rm -rf lower upper workdir merged + +# ======================================== +# Test 3: Add .wh.* after mount + ioctl → file disappears +# ======================================== +echo "=== Test 3: Add .wh.* then ioctl ===" +mkdir -p lower upper workdir merged + +echo "lower file" > lower/myfile.txt + +fuse-overlayfs -o lowerdir=lower,upperdir=upper,workdir=workdir merged + +# File visible after mount +test -e merged/myfile.txt + +# Load cache by listing the directory (avoids kernel dentry cache on myfile.txt) +ls merged > /dev/null + +# Verify file still visible (cache loaded) +ls merged/ > /tmp/cache-test-out.txt +grep myfile.txt /tmp/cache-test-out.txt + +# Create whiteout externally +touch upper/.wh.myfile.txt + +# Cache loaded — file should still be visible +ls merged/ > /tmp/cache-test-out.txt +grep myfile.txt /tmp/cache-test-out.txt + +# Invalidate +$IOCTL_BIN merged + +# File should now be hidden from directory listing +ls merged/ > /tmp/cache-test-out.txt +if grep -q myfile.txt /tmp/cache-test-out.txt; then + echo "ERROR: myfile.txt should be hidden after ioctl" + exit 1 +fi + +umount merged +rm -rf lower upper workdir merged + +# ======================================== +# Test 4: Add file to lower after mount + ioctl → file appears +# ======================================== +echo "=== Test 4: Add file to lower then ioctl ===" +mkdir -p lower upper workdir merged + +echo "initial" > lower/initial.txt + +fuse-overlayfs -o lowerdir=lower,upperdir=upper,workdir=workdir merged + +# Load cache +ls merged > /dev/null + +# Add file to lower externally +echo "new lower file" > lower/newfile.txt + +# Cache loaded — new file NOT visible +test ! -e merged/newfile.txt + +# Invalidate +$IOCTL_BIN merged + +# File should now be visible +test -e merged/newfile.txt +grep "new lower file" merged/newfile.txt + +umount merged +rm -rf lower upper workdir merged + +# ======================================== +# Test 5: ioctl on non-directory → ENOTDIR +# ======================================== +echo "=== Test 5: ioctl on non-directory ===" +mkdir -p lower upper workdir merged + +touch upper/file.txt + +fuse-overlayfs -o lowerdir=lower,upperdir=upper,workdir=workdir merged + +set +e +$IOCTL_BIN merged/file.txt +rc=$? +set -e +test "$rc" -ne 0 + +umount merged +rm -rf lower upper workdir merged + +# ======================================== +# Test 6: Remove file from lower externally + ioctl → file disappears +# ======================================== +echo "=== Test 6: Remove file from lower then ioctl ===" +mkdir -p lower upper workdir merged + +echo "remove me" > lower/rmfile.txt + +fuse-overlayfs -o lowerdir=lower,upperdir=upper,workdir=workdir merged + +# File visible +test -e merged/rmfile.txt + +# Load cache by listing (avoids kernel dentry on rmfile.txt itself) +ls merged > /dev/null + +# Verify file in listing +ls merged/ > /tmp/cache-test-out.txt +grep rmfile.txt /tmp/cache-test-out.txt + +# Remove file from lower externally +rm -f lower/rmfile.txt + +# Cache loaded — file should still be in listing +ls merged/ > /tmp/cache-test-out.txt +grep rmfile.txt /tmp/cache-test-out.txt + +# Invalidate +$IOCTL_BIN merged + +# File should now be gone from listing +ls merged/ > /tmp/cache-test-out.txt +if grep -q rmfile.txt /tmp/cache-test-out.txt; then + echo "ERROR: rmfile.txt should be gone after ioctl" + exit 1 +fi + +umount merged +rm -rf lower upper workdir merged + +# ======================================== +# Test 7: Multiple lower layers + whiteout in upper +# ======================================== +echo "=== Test 7: Multiple lower layers ===" +mkdir -p lower1 lower2 upper workdir merged + +echo "from lower1" > lower1/file_a.txt +echo "from lower2" > lower2/file_b.txt + +fuse-overlayfs -o lowerdir=lower1:lower2,upperdir=upper,workdir=workdir merged + +# Load cache +ls merged > /dev/null + +# Verify both files in listing +ls merged/ > /tmp/cache-test-out.txt +grep file_a.txt /tmp/cache-test-out.txt +grep file_b.txt /tmp/cache-test-out.txt + +# Create whiteout in upper for lower1's file +touch upper/.wh.file_a.txt + +# Cache loaded — both files still in listing +ls merged/ > /tmp/cache-test-out.txt +grep file_a.txt /tmp/cache-test-out.txt +grep file_b.txt /tmp/cache-test-out.txt + +# Invalidate +$IOCTL_BIN merged + +# file_a should be gone +ls merged/ > /tmp/cache-test-out.txt +if grep -q file_a.txt /tmp/cache-test-out.txt; then + echo "ERROR: file_a.txt should be hidden after ioctl" + exit 1 +fi +# file_b should still be there +grep file_b.txt /tmp/cache-test-out.txt +grep "from lower2" merged/file_b.txt + +umount merged +rm -rf lower1 lower2 upper workdir merged + +echo "All cache invalidation tests passed!"