Skip to content
Open
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
38 changes: 38 additions & 0 deletions src/abi/fuse_abi_linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ const PERFILE_DAX: u64 = 0x2_0000_0000;
// this flag indicates whether the guest kernel enable resend
const HAS_RESEND: u64 = 1_u64 << 39;

// The kernel sends a supplementary group matching the parent directory's
// group in create/mkdir/symlink/mknod requests (FUSE_EXT_GROUPS extension).
const CREATE_SUPP_GROUP: u64 = 1_u64 << 34;

// This flag indicates whether to enable fd-passthrough. It was defined in the
// Anolis kernel but not in the upstream kernel. To avoid collision, we'll set
// it to the most significant bit.
Expand Down Expand Up @@ -464,6 +468,12 @@ bitflags! {

/// indicates whether the kernel support resend inflight request
const HAS_RESEND = HAS_RESEND;

/// Indicates that the kernel sends a supplementary group matching the
/// parent directory's group in create/mkdir/symlink/mknod requests
/// (FUSE_EXT_GROUPS extension), so that objects created in setgid
/// directories get the correct group ownership.
const CREATE_SUPP_GROUP = CREATE_SUPP_GROUP;
}
}

Expand Down Expand Up @@ -1086,6 +1096,34 @@ pub struct InitOut {
}
unsafe impl ByteValued for InitOut {}

/// Extension type: supplementary group extension (`SuppGroups`).
///
/// Values 0..=31 of `ExtHeader::ext_type` are reserved for the security
/// context extension.
pub const FUSE_EXT_GROUPS: u32 = 32;

/// Header of a request extension appended to create/mkdir/symlink/mknod
/// requests by the kernel.
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct ExtHeader {
/// Total size of this extension including this header.
pub size: u32,
/// Type of extension.
pub ext_type: u32,
}
unsafe impl ByteValued for ExtHeader {}

/// Payload of the supplementary group extension: `nr_groups` is followed by
/// a flexible array of group IDs.
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct SuppGroups {
/// Number of supplementary groups.
pub nr_groups: u32,
}
unsafe impl ByteValued for SuppGroups {}

#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct InterruptIn {
Expand Down
11 changes: 11 additions & 0 deletions src/api/filesystem/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,16 @@ pub struct Context {

/// The thread group ID of the calling process.
pub pid: libc::pid_t,

/// A supplementary group of the calling process matching the parent
/// directory's group.
///
/// Set by the server for create/mkdir/symlink/mknod requests when the
/// kernel negotiated FUSE_CREATE_SUPP_GROUP and sent the FUSE_EXT_GROUPS
/// request extension. File systems should create the new object with
/// this group in their supplementary group list so that objects created
/// in setgid directories get the correct group ownership.
pub supp_gid: Option<libc::gid_t>,
}

impl Context {
Expand All @@ -408,6 +418,7 @@ impl From<&fuse::InHeader> for Context {
uid: source.uid,
gid: source.gid,
pid: source.pid as i32,
supp_gid: None,
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/api/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ pub const MAX_REQ_PAGES: u16 = 256; // 1MB
pub struct Server<F: FileSystem + Sync> {
fs: F,
vers: ArcSwap<ServerVersion>,
// Options negotiated with the kernel through INIT, see `init()`.
options: ArcSwap<FsOptions>,
}

impl<F: FileSystem + Sync> Server<F> {
Expand All @@ -62,6 +64,7 @@ impl<F: FileSystem + Sync> Server<F> {
major: KERNEL_VERSION,
minor: KERNEL_MINOR_VERSION,
})),
options: ArcSwap::new(Arc::new(FsOptions::empty())),
}
}
}
Expand Down
192 changes: 192 additions & 0 deletions src/api/server/sync_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,80 @@ use crate::transport::FuseDevWriter;
use crate::transport::{pagesize, FsCacheReqHandler, Reader, Writer};
use crate::{bytes_to_cstr, encode_io_error_kind, BitmapSlice, Error, Result};

/// Parse the request extensions appended by the kernel after the
/// NUL-terminated name(s) of create/mkdir/symlink/mknod requests, and return
/// the first supplementary group carried by a FUSE_EXT_GROUPS extension.
///
/// The kernel sends that extension only when FUSE_CREATE_SUPP_GROUP has been
/// negotiated and one of the caller's supplementary groups matches the parent
/// directory's group, so that objects created in setgid directories can get
/// the correct group ownership. See `get_create_supp_group()` and
/// `fuse_ext_size()` in `fs/fuse/dir.c` of the Linux kernel.
#[cfg(target_os = "linux")]
fn parse_create_extensions(options: &FsOptions, mut tail: &[u8]) -> Result<Option<u32>> {
const LINUX_NRGROUPS_MAX: u32 = 65536;

let einval = || Error::DecodeMessage(io::Error::from_raw_os_error(libc::EINVAL));
let read_u32 = |buf: &[u8]| {
let mut bytes = [0u8; 4];
bytes.copy_from_slice(&buf[0..4]);
u32::from_ne_bytes(bytes)
};

while tail.len() >= size_of::<ExtHeader>() {
// struct fuse_ext_header { u32 size; u32 type; }. The extension may
// start at an unaligned offset, so read the fields manually.
let size = read_u32(tail) as usize;
let ext_type = read_u32(&tail[size_of::<u32>()..]);
if size < size_of::<ExtHeader>() || size > tail.len() {
return Err(einval());
}

if ext_type == FUSE_EXT_GROUPS {
// The kernel only appends this extension when FUSE_CREATE_SUPP_GROUP
// has been negotiated through INIT; reject it otherwise.
if !options.contains(FsOptions::CREATE_SUPP_GROUP) {
return Err(einval());
}

let body = &tail[size_of::<ExtHeader>()..size];
// struct fuse_supp_groups { u32 nr_groups; u32 groups[]; }
if body.len() < size_of::<SuppGroups>() {
return Err(einval());
}
let nr_groups = read_u32(body);
if nr_groups == 0 || nr_groups > LINUX_NRGROUPS_MAX {
return Err(einval());
}
// Extensions are padded to an 8-byte boundary, see FUSE_REC_ALIGN
// and fuse_ext_size() in the kernel.
let expected = size_of::<SuppGroups>() + size_of::<u32>() * nr_groups as usize;
if body.len() != ((expected + 7) & !7) {
return Err(einval());
}
// The upstream kernel currently sends a single group: the one
// matching the parent directory's group.
let gid = read_u32(&body[size_of::<SuppGroups>()..]);
return Ok(Some(gid));
}

tail = &tail[size..];
}

// A trailing chunk shorter than an extension header is a protocol violation.
if !tail.is_empty() {
return Err(einval());
}

Ok(None)
}

// The macOS kernel doesn't support request extensions.
#[cfg(not(target_os = "linux"))]
fn parse_create_extensions(_options: &FsOptions, _tail: &[u8]) -> Result<Option<u32>> {
Ok(None)
}

impl<F: FileSystem + Sync> Server<F> {
#[cfg(feature = "fusedev")]
/// Use to send notify msg to kernel fuse
Expand Down Expand Up @@ -311,6 +385,10 @@ impl<F: FileSystem + Sync> Server<F> {
let buf = ServerUtil::get_message_body(&mut ctx.r, &ctx.in_header, 0)?;
// The name and linkname are encoded one after another and separated by a nul character.
let (name, linkname) = ServerUtil::extract_two_cstrs(&buf)?;
ctx.context.supp_gid = parse_create_extensions(
&self.options.load(),
&buf[name.to_bytes_with_nul().len() + linkname.to_bytes_with_nul().len()..],
)?;

match self.fs.symlink(ctx.context(), linkname, ctx.nodeid(), name) {
Ok(entry) => ctx.reply_ok(Some(EntryOut::from(entry)), None),
Expand All @@ -328,6 +406,8 @@ impl<F: FileSystem + Sync> Server<F> {
error!("fuse: bytes to cstr error: {:?}, {:?}", buf, e);
e
})?;
ctx.context.supp_gid =
parse_create_extensions(&self.options.load(), &buf[name.to_bytes_with_nul().len()..])?;

match self
.fs
Expand All @@ -346,6 +426,8 @@ impl<F: FileSystem + Sync> Server<F> {
error!("fuse: bytes to cstr error: {:?}, {:?}", buf, e);
e
})?;
ctx.context.supp_gid =
parse_create_extensions(&self.options.load(), &buf[name.to_bytes_with_nul().len()..])?;

match self
.fs
Expand Down Expand Up @@ -774,6 +856,7 @@ impl<F: FileSystem + Sync> Server<F> {
};

let enabled = capable & want;
self.options.store(Arc::new(enabled));
let enabled_flags = enabled.bits();
let mut out = InitOut {
major: KERNEL_VERSION,
Expand Down Expand Up @@ -1019,6 +1102,8 @@ impl<F: FileSystem + Sync> Server<F> {
error!("fuse: bytes to cstr error: {:?}, {:?}", buf, e);
e
})?;
ctx.context.supp_gid =
parse_create_extensions(&self.options.load(), &buf[name.to_bytes_with_nul().len()..])?;

match self.fs.create(ctx.context(), ctx.nodeid(), name, args) {
Ok((entry, handle, opts, passthrough)) => {
Expand Down Expand Up @@ -1450,6 +1535,113 @@ fn add_dirent<S: BitmapSlice>(
#[cfg(test)]
mod tests {

#[cfg(target_os = "linux")]
mod tests_parse_extensions {
use super::super::*;

fn build_supp_groups_ext(gid: u32) -> Vec<u8> {
let mut buf = Vec::new();
let size = (size_of::<ExtHeader>() + size_of::<SuppGroups>() + size_of::<u32>()) as u32;
buf.extend_from_slice(&size.to_ne_bytes());
buf.extend_from_slice(&FUSE_EXT_GROUPS.to_ne_bytes());
buf.extend_from_slice(&1u32.to_ne_bytes());
buf.extend_from_slice(&gid.to_ne_bytes());
buf
}

fn assert_einval(res: Result<Option<u32>>) {
match res {
Err(Error::DecodeMessage(e)) => {
assert_eq!(e.raw_os_error(), Some(libc::EINVAL));
}
other => panic!("unexpected result: {:?}", other),
}
}

#[test]
fn test_parse_create_extensions_empty() {
assert_eq!(
parse_create_extensions(&FsOptions::CREATE_SUPP_GROUP, &[]).unwrap(),
None
);
}

#[test]
fn test_parse_create_extensions_supp_group() {
let buf = build_supp_groups_ext(1000);
assert_eq!(
parse_create_extensions(&FsOptions::CREATE_SUPP_GROUP, &buf).unwrap(),
Some(1000)
);
}

#[test]
fn test_parse_create_extensions_skip_unknown() {
// An unknown extension followed by the supp group extension:
// extensions are padded to 8 bytes (fuse_ext_size()).
let mut buf = Vec::new();
let size = (size_of::<ExtHeader>() + 8) as u32;
buf.extend_from_slice(&size.to_ne_bytes());
buf.extend_from_slice(&0u32.to_ne_bytes());
buf.extend_from_slice(&[0u8; 8]);
buf.extend_from_slice(&build_supp_groups_ext(1000));

assert_eq!(
parse_create_extensions(&FsOptions::CREATE_SUPP_GROUP, &buf).unwrap(),
Some(1000)
);
}

#[test]
fn test_parse_create_extensions_truncated_header() {
assert_einval(parse_create_extensions(
&FsOptions::CREATE_SUPP_GROUP,
&[0u8; 4],
));
}

#[test]
fn test_parse_create_extensions_bad_size() {
// Extension size smaller than the header.
let mut buf = Vec::new();
buf.extend_from_slice(&4u32.to_ne_bytes());
buf.extend_from_slice(&FUSE_EXT_GROUPS.to_ne_bytes());
assert_einval(parse_create_extensions(&FsOptions::CREATE_SUPP_GROUP, &buf));

// Extension size exceeding the payload.
let mut buf = build_supp_groups_ext(1000);
buf.truncate(8);
buf[0..4].copy_from_slice(&100u32.to_ne_bytes());
assert_einval(parse_create_extensions(&FsOptions::CREATE_SUPP_GROUP, &buf));
}

#[test]
fn test_parse_create_extensions_bad_nr_groups() {
// nr_groups is zero.
let mut buf = build_supp_groups_ext(1000);
buf[8..12].copy_from_slice(&0u32.to_ne_bytes());
assert_einval(parse_create_extensions(&FsOptions::CREATE_SUPP_GROUP, &buf));

// Payload size doesn't match nr_groups.
let mut buf = build_supp_groups_ext(1000);
buf[8..12].copy_from_slice(&2u32.to_ne_bytes());
assert_einval(parse_create_extensions(&FsOptions::CREATE_SUPP_GROUP, &buf));
}

#[test]
fn test_parse_create_extensions_not_negotiated() {
// The supp group extension must be rejected when
// FUSE_CREATE_SUPP_GROUP hasn't been negotiated.
let buf = build_supp_groups_ext(1000);
assert_einval(parse_create_extensions(&FsOptions::empty(), &buf));
// An empty tail is still fine.
assert_eq!(
parse_create_extensions(&FsOptions::empty(), &[]).unwrap(),
None
);
}
}

#[cfg(all(feature = "fusedev", target_os = "linux"))]
mod tests_fusedev {
use super::super::*;
Expand Down
1 change: 1 addition & 0 deletions src/api/vfs/async_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ mod tests {
uid: 0,
gid: 0,
pid: 0,
supp_gid: None,
};

assert!(vfs.mount(Box::new(fs), "/x/y").is_ok());
Expand Down
1 change: 1 addition & 0 deletions src/api/vfs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ impl Default for VfsOptions {
| FsOptions::EXPLICIT_INVAL_DATA
| FsOptions::ZERO_MESSAGE_OPENDIR
| FsOptions::HANDLE_KILLPRIV_V2
| FsOptions::CREATE_SUPP_GROUP
| FsOptions::PERFILE_DAX;
VfsOptions {
no_open: true,
Expand Down
Loading
Loading