From 2ed990013467ffa7e9fc8d766892b700a68ce76f Mon Sep 17 00:00:00 2001 From: thomasjm Date: Sat, 14 Nov 2026 05:43:56 -0800 Subject: [PATCH 1/6] Fix broken doc links and check docs build without warnings --- flake.nix | 7 ++++++- src/ninep.rs | 9 +++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/flake.nix b/flake.nix index 5528b0e..3740fac 100644 --- a/flake.nix +++ b/flake.nix @@ -46,13 +46,18 @@ packages.default = p9fuse; packages.p9fuse = p9fuse; - # `nix flake check` runs all of these: build, clippy (-D warnings), and rustfmt. + # `nix flake check` runs all of these: build, clippy (-D warnings), docs (-D warnings), and + # rustfmt. checks = { inherit p9fuse; clippy = craneLib.cargoClippy (commonArgs // { inherit cargoArtifacts; cargoClippyExtraArgs = "--all-targets -- -D warnings"; }); + doc = craneLib.cargoDoc (commonArgs // { + inherit cargoArtifacts; + RUSTDOCFLAGS = "-D warnings"; + }); fmt = craneLib.cargoFmt { inherit (commonArgs) src; }; }; diff --git a/src/ninep.rs b/src/ninep.rs index 79e65ec..9d1bbbf 100644 --- a/src/ninep.rs +++ b/src/ninep.rs @@ -212,9 +212,9 @@ impl<'a> R<'a> { } } -/// Parse an Rgetattr body. Layout: valid[8] qid[13] mode[4] uid[4] gid[4] nlink[8] rdev[8] size[8] -/// blksize[8] blocks[8] atime(sec,nsec) mtime(sec,nsec) ctime(sec,nsec) btime(sec,nsec) gen[8] -/// data_version[8]. We stop after ctime since that's all FUSE consumes. +/// Parse an Rgetattr body. Layout: `valid[8] qid[13] mode[4] uid[4] gid[4] nlink[8] rdev[8] +/// size[8] blksize[8] blocks[8] atime(sec,nsec) mtime(sec,nsec) ctime(sec,nsec) btime(sec,nsec) +/// gen[8] data_version[8]`. We stop after ctime since that's all FUSE consumes. pub fn parse_getattr(body: &[u8]) -> Option { let mut r = R::new(body); let _valid = r.u64()?; @@ -246,7 +246,8 @@ pub fn parse_getattr(body: &[u8]) -> Option { }) } -/// Parse the packed Rreaddir data blob into entries. Each entry: qid[13] offset[8] type[1] name[s]. +/// Parse the packed Rreaddir data blob into entries. Each entry: +/// `qid[13] offset[8] type[1] name[s]`. pub fn parse_readdir(data: &[u8]) -> Vec { let mut r = R::new(data); let mut out = Vec::new(); From bb485e167e8e10a6b2a52694512d319aa0ceed9e Mon Sep 17 00:00:00 2001 From: thomasjm Date: Sat, 14 Nov 2026 05:45:11 -0800 Subject: [PATCH 2/6] Turn on the rust_2018_idioms and unused_qualifications lints --- Cargo.toml | 5 ++++ src/fuse9p/filesystem.rs | 60 +++++++++++++++++++++++++--------------- src/transport.rs | 2 +- tests/diod_fuse.rs | 15 ++++------ 4 files changed, 48 insertions(+), 34 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d0f58de..54acb50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,3 +42,8 @@ tempfile = "3" [profile.release] lto = true codegen-units = 1 + +# CI runs clippy with -D warnings, so everything here set to "warn" fails CI. +[lints.rust] +rust_2018_idioms = { level = "warn", priority = -1 } +unused_qualifications = "warn" diff --git a/src/fuse9p/filesystem.rs b/src/fuse9p/filesystem.rs index a3a9967..434a7b4 100644 --- a/src/fuse9p/filesystem.rs +++ b/src/fuse9p/filesystem.rs @@ -26,7 +26,7 @@ const FUSE_WRITEBACK_CACHE: u32 = 1 << 16; const READDIR_BYTES: u32 = 8192; impl Filesystem for Fuse9p { - fn init(&mut self, _req: &Request, config: &mut KernelConfig) -> Result<(), libc::c_int> { + fn init(&mut self, _req: &Request<'_>, config: &mut KernelConfig) -> Result<(), libc::c_int> { // Let reads and writes be as large as a 9p message allows, rather than 128 KiB. let max = self.client.msize.saturating_sub(64).max(8192); let _ = config.set_max_write(max); @@ -40,7 +40,7 @@ impl Filesystem for Fuse9p { Ok(()) } - fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) { + fn lookup(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) { let parent_fid = or_reply!(reply, self.fid_of(parent)); let name = name.to_string_lossy(); let found = self.walk(parent_fid, &[&name]).and_then(|(fid, qids)| { @@ -60,7 +60,7 @@ impl Filesystem for Fuse9p { } } - fn forget(&mut self, _req: &Request, ino: u64, nlookup: u64) { + fn forget(&mut self, _req: &Request<'_>, ino: u64, nlookup: u64) { if ino == ROOT_INO { return; } @@ -74,7 +74,7 @@ impl Filesystem for Fuse9p { } } - fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) { + fn getattr(&mut self, _req: &Request<'_>, ino: u64, reply: ReplyAttr) { let fid = or_reply!(reply, self.fid_of(ino)); self.settle_writes(Some(ino)); let attr = or_reply!(reply, self.rt.block_on(self.client.getattr(fid))); @@ -83,7 +83,7 @@ impl Filesystem for Fuse9p { fn setattr( &mut self, - _req: &Request, + _req: &Request<'_>, ino: u64, mode: Option, uid: Option, @@ -135,7 +135,7 @@ impl Filesystem for Fuse9p { reply.attr(&self.tuning.attr_ttl, &to_fileattr(ino, &attr)); } - fn open(&mut self, _req: &Request, ino: u64, flags: i32, reply: ReplyOpen) { + fn open(&mut self, _req: &Request<'_>, ino: u64, flags: i32, reply: ReplyOpen) { let base = or_reply!(reply, self.fid_of(ino)); let (oflags, pipelined) = self.open_flags(flags); if oflags & libc::O_TRUNC as u32 != 0 { @@ -149,7 +149,7 @@ impl Filesystem for Fuse9p { fn read( &mut self, - _req: &Request, + _req: &Request<'_>, ino: u64, fh: u64, offset: i64, @@ -168,7 +168,7 @@ impl Filesystem for Fuse9p { fn write( &mut self, - _req: &Request, + _req: &Request<'_>, _ino: u64, fh: u64, offset: i64, @@ -201,7 +201,7 @@ impl Filesystem for Fuse9p { reply.written(data.len() as u32); } - fn flush(&mut self, _req: &Request, _ino: u64, fh: u64, _lock: u64, reply: ReplyEmpty) { + fn flush(&mut self, _req: &Request<'_>, _ino: u64, fh: u64, _lock: u64, reply: ReplyEmpty) { // Called on every close(2), which is where a write error should show up. match self.handles.get(&fh).and_then(|h| self.take_write_error(h)) { Some(e) => reply.error(e), @@ -209,7 +209,14 @@ impl Filesystem for Fuse9p { } } - fn fsync(&mut self, _req: &Request, _ino: u64, fh: u64, _datasync: bool, reply: ReplyEmpty) { + fn fsync( + &mut self, + _req: &Request<'_>, + _ino: u64, + fh: u64, + _datasync: bool, + reply: ReplyEmpty, + ) { let Ok(h) = self.handle(fh) else { return reply.ok(); }; @@ -223,7 +230,7 @@ impl Filesystem for Fuse9p { fn release( &mut self, - _req: &Request, + _req: &Request<'_>, _ino: u64, fh: u64, _flags: i32, @@ -239,7 +246,7 @@ impl Filesystem for Fuse9p { fn create( &mut self, - req: &Request, + req: &Request<'_>, parent: u64, name: &OsStr, mode: u32, @@ -269,7 +276,7 @@ impl Filesystem for Fuse9p { reply.created(&self.tuning.entry_ttl, &attr, 0, fh, 0); } - fn opendir(&mut self, _req: &Request, ino: u64, _flags: i32, reply: ReplyOpen) { + fn opendir(&mut self, _req: &Request<'_>, ino: u64, _flags: i32, reply: ReplyOpen) { let base = or_reply!(reply, self.fid_of(ino)); let fid = or_reply!(reply, self.open_fid(base, libc::O_RDONLY as u32)); let fh = self.insert_handle(fid, ino, false); @@ -278,7 +285,7 @@ impl Filesystem for Fuse9p { fn readdir( &mut self, - _req: &Request, + _req: &Request<'_>, _ino: u64, fh: u64, offset: i64, @@ -303,7 +310,7 @@ impl Filesystem for Fuse9p { /// `readdir` that also returns each entry's attributes. Only used with `Tuning::readdirplus`. fn readdirplus( &mut self, - _req: &Request, + _req: &Request<'_>, ino: u64, fh: u64, offset: i64, @@ -340,14 +347,21 @@ impl Filesystem for Fuse9p { reply.ok(); } - fn releasedir(&mut self, _req: &Request, _ino: u64, fh: u64, _flags: i32, reply: ReplyEmpty) { + fn releasedir( + &mut self, + _req: &Request<'_>, + _ino: u64, + fh: u64, + _flags: i32, + reply: ReplyEmpty, + ) { self.close_handle(fh); reply.ok(); } fn mkdir( &mut self, - req: &Request, + req: &Request<'_>, parent: u64, name: &OsStr, mode: u32, @@ -365,7 +379,7 @@ impl Filesystem for Fuse9p { fn symlink( &mut self, - req: &Request, + req: &Request<'_>, parent: u64, name: &OsStr, target: &Path, @@ -379,23 +393,23 @@ impl Filesystem for Fuse9p { self.reply_new_entry(parent_fid, &name, reply); } - fn readlink(&mut self, _req: &Request, ino: u64, reply: ReplyData) { + fn readlink(&mut self, _req: &Request<'_>, ino: u64, reply: ReplyData) { let fid = or_reply!(reply, self.fid_of(ino)); let target = or_reply!(reply, self.rt.block_on(self.client.readlink(fid))); reply.data(target.as_bytes()); } - fn unlink(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEmpty) { + fn unlink(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) { self.remove(parent, name, 0, reply); } - fn rmdir(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEmpty) { + fn rmdir(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) { self.remove(parent, name, libc::AT_REMOVEDIR as u32, reply); } fn rename( &mut self, - _req: &Request, + _req: &Request<'_>, parent: u64, name: &OsStr, newparent: u64, @@ -424,7 +438,7 @@ impl Filesystem for Fuse9p { reply.ok(); } - fn statfs(&mut self, _req: &Request, ino: u64, reply: ReplyStatfs) { + fn statfs(&mut self, _req: &Request<'_>, ino: u64, reply: ReplyStatfs) { let fid = self.fid_of(ino).unwrap_or(self.client.root_fid); let stats = or_reply!(reply, self.rt.block_on(self.client.statfs(fid))); let (bsize, blocks, bfree, bavail, files, ffree, namelen) = stats; diff --git a/src/transport.rs b/src/transport.rs index 24d0f7d..e882e24 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -97,7 +97,7 @@ impl FdTransport { } let mut domain: libc::c_int = 0; - let mut len = std::mem::size_of::() as libc::socklen_t; + let mut len = size_of::() as libc::socklen_t; let rc = libc::getsockopt( fd, libc::SOL_SOCKET, diff --git a/tests/diod_fuse.rs b/tests/diod_fuse.rs index fdcdc03..8013ab8 100644 --- a/tests/diod_fuse.rs +++ b/tests/diod_fuse.rs @@ -349,7 +349,7 @@ where /// [`with_mount_via`], but the body also gets the export directory and the invalidation input. async fn with_mount_full(via: Via, tuning: Tuning, body: F) where - F: FnOnce(Mounted) + Send + 'static, + F: FnOnce(Mounted<'_>) + Send + 'static, { let diod = Diod::start(); // Seed a readiness sentinel in the export; it appears through the mount once FUSE is live. @@ -615,15 +615,10 @@ async fn client_rename_then_walk() { let gid = unsafe { libc::getegid() }; let transport = Box::new(TcpTransport::connect(&addr).await.unwrap()); - let (client, _root) = NineClient::connect( - transport, - 512_000, - uid, - &aname, - std::time::Duration::from_secs(0), - ) - .await - .unwrap(); + let (client, _root) = + NineClient::connect(transport, 512_000, uid, &aname, Duration::from_secs(0)) + .await + .unwrap(); let root = client.root_fid; // Create "a.txt" (lcreate opens `dfid` on the new file, so clone root into a scratch fid first). From a3c5ea930ff5f98b447b4acb65219ef5ab1924f7 Mon Sep 17 00:00:00 2001 From: thomasjm Date: Sat, 14 Nov 2026 05:49:26 -0800 Subject: [PATCH 3/6] Check integer casts that could truncate or change sign --- Cargo.toml | 6 ++++++ src/client.rs | 12 +++++++----- src/fuse9p/attr.rs | 10 +++++++--- src/fuse9p/filesystem.rs | 37 +++++++++++++++++++++++++------------ src/fuse9p/invalidate.rs | 7 +++++++ src/fuse9p/writes.rs | 2 +- src/main.rs | 4 ++-- src/mount9p.rs | 2 +- src/ninep.rs | 8 +++++++- src/transport.rs | 2 +- tests/diod_fuse.rs | 3 +++ 11 files changed, 67 insertions(+), 26 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 54acb50..1342a20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,3 +47,9 @@ codegen-units = 1 [lints.rust] rust_2018_idioms = { level = "warn", priority = -1 } unused_qualifications = "warn" + +[lints.clippy] +# Silent integer truncation is the easiest way for 9p message encoding to go wrong. +cast_possible_truncation = "warn" +cast_sign_loss = "warn" +cast_possible_wrap = "warn" diff --git a/src/client.rs b/src/client.rs index 29afcad..05b6155 100644 --- a/src/client.rs +++ b/src/client.rs @@ -214,6 +214,7 @@ impl NineClient { /// Send a T-message body and await the matching response. Returns Err(errno) on Rlerror or a /// transport/protocol failure (mapped to EIO). async fn transact(&self, mtype: u8, tag: u16, body: &[u8]) -> Result { + let size = u32::try_from(4 + 1 + 2 + body.len()).map_err(|_| libc::EMSGSIZE)?; let (tx, rx) = oneshot::channel(); let displaced = self.pending.lock().unwrap().insert( tag, @@ -236,7 +237,6 @@ impl NineClient { ); } - let size = (4 + 1 + 2 + body.len()) as u32; let mut frame = Vec::with_capacity(size as usize); frame.extend_from_slice(&size.to_le_bytes()); frame.push(mtype); @@ -254,8 +254,8 @@ impl NineClient { match rx.await { Ok(resp) => { if resp.typ == RLERROR { - let ecode = R::new(&resp.body).u32().unwrap_or(libc::EIO as u32); - Err(ecode as i32) + let ecode = R::new(&resp.body).u32().and_then(|e| i32::try_from(e).ok()); + Err(ecode.unwrap_or(libc::EIO)) } else { Ok(resp) } @@ -330,7 +330,8 @@ impl NineClient { /// that short reply is mapped to ENOENT here to keep the rule "Err means no fid" exact. pub async fn walk(&self, fid: u32, newfid: u32, names: &[&str]) -> Result, i32> { let mut w = W::new(); - w.u32(fid).u32(newfid).u16(names.len() as u16); + let count = u16::try_from(names.len()).map_err(|_| libc::EINVAL)?; + w.u32(fid).u32(newfid).u16(count); for n in names { w.str(n); } @@ -437,7 +438,8 @@ impl NineClient { pub async fn write(&self, fid: u32, offset: u64, data: &[u8]) -> Result { let mut w = W::new(); - w.u32(fid).u64(offset).u32(data.len() as u32).bytes(data); + let count = u32::try_from(data.len()).map_err(|_| libc::EINVAL)?; + w.u32(fid).u64(offset).u32(count).bytes(data); let body = self.req(TWRITE, RWRITE, &w.buf).await?; R::new(&body).u32().ok_or(libc::EIO) } diff --git a/src/fuse9p/attr.rs b/src/fuse9p/attr.rs index 5802a83..a7a2ce0 100644 --- a/src/fuse9p/attr.rs +++ b/src/fuse9p/attr.rs @@ -15,11 +15,14 @@ pub(super) fn to_fileattr(ino: u64, a: &Attr) -> FileAttr { crtime: UNIX_EPOCH, kind: kind_from_mode(a.mode), perm: (a.mode & 0o7777) as u16, - nlink: a.nlink.max(1) as u32, + nlink: u32::try_from(a.nlink.max(1)).unwrap_or(u32::MAX), uid: a.uid, gid: a.gid, + // The low 32 bits of a glibc dev_t are the kernel's encoding for devices with major + // numbers below 4096, which covers real devices. + #[allow(clippy::cast_possible_truncation)] rdev: a.rdev as u32, - blksize: a.blksize.max(512) as u32, + blksize: u32::try_from(a.blksize.max(512)).unwrap_or(u32::MAX), flags: 0, } } @@ -67,7 +70,8 @@ pub(super) fn time_or_now(t: TimeOrNow) -> (u64, u64) { } fn unixtime((sec, nsec): (u64, u64)) -> SystemTime { - UNIX_EPOCH + Duration::new(sec, (nsec as u32).min(999_999_999)) + let nsec = u32::try_from(nsec.min(999_999_999)).unwrap_or(0); + UNIX_EPOCH + Duration::new(sec, nsec) } fn kind_from_mode(mode: u32) -> FileType { diff --git a/src/fuse9p/filesystem.rs b/src/fuse9p/filesystem.rs index 434a7b4..9d72f15 100644 --- a/src/fuse9p/filesystem.rs +++ b/src/fuse9p/filesystem.rs @@ -160,8 +160,9 @@ impl Filesystem for Fuse9p { ) { let fid = or_reply!(reply, self.handle(fh)).fid; self.settle_writes(Some(ino)); + let offset = or_reply!(reply, file_offset(offset)); let size = size.min(self.io_chunk()); - let data = read_full(&self.client, fid, offset as u64, size); + let data = read_full(&self.client, fid, offset, size); let data = or_reply!(reply, self.rt.block_on(data)); reply.data(&data); } @@ -178,13 +179,15 @@ impl Filesystem for Fuse9p { _lock: Option, reply: ReplyWrite, ) { + let offset = or_reply!(reply, file_offset(offset)); + let len = or_reply!(reply, u32::try_from(data.len()).map_err(|_| libc::EINVAL)); let h = or_reply!(reply, self.handle(fh)); let (fid, ino, err) = (h.fid, h.ino, h.write_error.clone()); match (err, self.writes.get(&ino).cloned()) { (Some(err), Some(pipeline)) => { let w = Write { fid, - off: offset as u64, + off: offset, data: data.to_vec(), err, }; @@ -194,11 +197,11 @@ impl Filesystem for Fuse9p { // After the pipelined writes to the same file. self.settle_writes(Some(ino)); let chunk = self.io_chunk() as usize; - let written = write_all(&self.client, fid, offset as u64, data, chunk); + let written = write_all(&self.client, fid, offset, data, chunk); or_reply!(reply, self.rt.block_on(written)); } } - reply.written(data.len() as u32); + reply.written(len); } fn flush(&mut self, _req: &Request<'_>, _ino: u64, fh: u64, _lock: u64, reply: ReplyEmpty) { @@ -292,12 +295,14 @@ impl Filesystem for Fuse9p { mut reply: ReplyDirectory, ) { let fid = or_reply!(reply, self.handle(fh)).fid; - let entries = self.client.readdir(fid, offset as u64, READDIR_BYTES); + let entries = self + .client + .readdir(fid, offset.cast_unsigned(), READDIR_BYTES); for e in or_reply!(reply, self.rt.block_on(entries)) { // An entry's offset is where to continue after it. `add` is true once the buffer is full. if reply.add( self.ino(&e.qid), - e.offset as i64, + e.offset.cast_signed(), kind_from_qid(e.qid.typ), &e.name, ) { @@ -318,7 +323,9 @@ impl Filesystem for Fuse9p { ) { let open_fid = or_reply!(reply, self.handle(fh)).fid; let dir_fid = or_reply!(reply, self.fid_of(ino)); - let entries = self.client.readdir(open_fid, offset as u64, READDIR_BYTES); + let entries = self + .client + .readdir(open_fid, offset.cast_unsigned(), READDIR_BYTES); let entries = or_reply!(reply, self.rt.block_on(entries)); // The sizes must include writes we have acknowledged but not sent yet. self.settle_writes(None); @@ -337,7 +344,7 @@ impl Filesystem for Fuse9p { let ttl = self.tuning.entry_ttl; // `add` is true once the buffer is full; the kernel only takes a lookup reference to // entries that fit. - full = full || reply.add(ino, e.offset as i64, &e.name, &ttl, &attr, 0); + full = full || reply.add(ino, e.offset.cast_signed(), &e.name, &ttl, &attr, 0); if full { self.clunk(fid); } else { @@ -490,14 +497,20 @@ async fn walk_attr(client: &NineClient, dir: u32, name: &str) -> Option<(u32, At /// file to the application (SQLite then fails with "disk I/O error"), so keep reading. async fn read_full(client: &NineClient, fid: u32, off: u64, size: u32) -> Result, i32> { let mut out = Vec::new(); - while (out.len() as u32) < size { - let chunk = client - .read(fid, off + out.len() as u64, size - out.len() as u32) - .await?; + let mut remaining = size; + while remaining > 0 { + let chunk = client.read(fid, off + out.len() as u64, remaining).await?; if chunk.is_empty() { break; } + let n = u32::try_from(chunk.len()).map_err(|_| libc::EIO)?; + remaining = remaining.checked_sub(n).ok_or(libc::EIO)?; out.extend_from_slice(&chunk); } Ok(out) } + +/// A read or write offset from the kernel, which never sends a negative one. +fn file_offset(offset: i64) -> Result { + u64::try_from(offset).map_err(|_| libc::EINVAL) +} diff --git a/src/fuse9p/invalidate.rs b/src/fuse9p/invalidate.rs index 8d26033..ad509ce 100644 --- a/src/fuse9p/invalidate.rs +++ b/src/fuse9p/invalidate.rs @@ -11,6 +11,9 @@ use std::ffi::OsStr; use std::sync::Arc; use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader}; +/// The longest file name Linux allows. +const NAME_MAX: usize = 255; + pub(super) struct Invalidator { pub notifier: Notifier, pub client: Arc, @@ -67,6 +70,10 @@ impl Invalidator { let Some((name, parent)) = comps.split_last() else { return; }; + // No file has a longer name, and 9p can't send one over 65535 bytes. + if comps.iter().any(|c| c.len() > NAME_MAX) { + return; + } match self.resolve(parent).await { Ok(parent_ino) => { let name = name.to_string(); diff --git a/src/fuse9p/writes.rs b/src/fuse9p/writes.rs index 0d21d9f..9913b9f 100644 --- a/src/fuse9p/writes.rs +++ b/src/fuse9p/writes.rs @@ -8,7 +8,7 @@ impl Fuse9p { /// The flags to open a file's fid with, given the kernel's `flags`, and whether writes through /// it go through the write pipeline. pub(super) fn open_flags(&self, flags: i32) -> (u32, bool) { - let mut oflags = (flags as u32) & !(libc::O_CLOEXEC as u32); + let mut oflags = flags.cast_unsigned() & !(libc::O_CLOEXEC as u32); let access = oflags & libc::O_ACCMODE as u32; let append = oflags & libc::O_APPEND as u32 != 0; if self.tuning.writeback_cache { diff --git a/src/main.rs b/src/main.rs index 1f5c5fa..c8c5c95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,7 +68,7 @@ enum Cmd { headers: Vec, /// 9p msize (max message size) in bytes. #[arg(long, default_value_t = 512000)] - msize: usize, + msize: u32, /// Attach (n_uname) as this uid -- the user the server should act as for file ops. #[arg(long, default_value_t = 1000)] uid: u32, @@ -198,7 +198,7 @@ async fn main() -> Result<(), Box> { fuse9p::Fuse9p::run( transport, &mountpoint, - msize as u32, + msize, uid, &aname, tuning, diff --git a/src/mount9p.rs b/src/mount9p.rs index 4e0245e..42a1ef2 100644 --- a/src/mount9p.rs +++ b/src/mount9p.rs @@ -288,7 +288,7 @@ mod tests { /// Build a frame: size[4] type[1] tag[2] + body. fn frame(typ: u8, tag: u16, body: &[u8]) -> Vec { - let size = (7 + body.len()) as u32; + let size = u32::try_from(7 + body.len()).unwrap(); let mut f = size.to_le_bytes().to_vec(); f.push(typ); f.extend_from_slice(&tag.to_le_bytes()); diff --git a/src/ninep.rs b/src/ninep.rs index 9d1bbbf..a4799eb 100644 --- a/src/ninep.rs +++ b/src/ninep.rs @@ -151,8 +151,14 @@ impl W { self.buf.extend_from_slice(&v.to_le_bytes()); self } + /// Append a length-prefixed string. + /// + /// # Panics + /// + /// If `s` is longer than 9p's 65535-byte limit. Names and symlink targets are far shorter. pub fn str(&mut self, s: &str) -> &mut Self { - self.u16(s.len() as u16); + let len = u16::try_from(s.len()).expect("9p strings are at most 65535 bytes"); + self.u16(len); self.buf.extend_from_slice(s.as_bytes()); self } diff --git a/src/transport.rs b/src/transport.rs index e882e24..d4421f5 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -97,7 +97,7 @@ impl FdTransport { } let mut domain: libc::c_int = 0; - let mut len = size_of::() as libc::socklen_t; + let mut len = libc::socklen_t::try_from(size_of::()).unwrap(); let rc = libc::getsockopt( fd, libc::SOL_SOCKET, diff --git a/tests/diod_fuse.rs b/tests/diod_fuse.rs index 8013ab8..d82f7b0 100644 --- a/tests/diod_fuse.rs +++ b/tests/diod_fuse.rs @@ -10,6 +10,9 @@ //! `diod` is run rootless in TCP-listen mode with `-n` (no auth) and `-N` (no userdb), and the //! client attaches as the current uid, so `setfsuid` is a no-op and no privilege is required. +// Test data is made by truncating random numbers on purpose. +#![allow(clippy::cast_possible_truncation)] + use futures_util::{SinkExt, StreamExt}; use p9fuse::{ mount_with_invalidations, NineClient, NineTransport, TcpTransport, Tuning, UnixTransport, From 384932b65a3a0798aa68fa43d7b0df86c6b1badd Mon Sep 17 00:00:00 2001 From: thomasjm Date: Sat, 14 Nov 2026 05:53:00 -0800 Subject: [PATCH 4/6] Require a safety comment on unsafe code and remove what isn't needed --- Cargo.toml | 4 ++++ src/main.rs | 1 + src/mount9p.rs | 5 ++--- src/transport.rs | 52 +++++++++++++++++++++++++--------------------- tests/diod_fuse.rs | 8 ++++--- 5 files changed, 40 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1342a20..c2ae6b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,9 +47,13 @@ codegen-units = 1 [lints.rust] rust_2018_idioms = { level = "warn", priority = -1 } unused_qualifications = "warn" +unsafe_op_in_unsafe_fn = "warn" [lints.clippy] # Silent integer truncation is the easiest way for 9p message encoding to go wrong. cast_possible_truncation = "warn" cast_sign_loss = "warn" cast_possible_wrap = "warn" +undocumented_unsafe_blocks = "warn" +dbg_macro = "warn" +todo = "warn" diff --git a/src/main.rs b/src/main.rs index c8c5c95..2732474 100644 --- a/src/main.rs +++ b/src/main.rs @@ -275,6 +275,7 @@ async fn build_transport( } else if let Some(fd) = connect.strip_prefix("fd://") { // No retry: the fd is either a connected socket or it isn't -- there's nothing to wait for. let fd: std::os::unix::io::RawFd = fd.parse()?; + // SAFETY: whoever started us passed this fd for us to own; nothing else here uses it. Ok(Box::new(unsafe { FdTransport::from_raw_fd(fd)? })) } else { Err(format!( diff --git a/src/mount9p.rs b/src/mount9p.rs index 42a1ef2..51aac76 100644 --- a/src/mount9p.rs +++ b/src/mount9p.rs @@ -19,7 +19,7 @@ use nix::mount::{mount, umount2, MntFlags, MsFlags}; use nix::sys::socket::{socketpair, AddressFamily, SockFlag, SockType}; use std::collections::HashMap; use std::io; -use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd}; +use std::os::fd::AsRawFd; use std::path::Path; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; @@ -150,8 +150,7 @@ pub async fn mount9p( // 3. Turn the bridge end into a tokio stream. We must start pumping BEFORE mount(2) returns: // the kernel performs the 9p version/attach handshake synchronously inside mount(), so if the // bridge weren't already forwarding, mount() would block forever waiting for the server. - let bridge_std = - unsafe { std::os::unix::net::UnixStream::from_raw_fd(sock_bridge.into_raw_fd()) }; + let bridge_std = std::os::unix::net::UnixStream::from(sock_bridge); bridge_std.set_nonblocking(true)?; let bridge = tokio::net::UnixStream::from_std(bridge_std)?; let (mut bridge_rd, mut bridge_wr) = tokio::io::split(bridge); diff --git a/src/transport.rs b/src/transport.rs index d4421f5..dd042bf 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -85,40 +85,44 @@ impl FdTransport { /// # Safety /// `fd` must be a connected socket that nothing else owns or reads/writes concurrently. pub unsafe fn from_raw_fd(fd: std::os::unix::io::RawFd) -> io::Result { - use std::os::unix::io::FromRawFd; - - // tokio requires a nonblocking fd; an inherited one usually isn't. - let flags = libc::fcntl(fd, libc::F_GETFL); - if flags < 0 { - return Err(io::Error::last_os_error()); - } - if libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 { - return Err(io::Error::last_os_error()); - } + use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; + // SAFETY: the caller hands us `fd` to own. + let fd = unsafe { OwnedFd::from_raw_fd(fd) }; let mut domain: libc::c_int = 0; let mut len = libc::socklen_t::try_from(size_of::()).unwrap(); - let rc = libc::getsockopt( - fd, - libc::SOL_SOCKET, - libc::SO_DOMAIN, - &mut domain as *mut _ as *mut libc::c_void, - &mut len, - ); + // SAFETY: `fd` is open, and `domain` and `len` are live locals of the sizes passed. + let rc = unsafe { + libc::getsockopt( + fd.as_raw_fd(), + libc::SOL_SOCKET, + libc::SO_DOMAIN, + (&raw mut domain).cast::(), + &mut len, + ) + }; if rc < 0 { return Err(io::Error::last_os_error()); } + // tokio requires a nonblocking socket; an inherited one usually isn't. match domain { - libc::AF_UNIX => Ok(Self(FdInner::Unix(UnixStream::from_std( - std::os::unix::net::UnixStream::from_raw_fd(fd), - )?))), - libc::AF_INET | libc::AF_INET6 => Ok(Self(FdInner::Tcp(TcpStream::from_std( - std::net::TcpStream::from_raw_fd(fd), - )?))), + libc::AF_UNIX => { + let s = std::os::unix::net::UnixStream::from(fd); + s.set_nonblocking(true)?; + Ok(Self(FdInner::Unix(UnixStream::from_std(s)?))) + } + libc::AF_INET | libc::AF_INET6 => { + let s = std::net::TcpStream::from(fd); + s.set_nonblocking(true)?; + Ok(Self(FdInner::Tcp(TcpStream::from_std(s)?))) + } other => Err(io::Error::new( io::ErrorKind::InvalidInput, - format!("fd {fd} is a socket of unsupported domain {other}"), + format!( + "fd {} is a socket of unsupported domain {other}", + fd.as_raw_fd() + ), )), } } diff --git a/tests/diod_fuse.rs b/tests/diod_fuse.rs index d82f7b0..4a3c8ed 100644 --- a/tests/diod_fuse.rs +++ b/tests/diod_fuse.rs @@ -360,7 +360,7 @@ where let tcp_addr = format!("127.0.0.1:{}", diod.port); let aname = diod.export().to_string_lossy().into_owned(); - let uid = unsafe { libc::geteuid() }; + let uid = nix::unistd::geteuid().as_raw(); // Resolve the connect target, starting a relay to diod's TCP port for non-TCP transports. let reldir = tempfile::tempdir().unwrap(); @@ -614,8 +614,8 @@ async fn client_rename_then_walk() { let diod = Diod::start(); let addr = format!("127.0.0.1:{}", diod.port); let aname = diod.export().to_string_lossy().into_owned(); - let uid = unsafe { libc::geteuid() }; - let gid = unsafe { libc::getegid() }; + let uid = nix::unistd::geteuid().as_raw(); + let gid = nix::unistd::getegid().as_raw(); let transport = Box::new(TcpTransport::connect(&addr).await.unwrap()); let (client, _root) = @@ -824,7 +824,9 @@ async fn statfs_reports_capacity() { } with_mount(Tuning::default(), |mp| { let cpath = std::ffi::CString::new(mp.to_str().unwrap()).unwrap(); + // SAFETY: statvfs is a plain C struct, for which all zeroes is a valid value. let mut st: libc::statvfs = unsafe { std::mem::zeroed() }; + // SAFETY: `cpath` is NUL-terminated and `st` is a live statvfs. assert_eq!(unsafe { libc::statvfs(cpath.as_ptr(), &mut st) }, 0); assert!(st.f_bsize > 0, "block size should be non-zero"); assert!(st.f_blocks > 0, "total blocks should be non-zero"); From bc89d96e9f932f8563a4f109fa4cc1424a9e94d4 Mon Sep 17 00:00:00 2001 From: thomasjm Date: Sat, 14 Nov 2026 05:55:29 -0800 Subject: [PATCH 5/6] Update tokio and rustix past unsound-code advisories --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 390a996..709ae32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -583,9 +583,9 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rustix" -version = "1.1.4" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" dependencies = [ "bitflags", "errno", @@ -713,9 +713,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.52.3" +version = "1.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" dependencies = [ "bytes", "libc", From 974f0d055b7ad10edfa8a4e1f5f85ce5e5f6ea6b Mon Sep 17 00:00:00 2001 From: thomasjm Date: Sat, 14 Nov 2026 05:57:34 -0800 Subject: [PATCH 6/6] Check dependencies for advisories, licenses and sources --- deny.toml | 18 ++++++++++++++++++ flake.lock | 17 +++++++++++++++++ flake.nix | 22 +++++++++++++++++++--- 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 deny.toml diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..0888693 --- /dev/null +++ b/deny.toml @@ -0,0 +1,18 @@ +# cargo-deny settings. `nix flake check` runs the licenses, bans and sources checks; security +# advisories are checked by cargo-audit there, since cargo-deny can't download them in the sandbox. + +[licenses] +allow = ["MIT", "Apache-2.0", "Unicode-3.0"] + +[bans] +multiple-versions = "warn" +wildcards = "deny" + +[sources] +unknown-registry = "deny" +unknown-git = "deny" + +[advisories] +ignore = [ + { id = "RUSTSEC-2021-0154", reason = "Only affects fuser's libfuse code, which we build without (default-features = false)." }, +] diff --git a/flake.lock b/flake.lock index 07f47b2..73f593d 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,21 @@ { "nodes": { + "advisory-db": { + "flake": false, + "locked": { + "lastModified": 1794573183, + "narHash": "sha256-mf9WmUD1+2HL58DRmUQDfCXLxoN4RYKB3A2DxDSKt40=", + "owner": "rustsec", + "repo": "advisory-db", + "rev": "4ef2df8e806a65e2f1ecc14f56f51249062b23d6", + "type": "github" + }, + "original": { + "owner": "rustsec", + "repo": "advisory-db", + "type": "github" + } + }, "crane": { "locked": { "lastModified": 1783030479, @@ -51,6 +67,7 @@ }, "root": { "inputs": { + "advisory-db": "advisory-db", "crane": "crane", "flake-utils": "flake-utils", "nixpkgs": "nixpkgs" diff --git a/flake.nix b/flake.nix index 3740fac..f4ddb0b 100644 --- a/flake.nix +++ b/flake.nix @@ -5,9 +5,14 @@ nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; crane.url = "github:ipetkov/crane"; flake-utils.url = "github:numtide/flake-utils"; + # RustSec security advisories, for the audit check. `nix flake update advisory-db` refreshes it. + advisory-db = { + url = "github:rustsec/advisory-db"; + flake = false; + }; }; - outputs = { self, nixpkgs, crane, flake-utils }: + outputs = { self, nixpkgs, crane, flake-utils, advisory-db }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; @@ -46,8 +51,8 @@ packages.default = p9fuse; packages.p9fuse = p9fuse; - # `nix flake check` runs all of these: build, clippy (-D warnings), docs (-D warnings), and - # rustfmt. + # `nix flake check` runs all of these: build, clippy (-D warnings), docs (-D warnings), + # rustfmt, and dependency checks. checks = { inherit p9fuse; clippy = craneLib.cargoClippy (commonArgs // { @@ -59,6 +64,17 @@ RUSTDOCFLAGS = "-D warnings"; }); fmt = craneLib.cargoFmt { inherit (commonArgs) src; }; + # Licenses, banned crates and crate sources, as configured in deny.toml. + deny = craneLib.cargoDeny { + src = ./.; + cargoDenyChecks = "bans licenses sources"; + }; + # Known security advisories, including unsound ones. + audit = craneLib.cargoAudit { + inherit advisory-db; + src = ./.; + cargoAuditExtraArgs = "--deny unsound --ignore RUSTSEC-2021-0154"; + }; }; devShells.default = craneLib.devShell {