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",
diff --git a/Cargo.toml b/Cargo.toml
index d0f58de..c2ae6b2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -42,3 +42,18 @@ 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"
+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/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 5528b0e..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,14 +51,30 @@
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),
+ # rustfmt, and dependency checks.
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; };
+ # 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 {
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 a3a9967..9d72f15 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,
@@ -160,15 +160,16 @@ 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);
}
fn write(
&mut self,
- _req: &Request,
+ _req: &Request<'_>,
_ino: u64,
fh: u64,
offset: i64,
@@ -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,14 +197,14 @@ 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) {
+ 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 +212,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 +233,7 @@ impl Filesystem for Fuse9p {
fn release(
&mut self,
- _req: &Request,
+ _req: &Request<'_>,
_ino: u64,
fh: u64,
_flags: i32,
@@ -239,7 +249,7 @@ impl Filesystem for Fuse9p {
fn create(
&mut self,
- req: &Request,
+ req: &Request<'_>,
parent: u64,
name: &OsStr,
mode: u32,
@@ -269,7 +279,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,19 +288,21 @@ impl Filesystem for Fuse9p {
fn readdir(
&mut self,
- _req: &Request,
+ _req: &Request<'_>,
_ino: u64,
fh: u64,
offset: i64,
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,
) {
@@ -303,7 +315,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,
@@ -311,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);
@@ -330,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 {
@@ -340,14 +354,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 +386,7 @@ impl Filesystem for Fuse9p {
fn symlink(
&mut self,
- req: &Request,
+ req: &Request<'_>,
parent: u64,
name: &OsStr,
target: &Path,
@@ -379,23 +400,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 +445,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;
@@ -476,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..2732474 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,
@@ -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 4e0245e..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);
@@ -288,7 +287,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 79e65ec..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
}
@@ -212,9 +218,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 +252,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();
diff --git a/src/transport.rs b/src/transport.rs
index 24d0f7d..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 = std::mem::size_of::() as libc::socklen_t;
- let rc = libc::getsockopt(
- fd,
- libc::SOL_SOCKET,
- libc::SO_DOMAIN,
- &mut domain as *mut _ as *mut libc::c_void,
- &mut len,
- );
+ let mut len = libc::socklen_t::try_from(size_of::()).unwrap();
+ // 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 fdcdc03..4a3c8ed 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,
@@ -349,7 +352,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.
@@ -357,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();
@@ -611,19 +614,14 @@ 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) = 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).
@@ -826,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");