Skip to content

Commit 9e5e405

Browse files
authored
Merge pull request #182 from influxdata/crepererum/harden-vfs
refactor: harden virtual file system
2 parents bd9abd6 + 70a85ce commit 9e5e405

File tree

11 files changed

+2402
-108
lines changed

11 files changed

+2402
-108
lines changed

guests/evil/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ impl Evil {
5353
root: Box::new(root::not_tar::root),
5454
udfs: Box::new(common::udfs_empty),
5555
},
56+
"root::path_long" => Self {
57+
root: Box::new(root::path_long::root),
58+
udfs: Box::new(common::udfs_empty),
59+
},
5660
"root::unsupported_entry" => Self {
5761
root: Box::new(root::unsupported_entry::root),
5862
udfs: Box::new(common::udfs_empty),

guests/evil/src/root/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
pub(crate) mod invalid_entry;
33
pub(crate) mod many_files;
44
pub(crate) mod not_tar;
5+
pub(crate) mod path_long;
56
pub(crate) mod unsupported_entry;

guests/evil/src/root/path_long.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//! Evil payloads that creates a file with a long path.
2+
3+
/// Return root file system.
4+
#[expect(clippy::unnecessary_wraps, reason = "public API through export! macro")]
5+
pub(crate) fn root() -> Option<Vec<u8>> {
6+
let mut ar = tar::Builder::new(Vec::new());
7+
8+
let limit: usize = std::env::var("limit").unwrap().parse().unwrap();
9+
10+
let mut header = tar::Header::new_gnu();
11+
header
12+
.set_path(std::iter::repeat_n('x', limit + 1).collect::<String>())
13+
.unwrap();
14+
header.set_size(0);
15+
header.set_cksum();
16+
ar.append(&header, b"".as_slice()).unwrap();
17+
18+
Some(ar.into_inner().unwrap())
19+
}

host/src/error.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Helper for simpler error handling.
22
use datafusion_common::DataFusionError;
3+
use wasmtime_wasi::p2::FsError;
34

45
/// Extension for [`wasmtime::Error`].
56
pub(crate) trait WasmToDataFusionErrorExt {
@@ -74,3 +75,51 @@ where
7475
self.map_err(|e| e.into().context(description))
7576
}
7677
}
78+
79+
/// Failed allocation error.
80+
#[derive(Debug, Clone)]
81+
#[expect(missing_copy_implementations, reason = "allow later extensions")]
82+
pub struct LimitExceeded {
83+
/// Name of the allocation type/resource.
84+
pub(crate) name: &'static str,
85+
86+
/// Allocation limit.
87+
pub(crate) limit: u64,
88+
89+
/// Current allocation size.
90+
pub(crate) current: u64,
91+
92+
/// Requested/additional allocation.
93+
pub(crate) requested: u64,
94+
}
95+
96+
impl std::fmt::Display for LimitExceeded {
97+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98+
let Self {
99+
name,
100+
limit,
101+
current,
102+
requested,
103+
} = self;
104+
105+
write!(
106+
f,
107+
"{name} limit reached: limit<={limit} current=={current} requested+={requested}"
108+
)
109+
}
110+
}
111+
112+
impl std::error::Error for LimitExceeded {}
113+
114+
impl From<LimitExceeded> for std::io::Error {
115+
fn from(e: LimitExceeded) -> Self {
116+
Self::new(std::io::ErrorKind::QuotaExceeded, e.to_string())
117+
}
118+
}
119+
120+
impl From<LimitExceeded> for FsError {
121+
fn from(e: LimitExceeded) -> Self {
122+
let e: std::io::Error = e.into();
123+
e.into()
124+
}
125+
}

host/src/lib.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,13 @@ use crate::{
4343
#[cfg(test)]
4444
use datafusion_udf_wasm_bundle as _;
4545
#[cfg(test)]
46-
use insta as _;
47-
#[cfg(test)]
4846
use regex as _;
4947
#[cfg(test)]
5048
use wiremock as _;
5149

5250
mod bindings;
5351
mod conversion;
54-
mod error;
52+
pub mod error;
5553
pub mod http;
5654
mod linker;
5755
mod tokio_helpers;
@@ -449,7 +447,7 @@ impl WasmScalarUdf {
449447
let component = component_res.context("create WASM component", None)?;
450448

451449
// Create in-memory VFS
452-
let vfs_state = VfsState::new(&permissions.vfs);
450+
let vfs_state = VfsState::new(permissions.vfs.clone());
453451

454452
// set up WASI p2 context
455453
let stderr = MemoryOutputPipe::new(1024);

host/src/vfs/limits.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//! Limit configuration.
2+
3+
/// Limits for virtual filesystems.
4+
///
5+
/// # Depth
6+
/// Note that we do NOT per se limit the depth of the file system, since it is virtually not different from limiting
7+
/// [the number of inodes](Self::inodes). Expensive path traversal is further limited by
8+
/// [`max_path_length`](Self::max_path_length).
9+
#[derive(Debug, Clone)]
10+
#[expect(missing_copy_implementations, reason = "allow later extensions")]
11+
pub struct VfsLimits {
12+
/// Maximum number of inodes.
13+
pub inodes: u64,
14+
15+
/// Maximum number of bytes in size.
16+
pub bytes: u64,
17+
18+
/// Maximum path length, in bytes.
19+
pub max_path_length: u64,
20+
21+
/// Maximum path segment size, in bytes.
22+
///
23+
/// Keep this to a rather small size to prevent super-linear complexity due to string hashing.
24+
pub max_path_segment_size: u64,
25+
}
26+
27+
impl Default for VfsLimits {
28+
fn default() -> Self {
29+
Self {
30+
inodes: 10_000,
31+
// 100MB
32+
bytes: 100 * 1024 * 1024,
33+
max_path_length: 255,
34+
max_path_segment_size: 50,
35+
}
36+
}
37+
}

0 commit comments

Comments
 (0)