diff --git a/NEWS b/NEWS index 502bf31..31d17f6 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,9 @@ +* fuse-overlayfs-1.17 + +- main: clear SUID/SGID bits on truncate (CVE-2026-52791). +- main: make sure st_ino needs is unique for all inodes on a device. +- man: uniformly apply bold attributes. + * fuse-overlayfs-1.16 - main: fix incorrect directory entries due to unstable iteration diff --git a/src/overlay.rs b/src/overlay.rs index 97c6e66..c3ea971 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -78,6 +78,16 @@ fn empty_upper_dir(upper: &OvlLayer, path: &[u8]) -> FsResult<()> { Ok(()) } +/// Clear SUID/SGID bits on a file descriptor after a content-modifying operation. +fn clear_suid_sgid(fd: RawFd) -> FsResult<()> { + let st = crate::sys::fs::fstat(fd)?; + if st.st_mode & (libc::S_ISUID | libc::S_ISGID) != 0 { + let new_mode = st.st_mode & !(libc::S_ISUID | libc::S_ISGID); + crate::sys::fs::fchmod(fd, new_mode)?; + } + Ok(()) +} + /// The main overlay filesystem state. pub struct OverlayFs { config: OverlayConfig, @@ -1861,6 +1871,12 @@ impl Filesystem for OverlayFs { reply.error(Errno::from_i32(e.0)); return; } + if fd >= 0 { + if let Err(e) = clear_suid_sgid(fd) { + reply.error(Errno::from_i32(e.0)); + return; + } + } } match inner.do_getattr(node_id, &self.config) { @@ -2640,6 +2656,11 @@ impl Filesystem for OverlayFs { match layer.ds.openat(&path, open_flags, 0o700) { Ok(safe_fd) => { let owned_fd = safe_fd.into_owned(); + if (flags_raw & libc::O_TRUNC) != 0 { + if let Some(notifier) = self.notifier.get() { + let _ = notifier.inval_inode(INodeNo(ino), -1, 0); + } + } drop(inner); self.reply_open_maybe_passthrough(ino, owned_fd, reply); return; @@ -2678,6 +2699,11 @@ impl Filesystem for OverlayFs { match layer.ds.openat(&path, open_flags, 0o700) { Ok(safe_fd) => { let owned_fd = safe_fd.into_owned(); + if (flags_raw & libc::O_TRUNC) != 0 { + if let Some(notifier) = self.notifier.get() { + let _ = notifier.inval_inode(INodeNo(ino), -1, 0); + } + } drop(inner); self.reply_open_maybe_passthrough(ino, owned_fd, reply); } @@ -2763,6 +2789,8 @@ impl Filesystem for OverlayFs { Ok(n) => { if let Some(mode) = restore_mode { let _ = crate::sys::fs::fchmod(raw_fd, mode); + } else { + let _ = clear_suid_sgid(raw_fd); } reply.written(n as u32); } @@ -3544,7 +3572,10 @@ impl Filesystem for OverlayFs { match crate::sys::fs::fallocate(fd.as_raw_fd(), mode, offset as i64, length as i64) { Err(e) => reply.error(Errno::from_i32(e.0)), - Ok(()) => reply.ok(), + Ok(()) => { + let _ = clear_suid_sgid(fd.as_raw_fd()); + reply.ok(); + } } } } @@ -3614,7 +3645,10 @@ impl Filesystem for OverlayFs { capped_len, ) { Err(e) => reply.error(Errno::from_i32(e.0)), - Ok(n) => reply.written(std::cmp::min(n, u32::MAX as usize) as u32), + Ok(n) => { + let _ = clear_suid_sgid(fd_out.as_raw_fd()); + reply.written(std::cmp::min(n, u32::MAX as usize) as u32); + } } } diff --git a/tests/fedora-installs.sh b/tests/fedora-installs.sh index 5dbca96..79959cb 100755 --- a/tests/fedora-installs.sh +++ b/tests/fedora-installs.sh @@ -19,7 +19,7 @@ mkdir lower:1 upper:2 workdir:3 merged fuse-overlayfs -o 'sync=0,lowerdir=lower\\:1,upperdir=upper\\:2,workdir=workdir\\:3,suid,dev' merged -$CONTAINER_RUNTIME run --rm -v $(pwd)/merged:/merged fedora dnf --use-host-config --installroot /merged --releasever 41 install -y glibc-common gedit +$CONTAINER_RUNTIME run --rm -v $(pwd)/merged:/merged quay.io/fedora/fedora dnf --use-host-config --installroot /merged --releasever 44 install -y glibc-common gedit umount merged @@ -30,6 +30,11 @@ mkdir upper workdir gcc -static -o suid-test $(dirname $0)/suid-test.c +printf "truncate through open\n" > lower/killpriv-open-trunc +printf "truncate through setattr\n" > lower/killpriv-setattr-trunc +printf "clear on write\n" > lower/killpriv-write +chmod 6777 lower/killpriv-open-trunc lower/killpriv-setattr-trunc lower/killpriv-write + fuse-overlayfs -o sync=0,threaded=1,lowerdir=lower,upperdir=upper,workdir=workdir,suid,dev merged SUID_TEST=$(pwd)/suid-test (cd merged; $SUID_TEST) @@ -42,15 +47,30 @@ test $(stat -c %h merged/usr) -gt 2 stat -c %A upper/suid | grep s stat -c %a upper/nosuid | grep -v s +# Test SUID/SGID clearing on truncate (CVE-2026-52791) +: > merged/killpriv-open-trunc +truncate -s 0 merged/killpriv-setattr-trunc +test $(stat -c %s merged/killpriv-open-trunc) -eq 0 +test $(stat -c %s merged/killpriv-setattr-trunc) -eq 0 +test $(stat -c %a upper/killpriv-open-trunc) = 777 +test $(stat -c %a upper/killpriv-setattr-trunc) = 777 +test $(stat -c %a merged/killpriv-open-trunc) = 777 +test $(stat -c %a merged/killpriv-setattr-trunc) = 777 + +# Test SUID/SGID clearing on write +dd if=/dev/zero of=merged/killpriv-write bs=1 count=1 conv=notrunc 2>/dev/null +test $(stat -c %a upper/killpriv-write) = 777 +test $(stat -c %a merged/killpriv-write) = 777 + # Install some big packages -$CONTAINER_RUNTIME run --rm -v $(pwd)/merged:/merged fedora dnf --use-host-config --installroot /merged --releasever 41 install -y emacs texlive +$CONTAINER_RUNTIME run --rm -v $(pwd)/merged:/merged quay.io/fedora/fedora dnf --use-host-config --installroot /merged --releasever 44 install -y emacs texlive -$CONTAINER_RUNTIME run --rm -v $(pwd)/merged:/merged fedora sh -c 'rm /merged/usr/share/glib-2.0/schemas/gschemas.compiled; glib-compile-schemas /merged/usr/share/glib-2.0/schemas/' +$CONTAINER_RUNTIME run --rm -v $(pwd)/merged:/merged quay.io/fedora/fedora sh -c 'rm /merged/usr/share/glib-2.0/schemas/gschemas.compiled; glib-compile-schemas /merged/usr/share/glib-2.0/schemas/' umount merged fuse-overlayfs -o sync=0,lowerdir=lower,upperdir=upper,workdir=workdir,suid,dev merged -$CONTAINER_RUNTIME run --rm -v $(pwd)/merged:/merged fedora sh -c 'rm -rf /merged/usr/share/glib-2.0/' +$CONTAINER_RUNTIME run --rm -v $(pwd)/merged:/merged quay.io/fedora/fedora sh -c 'rm -rf /merged/usr/share/glib-2.0/' tar -c --to-stdout $(pwd)/merged > /dev/null @@ -61,7 +81,7 @@ mkdir upper workdir lower # fast_ino_check fuse-overlayfs -o fast_ino_check=1,sync=0,lowerdir=lower,upperdir=upper,workdir=workdir,suid,dev merged -$CONTAINER_RUNTIME run --rm -v $(pwd)/merged:/merged fedora dnf --use-host-config --installroot /merged --releasever 41 install -y glibc-common gedit +$CONTAINER_RUNTIME run --rm -v $(pwd)/merged:/merged quay.io/fedora/fedora dnf --use-host-config --installroot /merged --releasever 44 install -y glibc-common gedit mkdir merged/a-directory