Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 1 addition & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,19 @@
name: CI

# Runs on the self-hosted Nix runners, where nix (with flakes) and a setuid `fusermount3`
# (/run/wrappers/bin) are already present -- so every tool comes from the flake: no apt, no rustup,
# no nix-installer. `nix flake check` runs build + clippy (-D warnings) + rustfmt; the FUSE
# integration tests run under `nix develop` because they need /dev/fuse.

on:
push:
pull_request:
workflow_dispatch:

jobs:
ci:
runs-on: [self-hosted, Linux]
runs-on: arc-medium
if: "!contains(github.event.head_commit.message, 'noci')"
steps:
- uses: actions/checkout@v5

# Build + clippy (-D warnings) + rustfmt, all defined as flake checks.
- name: nix flake check
run: nix flake check -L

# Integration tests mount a real diod export over FUSE, so they need /dev/fuse and a *setuid*
# `fusermount3` -- put /run/wrappers/bin ahead of the dev shell's non-setuid fusermount3 (set
# inside `nix develop`, since nix re-prepends its own bins).
- name: integration tests (diod + FUSE)
run: nix develop -c bash -c 'export PATH="/run/wrappers/bin:$PATH"; cargo test'
8 changes: 7 additions & 1 deletion src/fuse9p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ fn to_fileattr(ino: u64, a: &Attr) -> FileAttr {
impl Fuse9p {
/// Mount at `mountpoint`, blocking until unmounted. Builds the client, attaches, then runs the
/// FUSE session on a blocking thread (callbacks bridge back to the runtime via `Handle`).
#[allow(clippy::too_many_arguments)]
pub async fn run(
transport: Box<dyn crate::transport::NineTransport>,
mountpoint: &Path,
Expand All @@ -286,6 +287,8 @@ impl Fuse9p {
// the same path (the default); leaving it undetached keeps failed I/O visible as ENOTCONN
// rather than briefly exposing whatever is underneath.
detach_on_transport_loss: bool,
// Whether to mount with `default_permissions`.
default_permissions: bool,
) -> Result<(), Box<dyn std::error::Error>> {
tracing::info!(?tuning, "mount9p-fuse: tuning");
// Attach as `uid` so the server acts as that user for file ops (a multiuser server like diod
Expand Down Expand Up @@ -324,15 +327,18 @@ impl Fuse9p {
let mut options = vec![
MountOption::FSName("p9fuse".to_string()),
MountOption::Subtype("9p".to_string()),
MountOption::DefaultPermissions,
];
if default_permissions {
options.push(MountOption::DefaultPermissions);
}
// When mounting as root, the mount is root-owned but processes running as another uid can
// only traverse it with `allow_other`. Only root may set allow_other without
// `user_allow_other` in /etc/fuse.conf, so gate it on euid 0 -- an unprivileged mount is
// same-uid and doesn't need it (and would fail to set it).
if nix::unistd::geteuid().as_raw() == 0 {
options.push(MountOption::AllowOther);
}

let mp = mountpoint.to_path_buf();
tracing::info!(?mp, "mount9p-fuse: mounting FUSE filesystem");
// Use Session (not fuser::mount2) so we can take a Notifier: that's how out-of-band changes
Expand Down
5 changes: 1 addition & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@ use std::path::Path;
/// - `uid` is the identity to attach as (`n_uname`); the server acts as this user for file ops.
/// - `aname` is the export name to attach (must match the server's export, e.g. `"/export"`).
/// - `tuning` controls the caching / write-back knobs (see [`Tuning`]).
///
/// On 9p transport loss this exits and detaches the mount so a supervisor can remount cleanly (the
/// default). Call [`Fuse9p::run`] directly to control that with `detach_on_transport_loss`.
pub async fn mount(
transport: Box<dyn NineTransport>,
mountpoint: &Path,
Expand All @@ -53,5 +50,5 @@ pub async fn mount(
aname: &str,
tuning: Tuning,
) -> Result<(), Box<dyn std::error::Error>> {
Fuse9p::run(transport, mountpoint, msize, uid, aname, tuning, true).await
Fuse9p::run(transport, mountpoint, msize, uid, aname, tuning, true, true).await
}
13 changes: 11 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ enum Cmd {
/// place (I/O then fails with ENOTCONN rather than briefly exposing what's underneath).
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
detach_on_transport_loss: bool,
/// Let the kernel enforce permissions against the owner/mode the 9p server reports.
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
default_permissions: bool,

mountpoint: PathBuf,
},
Expand Down Expand Up @@ -141,6 +144,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
writeback,
wb_depth,
detach_on_transport_loss,
default_permissions,
mountpoint,
} => {
let transport = build_transport(&connect, &parse_headers(&headers)?).await?;
Expand All @@ -161,6 +165,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
&aname,
tuning,
detach_on_transport_loss,
default_permissions,
)
.await
}
Expand Down Expand Up @@ -214,10 +219,14 @@ async fn build_transport(
headers: &[(String, String)],
) -> Result<Box<dyn NineTransport>, Box<dyn std::error::Error>> {
if let Some(addr) = connect.strip_prefix("tcp://") {
Ok(Box::new(retry_connect(|| TcpTransport::connect(addr)).await?))
Ok(Box::new(
retry_connect(|| TcpTransport::connect(addr)).await?,
))
} else if let Some(path) = connect.strip_prefix("unix://") {
let path = Path::new(path);
Ok(Box::new(retry_connect(|| UnixTransport::connect(path)).await?))
Ok(Box::new(
retry_connect(|| UnixTransport::connect(path)).await?,
))
} else if connect.starts_with("ws://") || connect.starts_with("wss://") {
Ok(Box::new(
WebSocketTransport::connect(connect, headers).await?,
Expand Down