Skip to content

Commit f1b8d24

Browse files
committed
virtual-fs: Hide package contents under bind‑mounts by filtering overlay secondaries
1 parent d82f95b commit f1b8d24

2 files changed

Lines changed: 173 additions & 11 deletions

File tree

lib/virtual-fs/src/overlay_fs.rs

Lines changed: 117 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ use crate::{
6565
pub struct OverlayFileSystem<P, S> {
6666
primary: Arc<P>,
6767
secondaries: S,
68+
opaque_prefixes: Vec<PathBuf>,
6869
}
6970

7071
impl<P, S> OverlayFileSystem<P, S>
@@ -78,6 +79,21 @@ where
7879
OverlayFileSystem {
7980
primary: Arc::new(primary),
8081
secondaries,
82+
opaque_prefixes: Vec::new(),
83+
}
84+
}
85+
86+
/// Create a new [`FileSystem`] with opaque prefixes that hide any secondary
87+
/// entries under those paths.
88+
pub fn new_with_opaque_prefixes(
89+
primary: P,
90+
secondaries: S,
91+
opaque_prefixes: Vec<PathBuf>,
92+
) -> Self {
93+
OverlayFileSystem {
94+
primary: Arc::new(primary),
95+
secondaries,
96+
opaque_prefixes,
8197
}
8298
}
8399

@@ -96,8 +112,31 @@ where
96112
&mut self.secondaries
97113
}
98114

115+
fn is_opaque(&self, path: &Path) -> bool {
116+
let normalized = if path.is_absolute() {
117+
path.to_path_buf()
118+
} else {
119+
Path::new("/").join(path)
120+
};
121+
122+
self.opaque_prefixes
123+
.iter()
124+
.any(|prefix| normalized.starts_with(prefix))
125+
}
126+
127+
fn secondaries_iter<'a>(
128+
&'a self,
129+
path: &Path,
130+
) -> Box<dyn Iterator<Item = &'a (dyn FileSystem + Send)> + 'a> {
131+
if self.is_opaque(path) {
132+
Box::new(std::iter::empty())
133+
} else {
134+
Box::new(self.secondaries.filesystems().into_iter())
135+
}
136+
}
137+
99138
fn permission_error_or_not_found(&self, path: &Path) -> Result<(), FsError> {
100-
for fs in self.secondaries.filesystems() {
139+
for fs in self.secondaries_iter(path) {
101140
if ops::exists(fs, path) {
102141
return Err(FsError::PermissionDenied);
103142
}
@@ -132,7 +171,7 @@ where
132171
}
133172

134173
// Otherwise scan the secondaries
135-
for fs in self.secondaries.filesystems() {
174+
for fs in self.secondaries_iter(path) {
136175
match fs.readlink(path) {
137176
Err(e) if should_continue(e) => continue,
138177
other => return other,
@@ -148,7 +187,7 @@ where
148187
let mut white_outs = HashSet::new();
149188

150189
let filesystems = std::iter::once(&self.primary as &(dyn FileSystem + Send))
151-
.chain(self.secondaries().filesystems());
190+
.chain(self.secondaries_iter(path));
152191

153192
for fs in filesystems {
154193
match fs.read_dir(path) {
@@ -239,7 +278,7 @@ where
239278
// If the directory is contained in a secondary file system then we need to create a
240279
// whiteout file so that it is suppressed and is no longer returned in `readdir` calls.
241280

242-
let had_at_least_one_success = self.secondaries.filesystems().into_iter().any(|fs| {
281+
let had_at_least_one_success = self.secondaries_iter(path).any(|fs| {
243282
fs.read_dir(path).is_ok() && ops::create_white_out(&self.primary, path).is_ok()
244283
});
245284

@@ -297,7 +336,8 @@ where
297336
// the secondaries, in which case we need to copy it to the
298337
// primary rather than rename it
299338
if !had_at_least_one_success {
300-
for fs in self.secondaries.filesystems() {
339+
let secondaries: Vec<_> = self.secondaries_iter(&from).collect();
340+
for fs in secondaries {
301341
if fs.metadata(&from).is_ok() {
302342
ops::copy_reference_ext(fs, &self.primary, &from, &to).await?;
303343
had_at_least_one_success = true;
@@ -309,7 +349,8 @@ where
309349
// If the rename operation was a success then we need to update any
310350
// whiteout files on the primary before we return success.
311351
if had_at_least_one_success {
312-
for fs in self.secondaries.filesystems() {
352+
let secondaries: Vec<_> = self.secondaries_iter(&from).collect();
353+
for fs in secondaries {
313354
if fs.metadata(&from).is_ok() {
314355
tracing::trace!(
315356
path=%from.display(),
@@ -347,7 +388,7 @@ where
347388
}
348389

349390
// Otherwise scan the secondaries
350-
for fs in self.secondaries.filesystems() {
391+
for fs in self.secondaries_iter(path) {
351392
match fs.metadata(path) {
352393
Err(e) if should_continue(e) => continue,
353394
other => return other,
@@ -376,7 +417,7 @@ where
376417
}
377418

378419
// Otherwise scan the secondaries
379-
for fs in self.secondaries.filesystems() {
420+
for fs in self.secondaries_iter(path) {
380421
match fs.symlink_metadata(path) {
381422
Err(e) if should_continue(e) => continue,
382423
other => return other,
@@ -395,7 +436,7 @@ where
395436

396437
// If the file is contained in a secondary then then we need to create a
397438
// whiteout file so that it is suppressed.
398-
let had_at_least_one_success = self.secondaries.filesystems().into_iter().any(|fs| {
439+
let had_at_least_one_success = self.secondaries_iter(path).any(|fs| {
399440
fs.metadata(path).is_ok() && ops::create_white_out(&self.primary, path).is_ok()
400441
});
401442

@@ -503,7 +544,7 @@ where
503544

504545
// If the file is on a secondary then we should open it
505546
if !ops::has_white_out(&self.primary, path) {
506-
for fs in self.secondaries.filesystems() {
547+
for fs in self.secondaries_iter(path) {
507548
let mut sub_conf = conf.clone();
508549
sub_conf.create = false;
509550
sub_conf.create_new = false;
@@ -1169,6 +1210,72 @@ mod tests {
11691210
);
11701211
}
11711212

1213+
#[test]
1214+
fn overlay_opaque_prefix_hides_secondaries() {
1215+
let primary = MemFS::default();
1216+
let secondary = MemFS::default();
1217+
1218+
ops::create_dir_all(&primary, "/app/wp-content").unwrap();
1219+
primary
1220+
.new_open_options()
1221+
.create(true)
1222+
.write(true)
1223+
.open("/app/wp-content/host.txt")
1224+
.unwrap();
1225+
1226+
ops::create_dir_all(&secondary, "/app/wp-content/themes/twentyten").unwrap();
1227+
1228+
let overlay = OverlayFileSystem::new_with_opaque_prefixes(
1229+
primary,
1230+
[secondary],
1231+
vec![PathBuf::from("/app/wp-content")],
1232+
);
1233+
1234+
let entries: Vec<_> = overlay
1235+
.read_dir(Path::new("/app/wp-content"))
1236+
.unwrap()
1237+
.map(|entry| entry.unwrap().path)
1238+
.collect();
1239+
1240+
assert_eq!(entries, vec![PathBuf::from("/app/wp-content/host.txt")]);
1241+
assert_eq!(
1242+
overlay
1243+
.metadata(Path::new("/app/wp-content/themes"))
1244+
.unwrap_err(),
1245+
FsError::EntryNotFound
1246+
);
1247+
}
1248+
1249+
#[test]
1250+
fn overlay_opaque_prefix_prevents_parent_copy_up_on_create() {
1251+
let primary = MemFS::default();
1252+
let secondary = MemFS::default();
1253+
1254+
ops::create_dir_all(&secondary, "/app/wp-content/themes").unwrap();
1255+
1256+
let overlay = OverlayFileSystem::new_with_opaque_prefixes(
1257+
primary,
1258+
[secondary],
1259+
vec![PathBuf::from("/app/wp-content")],
1260+
);
1261+
1262+
let err = overlay
1263+
.new_open_options()
1264+
.create(true)
1265+
.write(true)
1266+
.open("/app/wp-content/themes/foo.txt")
1267+
.unwrap_err();
1268+
assert_eq!(err, FsError::EntryNotFound);
1269+
1270+
assert_eq!(
1271+
overlay
1272+
.primary()
1273+
.metadata(Path::new("/app/wp-content/themes"))
1274+
.unwrap_err(),
1275+
FsError::EntryNotFound
1276+
);
1277+
}
1278+
11721279
#[tokio::test]
11731280
async fn remove_directory() {
11741281
let primary = MemFS::default();

lib/wasix/src/runners/wasi_common.rs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,24 @@ fn prepare_filesystem(
203203
mounted_dirs: &[MountedDirectory],
204204
container_fs: Option<UnionFileSystem>,
205205
) -> Result<WasiFsRoot, Error> {
206+
let opaque_prefixes = mounted_dirs
207+
.iter()
208+
.map(|dir| {
209+
let mut guest_path = PathBuf::from(&dir.guest);
210+
if guest_path.is_relative() {
211+
guest_path = apply_relative_path_mounting_hack(&guest_path);
212+
}
213+
root_fs
214+
.canonicalize_unchecked(&guest_path)
215+
.with_context(|| {
216+
format!(
217+
"Unable to canonicalize guest path '{}'",
218+
guest_path.display()
219+
)
220+
})
221+
})
222+
.collect::<Result<Vec<_>, Error>>()?;
223+
206224
if !mounted_dirs.is_empty() {
207225
build_directory_mappings(&mut root_fs, mounted_dirs)?;
208226
}
@@ -218,7 +236,7 @@ fn prepare_filesystem(
218236

219237
let fs = if let Some(container) = container_fs {
220238
let container = RelativeOrAbsolutePathHack(container);
221-
let fs = OverlayFileSystem::new(root_fs, [container]);
239+
let fs = OverlayFileSystem::new_with_opaque_prefixes(root_fs, [container], opaque_prefixes);
222240
WasiFsRoot::Overlay(Arc::new(fs))
223241
} else {
224242
WasiFsRoot::Sandbox(root_fs)
@@ -430,6 +448,43 @@ mod tests {
430448
}
431449
}
432450

451+
#[tokio::test]
452+
#[cfg_attr(not(feature = "host-fs"), ignore)]
453+
async fn mapped_directory_replaces_container_path() {
454+
let temp = TempDir::new().unwrap();
455+
let mapping = [MountedDirectory::from(MappedDirectory {
456+
guest: "/app/wp-content".to_string(),
457+
host: temp.path().to_path_buf(),
458+
})];
459+
460+
let container = wasmer_package::utils::from_bytes(PYTHON).unwrap();
461+
let webc_fs = virtual_fs::WebcVolumeFileSystem::mount_all(&container);
462+
let union_fs = UnionFileSystem::new();
463+
union_fs
464+
.mount("webc".to_string(), Path::new("/"), Box::new(webc_fs))
465+
.unwrap();
466+
467+
let root_fs = RootFileSystemBuilder::default().build();
468+
let fs = prepare_filesystem(root_fs, &mapping, Some(union_fs)).unwrap();
469+
470+
assert!(matches!(fs, WasiFsRoot::Overlay(_)));
471+
if let WasiFsRoot::Overlay(overlay_fs) = &fs {
472+
use virtual_fs::FileSystem;
473+
assert!(
474+
overlay_fs
475+
.metadata("/app/wp-content".as_ref())
476+
.unwrap()
477+
.is_dir()
478+
);
479+
assert_eq!(
480+
overlay_fs
481+
.metadata("/app/wp-content/themes".as_ref())
482+
.unwrap_err(),
483+
virtual_fs::FsError::EntryNotFound
484+
);
485+
}
486+
}
487+
433488
#[tokio::test]
434489
#[cfg_attr(not(feature = "host-fs"), ignore)]
435490
async fn convert_mapped_directory_to_mounted_directory() {

0 commit comments

Comments
 (0)