Skip to content

Rewrite fuse-overlayfs in Rust#457

Open
giuseppe wants to merge 8 commits intocontainers:mainfrom
giuseppe:rust-rewrite
Open

Rewrite fuse-overlayfs in Rust#457
giuseppe wants to merge 8 commits intocontainers:mainfrom
giuseppe:rust-rewrite

Conversation

@giuseppe
Copy link
Copy Markdown
Member

Drop-in rewrite of fuse-overlayfs from C to Rust. Same CLI, same mount options, same on-disk format.

@giuseppe giuseppe force-pushed the rust-rewrite branch 4 times, most recently from 84edfe7 to e64d961 Compare March 26, 2026 10:11
Copy link
Copy Markdown
Member

@saschagrunert saschagrunert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took a pass through the Rust rewrite. Left a few comments, mostly around dead code and a dangling fd in main.rs.

src/main.rs Outdated
std::process::exit(1);
}
};
fd.as_raw_fd()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: fd is an OwnedFd that gets dropped at the end of this if let block, closing the OS file descriptor. workdir_fd then holds a dangling raw fd.

Keep the OwnedFd alive (e.g. store it alongside workdir_fd, or pass the OwnedFd directly to OverlayFs::new).

src/main.rs Outdated
};

// Read overflow IDs
let _overflow = mapping::OverflowIds::read();
Copy link
Copy Markdown
Member

@saschagrunert saschagrunert Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code: _overflow is never used. OverlayInner::new creates its own OverflowIds. This can be removed.

src/main.rs Outdated
"noexec" => fuse_options.push(fuser::MountOption::NoExec),
"atime" => fuse_options.push(fuser::MountOption::Atime),
"noatime" => fuse_options.push(fuser::MountOption::NoAtime),
"diratime" => fuse_options.push(fuser::MountOption::DirSync), // closest mapping
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong mapping: DirSync != diratime. diratime controls directory access-time updates. This should be CUSTOM("diratime".to_string()).

let parent_fd = safe_openat(
dirfd,
parent,
libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_PATH,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O_NOFOLLOW causes ELOOP if any component in the parent path is a symlink. Since openat2 with RESOLVE_IN_ROOT already handles symlink containment, and the plain openat fallback path doesn't benefit from O_NOFOLLOW on intermediate components, drop O_NOFOLLOW and keep O_DIRECTORY | O_PATH.

}
}

// Fallback to plain openat
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent fallback from openat2 to plain openat drops the RESOLVE_IN_ROOT guarantee. Log a warning so users know the security constraint is absent.

Cargo.toml Outdated
fuser = { version = "0.17", features = ["abi-7-40"] }
nix = { version = "0.29", features = ["fs", "dir", "mount", "user", "signal", "resource"] }
libc = "0.2"
libloading = "0.8"
Copy link
Copy Markdown
Member

@saschagrunert saschagrunert Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

libloading is unused in the codebase. This can be removed.


// Safety: DirStream is only used within the big-lock mutex in OverlayFs.
// The mutex guarantees single-threaded access.
unsafe impl Send for DirStream {}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment says "never handed to another thread" but Send is exactly what enables cross-thread transfer. The real safety invariant is that DIR* is only used by one thread at a time (no concurrent readdir calls), which &mut self on next() already enforces.

src/mapping.rs Outdated
}

/// Convenience: map a UID from host to container.
pub fn get_uid(
Copy link
Copy Markdown
Member

@saschagrunert saschagrunert Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_uid and get_gid are never called. These can be removed.

fuse_map: FxHashMap::default(),
same_device: true,
next_fallback: 0x8000_0000_0000_0000,
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With ino_t_32 (32-bit inode numbers), next_fallback starts at u32::MAX and wraps around quickly, producing values 0 and 1 that collide with FUSE_ROOT_ID. Consider starting at 2 or skipping reserved values.

src/overlay.rs Outdated
drop(backings);
self.open_files.write().insert(fh, Arc::new(fd));
self.fh_to_ino.write().insert(fh, ino);
self.inode_backings.write().get_mut(&ino).unwrap().1 += 1;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition: the read lock on inode_backings is dropped on line 198, then a write lock is re-acquired here. Between the two, a concurrent release() can remove this entry, causing .unwrap() to panic.

Take a single write lock for the whole block instead of the read-then-write pattern.

workdir_fd,
&wd_name,
libc::O_CREAT | libc::O_WRONLY,
mode | 0o200,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File is created with mode | 0o200 to allow writing during copy, but fchmod is never called afterward to restore the original mode. A 0o444 source file ends up 0o644 after copy-up.

Add fchmod(dfd, mode) after copy_data completes.

src/sys/dir.rs Outdated
// SAFETY: d_name is a null-terminated C string in the dirent.
let name = unsafe {
std::ffi::CStr::from_ptr(entry.d_name.as_ptr())
.to_string_lossy()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to_string_lossy() replaces non-UTF-8 bytes with U+FFFD, making files with non-UTF-8 names (which are valid on Linux) inaccessible or silently mapped to the wrong name. This is a regression from the C version, which handles raw bytes.

Consider using OsString / CString throughout instead of String.

}

for m in mappings {
if direct {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m.host + m.len can overflow u32 when the mapping covers the top of the ID range. Use id >= m.host && id - m.host < m.len instead.

Same issue on line 90 with m.to + m.len.

src/overlay.rs Outdated
// root_id is always valid; if not, we have a fatal invariant violation
self.nodes
.get(&self.root_id)
.expect("root node missing from arena")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expect() will panic and crash the FUSE daemon if this invariant is ever violated. This goes against the project's own "no panic" rule in AGENTS.md. Consider returning a Result or using fuse_err!.

src/main.rs Outdated
//
// SPDX-License-Identifier: GPL-2.0-or-later

#![allow(dead_code)]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#![allow(dead_code)] at crate level suppresses warnings for all unused code, hiding real dead code (e.g. the get_uid/get_gid functions flagged in the previous review). Consider removing this and cleaning up the actual dead code.

Cargo.toml Outdated
description = "Overlay Filesystem in Userspace"

[dependencies]
fuser = { version = "0.17", features = ["abi-7-40"] }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider pinning dependencies to their exact patch versions (e.g. fuser = "0.17.1", nix = "0.29.1", etc.). For a filesystem, a semver-compatible but untested patch bump could introduce subtle behavioral changes or regressions. Pinning to known-good patch versions, combined with the Cargo.lock, gives more control over when updates are adopted.

@saschagrunert
Copy link
Copy Markdown
Member

This is a good chance to enhance the docs for this project, too.

@giuseppe giuseppe force-pushed the rust-rewrite branch 4 times, most recently from 2d2bb1d to 189db78 Compare March 26, 2026 13:48
Copy link
Copy Markdown
Member

@saschagrunert saschagrunert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second round of review. Previous comments were mostly addressed; these are the remaining and new findings.

Cargo.toml Outdated
[package]
name = "fuse-overlayfs"
version = "2.0.0"
edition = "2021"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider updating to edition = "2024". The 2024 edition enables unsafe_op_in_unsafe_fn by default, which would add an extra safety layer in the src/sys/ modules where most of the unsafe code lives. It also enables other useful lints like rust_2024_compatibility.

src/sys/dir.rs Outdated
let name = unsafe {
std::ffi::CStr::from_ptr(entry.d_name.as_ptr())
.to_string_lossy()
.into_owned()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This to_string_lossy() silently mangles non-UTF-8 filenames, which is a regression from the C version that handled raw bytes. This is a systemic issue: RawDirEntry.name, OvlNode.name, and the children map keys in node.rs all use String. They should use OsString (or CString/Vec<u8>) to preserve arbitrary POSIX filenames.

const RESOLVE_IN_ROOT: u64 = 0x10;

#[cfg(target_arch = "x86_64")]
const SYS_OPENAT2: libc::c_long = 437;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These per-arch #[cfg] blocks all define SYS_OPENAT2 = 437 (same value on every architecture). A single const SYS_OPENAT2: libc::c_long = 437; would suffice, or better yet use libc::SYS_openat2 which the libc crate already exports. The current approach silently fails to compile on unlisted architectures (e.g. loongarch64) with no error message.

src/config.rs Outdated
pub timeout: f64,
pub threaded: i32,
pub fsync: i32,
pub fast_ino_check: i32,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several fields here use i32 where more precise types would be appropriate:

  • fsync, writeback, disable_xattrs, fast_ino_check, threaded are used as booleans (!= 0) but typed as i32.
  • squash_to_uid / squash_to_gid use -1 as a sentinel for "not set" instead of Option<u32>.

Using bool and Option<u32> respectively would make the intent clearer and prevent bugs from arithmetic on what are logically boolean/optional values.


- name: Build
run: cargo build --release

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no cargo clippy or cargo fmt -- --check step, despite AGENTS.md requiring zero warnings and formatted code. Adding these as CI steps would enforce those requirements.


- name: Install cross
run: cargo install cross --git https://github.com/cross-rs/cross

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cargo install cross --git https://github.com/cross-rs/cross installs from HEAD, which is not reproducible and could break without warning. Pin to a specific tag or revision, e.g. cargo install cross --git https://github.com/cross-rs/cross --tag v0.2.5.

@giuseppe
Copy link
Copy Markdown
Member Author

@saschagrunert thanks for the review! I will ping you once I've addressed all the comments (I am pushing just to trigger the CI)

@giuseppe giuseppe force-pushed the rust-rewrite branch 6 times, most recently from e45068c to b592a54 Compare March 28, 2026 11:26
giuseppe and others added 5 commits March 28, 2026 16:28
Add comprehensive end-to-end tests covering copyup, directory
operations, hardlinks, readonly mode, rename (including
RENAME_NOREPLACE and RENAME_EXCHANGE), special files, symlinks,
and xattr handling. These tests validate the behavior of
fuse-overlayfs and serve as a regression suite.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
mknod for character/block devices requires CAP_MKNOD in the initial
user namespace. Skip those test cases gracefully when mknod is not
available, allowing the full test suite to pass rootless.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
@giuseppe giuseppe force-pushed the rust-rewrite branch 3 times, most recently from d217063 to 8db4c06 Compare March 28, 2026 16:42
@giuseppe
Copy link
Copy Markdown
Member Author

comments addressed!

giuseppe and others added 3 commits April 3, 2026 11:45
Complete reimplementation of fuse-overlayfs in Rust as a drop-in
replacement.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
Add a release job that collects all cross-compiled binaries (x86_64,
aarch64, armv7l, s390x, ppc64le, riscv64), generates SHA256SUMS, and
creates a draft GitHub release when a version tag is pushed. Matches
the existing C release workflow structure.

Also rename armv7 artifact to armv7l for consistency with the C release.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
@giuseppe giuseppe changed the title [RFC][WIP] Rewrite fuse-overlayfs in Rust Rewrite fuse-overlayfs in Rust Apr 3, 2026
@giuseppe
Copy link
Copy Markdown
Member Author

giuseppe commented Apr 7, 2026

@mheon FYI

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants