-
Notifications
You must be signed in to change notification settings - Fork 107
add FUSE_OVFS_IOC_REFRESH_DIR ioctl to refresh directory cache #462
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -100,6 +100,12 @@ pub struct OverlayFs { | |
| notifier: Arc<OnceLock<fuser::Notifier>>, | ||
| } | ||
|
|
||
| /// 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<OvlLayer>, | ||
| 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 { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we need to make sure the caller has appropriate permissions, can a process without privileges call the ioctl?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no special permission required by
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it can be abused by an unprivileged user (e.g. uid != 0 inside the container), to cause a lot of load on the file system as it invalidates the entire directory. |
||
| 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; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| #define _GNU_SOURCE | ||
| #include <stdio.h> | ||
| #include <stdlib.h> | ||
| #include <fcntl.h> | ||
| #include <sys/ioctl.h> | ||
| #include <unistd.h> | ||
| #include <errno.h> | ||
| #include <string.h> | ||
|
|
||
| /* _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 <directory>\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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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!" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can't we just use
mark_unloaded?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In fact, I tried
mark_unloadedbut that does not work for.wh.*changes. And I dont want to change the load part.