Skip to content

Commit 240dbd9

Browse files
shayan.syjiangliu
authored andcommitted
passthrough: add ability to disallow operations that could change file size
Introduce 'seal_size' flag prohibits operations which will change file size or allocate file block exceed file size. This assumes the fuse server side is trustable, and client side is not trusted and not allowed to change file size. For example, in virtiofs case, we trust the host side and don't trust guest side. Signed-off-by: shayan.sy <[email protected]> Signed-off-by: Eryu Guan <[email protected]>
1 parent 0131d11 commit 240dbd9

File tree

4 files changed

+112
-2
lines changed

4 files changed

+112
-2
lines changed

src/api/vfs/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,9 @@ pub struct VfsOptions {
217217
/// to remove security.capability xattr and setuid/setgid bits. See details in
218218
/// comments for HANDLE_KILLPRIV_V2
219219
pub killpriv_v2: bool,
220+
/// Reject requests which will change the file size, or allocate file
221+
/// blocks exceed file size.
222+
pub seal_size: bool,
220223
/// File system options passed in from client
221224
pub in_opts: FsOptions,
222225
/// File system options returned to client
@@ -236,6 +239,7 @@ impl Default for VfsOptions {
236239
no_opendir: true,
237240
no_writeback: false,
238241
no_readdir: false,
242+
seal_size: false,
239243
killpriv_v2: false,
240244
in_opts: FsOptions::empty(),
241245
out_opts: FsOptions::ASYNC_READ
@@ -946,6 +950,7 @@ mod tests {
946950
assert_eq!(opts.no_opendir, true);
947951
assert_eq!(opts.no_writeback, false);
948952
assert_eq!(opts.no_readdir, false);
953+
assert_eq!(opts.seal_size, false);
949954
assert_eq!(opts.killpriv_v2, false);
950955
assert_eq!(opts.in_opts.is_empty(), true);
951956

@@ -957,6 +962,7 @@ mod tests {
957962
assert_eq!(opts.no_opendir, false);
958963
assert_eq!(opts.no_writeback, false);
959964
assert_eq!(opts.no_readdir, false);
965+
assert_eq!(opts.seal_size, false);
960966
assert_eq!(opts.killpriv_v2, false);
961967

962968
vfs.destroy();

src/passthrough/async_io.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use async_trait::async_trait;
1212

1313
use super::*;
1414
use crate::abi::fuse_abi::{
15-
CreateIn, OpenOptions, SetattrValid, FOPEN_IN_KILL_SUIDGID, WRITE_KILL_PRIV,
15+
CreateIn, Opcode, OpenOptions, SetattrValid, FOPEN_IN_KILL_SUIDGID, WRITE_KILL_PRIV,
1616
};
1717
use crate::api::filesystem::{
1818
AsyncFileSystem, AsyncZeroCopyReader, AsyncZeroCopyWriter, Context, FileSystem,
@@ -478,6 +478,10 @@ impl<S: BitmapSlice + Send + Sync> AsyncFileSystem for PassthroughFs<S> {
478478
}
479479
};
480480
481+
if valid.contains(SetattrValid::SIZE) && self.seal_size.load(Ordering::Relaxed) {
482+
return Err(io::Error::from_raw_os_error(libc::EPERM));
483+
}
484+
481485
if valid.contains(SetattrValid::MODE) {
482486
// Safe because this doesn't modify any memory and we check the return value.
483487
let res = unsafe {
@@ -725,6 +729,13 @@ impl<S: BitmapSlice + Send + Sync> AsyncFileSystem for PassthroughFs<S> {
725729
.async_get_data(ctx, handle, inode, libc::O_RDWR)
726730
.await?;
727731
732+
if self.seal_size.load(Ordering::Relaxed) {
733+
let st = self
734+
.async_stat_fd(cxt, data.get_handle_raw_fd(), None)
735+
.await?;
736+
self.seal_size_check(Opcode::Write, st.st_size as u64, offset, size as u64, 0)?;
737+
}
738+
728739
// Fallback to sync io if KILLPRIV_V2 is enabled to work around a limitation of io_uring.
729740
if self.killpriv_v2.load(Ordering::Relaxed) && (fuse_flags & WRITE_KILL_PRIV != 0) {
730741
// Manually implement File::try_clone() by borrowing fd of data.file instead of dup().
@@ -786,6 +797,19 @@ impl<S: BitmapSlice + Send + Sync> AsyncFileSystem for PassthroughFs<S> {
786797
.get_drive::<D>()
787798
.ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))?;
788799
800+
if self.seal_size.load(Ordering::Relaxed) {
801+
let st = self
802+
.async_stat_fd(cxt, data.get_handle_raw_fd(), None)
803+
.await?;
804+
self.seal_size_check(
805+
Opcode::Fallocate,
806+
st.st_size as u64,
807+
offset,
808+
length,
809+
mode as i32,
810+
)?;
811+
}
812+
789813
AsyncUtil::fallocate(drive, data.get_handle_raw_fd(), offset, length, mode).await
790814
*/
791815
}

src/passthrough/mod.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use std::time::Duration;
3030
use vm_memory::ByteValued;
3131

3232
use crate::abi::fuse_abi as fuse;
33+
use crate::abi::fuse_abi::Opcode;
3334
use crate::api::filesystem::Entry;
3435
use crate::api::{
3536
validate_path_component, BackendFileSystem, CURRENT_DIR_CSTR, EMPTY_CSTR, PARENT_DIR_CSTR,
@@ -463,6 +464,11 @@ pub struct Config {
463464
/// directory is empty even if it has children.
464465
pub no_readdir: bool,
465466

467+
/// Control whether to refuse operations which modify the size of the file. For a share memory
468+
/// file mounted from host, seal_size can prohibit guest to increase the size of
469+
/// share memory file to attack the host.
470+
pub seal_size: bool,
471+
466472
/// What size file supports dax
467473
/// * If dax_file_size == None, DAX will disable to all files.
468474
/// * If dax_file_size == 0, DAX will enable all files.
@@ -486,6 +492,7 @@ impl Default for Config {
486492
killpriv_v2: false,
487493
inode_file_handles: false,
488494
no_readdir: false,
495+
seal_size: false,
489496
dax_file_size: None,
490497
}
491498
}
@@ -536,6 +543,9 @@ pub struct PassthroughFs<S: BitmapSlice + Send + Sync = ()> {
536543
// Whether no_readdir is enabled.
537544
no_readdir: AtomicBool,
538545

546+
// Whether seal_size is enabled.
547+
seal_size: AtomicBool,
548+
539549
// Whether per-file DAX feature is enabled.
540550
// Init from guest kernel Init cmd of fuse fs.
541551
perfile_dax: AtomicBool,
@@ -572,6 +582,7 @@ impl<S: BitmapSlice + Send + Sync> PassthroughFs<S> {
572582
no_opendir: AtomicBool::new(false),
573583
killpriv_v2: AtomicBool::new(false),
574584
no_readdir: AtomicBool::new(cfg.no_readdir),
585+
seal_size: AtomicBool::new(cfg.seal_size),
575586
perfile_dax: AtomicBool::new(false),
576587
cfg,
577588

@@ -1000,6 +1011,54 @@ impl<S: BitmapSlice + Send + Sync> PassthroughFs<S> {
10001011
}
10011012
validate_path_component(name)
10021013
}
1014+
1015+
// When seal_size is set, we don't allow operations that could change file size nor allocate
1016+
// space beyond EOF
1017+
fn seal_size_check(
1018+
&self,
1019+
opcode: Opcode,
1020+
file_size: u64,
1021+
offset: u64,
1022+
size: u64,
1023+
mode: i32,
1024+
) -> io::Result<()> {
1025+
if offset.checked_add(size).is_none() {
1026+
error!(
1027+
"fuse: {:?}: invalid `offset` + `size` ({}+{}) overflows u64::MAX",
1028+
opcode, offset, size
1029+
);
1030+
return Err(io::Error::from_raw_os_error(libc::EINVAL));
1031+
}
1032+
1033+
match opcode {
1034+
// write should not exceed the file size.
1035+
Opcode::Write if size + offset > file_size => {
1036+
Err(io::Error::from_raw_os_error(libc::EPERM))
1037+
}
1038+
1039+
// fallocate operation should not allocate blocks exceed the file size.
1040+
//
1041+
// FALLOC_FL_COLLAPSE_RANGE or FALLOC_FL_INSERT_RANGE mode will change file size which
1042+
// is not allowed.
1043+
//
1044+
// FALLOC_FL_PUNCH_HOLE mode won't change file size, as it must be ORed with
1045+
// FALLOC_FL_KEEP_SIZE.
1046+
Opcode::Fallocate
1047+
if ((mode == 0
1048+
|| mode == libc::FALLOC_FL_KEEP_SIZE
1049+
|| mode & libc::FALLOC_FL_ZERO_RANGE != 0)
1050+
&& size + offset > file_size)
1051+
|| (mode & libc::FALLOC_FL_COLLAPSE_RANGE != 0
1052+
|| mode & libc::FALLOC_FL_INSERT_RANGE != 0) =>
1053+
{
1054+
Err(io::Error::from_raw_os_error(libc::EPERM))
1055+
}
1056+
1057+
// setattr operation should be handled in setattr handler, other operations won't
1058+
// change file size.
1059+
_ => Ok(()),
1060+
}
1061+
}
10031062
}
10041063

10051064
#[cfg(not(feature = "async-io"))]

src/passthrough/sync_io.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use std::sync::Arc;
1515
use std::time::Duration;
1616

1717
use super::*;
18-
use crate::abi::fuse_abi::{CreateIn, FOPEN_IN_KILL_SUIDGID, WRITE_KILL_PRIV};
18+
use crate::abi::fuse_abi::{CreateIn, Opcode, FOPEN_IN_KILL_SUIDGID, WRITE_KILL_PRIV};
1919
#[cfg(any(feature = "vhost-user-fs", feature = "virtiofs"))]
2020
use crate::abi::virtio_fs;
2121
use crate::api::filesystem::{
@@ -689,6 +689,12 @@ impl<S: BitmapSlice + Send + Sync> FileSystem for PassthroughFs<S> {
689689
// It's safe because the `data` variable's lifetime spans the whole function,
690690
// so data.file won't be closed.
691691
let f = unsafe { File::from_raw_fd(data.get_handle_raw_fd()) };
692+
693+
if self.seal_size.load(Ordering::Relaxed) {
694+
let st = Self::stat_fd(f.as_raw_fd(), None)?;
695+
self.seal_size_check(Opcode::Write, st.st_size as u64, offset, size as u64, 0)?;
696+
}
697+
692698
let mut f = ManuallyDrop::new(f);
693699

694700
// Cap restored when _killpriv is dropped
@@ -744,6 +750,10 @@ impl<S: BitmapSlice + Send + Sync> FileSystem for PassthroughFs<S> {
744750
}
745751
};
746752

753+
if valid.contains(SetattrValid::SIZE) && self.seal_size.load(Ordering::Relaxed) {
754+
return Err(io::Error::from_raw_os_error(libc::EPERM));
755+
}
756+
747757
if valid.contains(SetattrValid::MODE) {
748758
// Safe because this doesn't modify any memory and we check the return value.
749759
let res = unsafe {
@@ -1264,6 +1274,17 @@ impl<S: BitmapSlice + Send + Sync> FileSystem for PassthroughFs<S> {
12641274
let data = self.get_data(handle, inode, libc::O_RDWR)?;
12651275
let fd = data.get_handle_raw_fd();
12661276

1277+
if self.seal_size.load(Ordering::Relaxed) {
1278+
let st = Self::stat_fd(fd, None)?;
1279+
self.seal_size_check(
1280+
Opcode::Fallocate,
1281+
st.st_size as u64,
1282+
offset,
1283+
length,
1284+
mode as i32,
1285+
)?;
1286+
}
1287+
12671288
// Safe because this doesn't modify any memory and we check the return value.
12681289
let res = unsafe {
12691290
libc::fallocate64(

0 commit comments

Comments
 (0)