Skip to content

Commit 0c93530

Browse files
authored
fix(wasip1): fd_renumber panic in the host (#11276)
* fix(wasip1): prevent duplicate FD usage The implementation assumed that only the runtime could ever issue FDs, however that's not the case in p1, where guests can choose arbitrary FDs to use (e.g. via `fd_renumber`). Due to incorrect accounting, guests could "mark" arbitrary FDs as "free" and trigger a panic in the host by requesting a new FD. Signed-off-by: Roman Volosatovs <[email protected]> * test(wasip1): expand `fd_renumber` test Signed-off-by: Roman Volosatovs <[email protected]> * doc: add release notes Signed-off-by: Roman Volosatovs <[email protected]> * test(wasip1): ignore `fd_renumber` tests when using adapter Signed-off-by: Roman Volosatovs <[email protected]> * refactor(wasip1): do not modify descriptors on `fd_renumber(n, n)` Since `remove` is now only used once, remove it. As a sideffect, this makes the implementation more explicit . Signed-off-by: Roman Volosatovs <[email protected]> * doc: reference the CVE prtest:full Signed-off-by: Roman Volosatovs <[email protected]> --------- Signed-off-by: Roman Volosatovs <[email protected]>
1 parent a5ed9fb commit 0c93530

File tree

5 files changed

+69
-42
lines changed

5 files changed

+69
-42
lines changed

RELEASES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ Unreleased.
66

77
### Changed
88

9+
### Fixed
10+
11+
* Fix a panic in the host caused by preview1 guests using `fd_renumber`.
12+
[CVE-2025-53901](https://github.com/bytecodealliance/wasmtime/security/advisories/GHSA-fm79-3f68-h2fc).
13+
914
--------------------------------------------------------------------------------
1015

1116
Release notes for previous releases of Wasmtime can be found on the respective

crates/test-programs/src/bin/preview1_renumber.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,38 @@ unsafe fn test_renumber(dir_fd: wasip1::Fd) {
7474
);
7575

7676
wasip1::fd_close(fd_to).expect("closing a file");
77+
78+
wasip1::fd_renumber(0, 0).expect("renumbering 0 to 0");
79+
let fd_file3 = wasip1::path_open(
80+
dir_fd,
81+
0,
82+
"file3",
83+
wasip1::OFLAGS_CREAT,
84+
wasip1::RIGHTS_FD_READ | wasip1::RIGHTS_FD_WRITE,
85+
0,
86+
0,
87+
)
88+
.expect("opening a file");
89+
assert!(
90+
fd_file3 > libc::STDERR_FILENO as wasip1::Fd,
91+
"file descriptor range check",
92+
);
93+
94+
wasip1::fd_renumber(fd_file3, u32::MAX).expect("renumbering file FD to `u32::MAX`");
95+
let fd_file4 = wasip1::path_open(
96+
dir_fd,
97+
0,
98+
"file4",
99+
wasip1::OFLAGS_CREAT,
100+
wasip1::RIGHTS_FD_READ | wasip1::RIGHTS_FD_WRITE,
101+
0,
102+
0,
103+
)
104+
.expect("opening a file");
105+
assert!(
106+
fd_file4 > libc::STDERR_FILENO as wasip1::Fd,
107+
"file descriptor range check",
108+
);
77109
}
78110

79111
fn main() {

crates/wasi/src/preview1.rs

Lines changed: 30 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,8 @@ use crate::p2::bindings::{
7474
};
7575
use crate::p2::{FsError, IsATTY, WasiCtx, WasiImpl, WasiView};
7676
use anyhow::{Context, bail};
77-
use std::collections::{BTreeMap, HashSet};
77+
use std::collections::{BTreeMap, BTreeSet, HashSet, btree_map};
7878
use std::mem::{self, size_of, size_of_val};
79-
use std::ops::{Deref, DerefMut};
8079
use std::slice;
8180
use std::sync::Arc;
8281
use std::sync::atomic::{AtomicU64, Ordering};
@@ -286,21 +285,7 @@ struct WasiPreview1Adapter {
286285
#[derive(Debug, Default)]
287286
struct Descriptors {
288287
used: BTreeMap<u32, Descriptor>,
289-
free: Vec<u32>,
290-
}
291-
292-
impl Deref for Descriptors {
293-
type Target = BTreeMap<u32, Descriptor>;
294-
295-
fn deref(&self) -> &Self::Target {
296-
&self.used
297-
}
298-
}
299-
300-
impl DerefMut for Descriptors {
301-
fn deref_mut(&mut self) -> &mut Self::Target {
302-
&mut self.used
303-
}
288+
free: BTreeSet<u32>,
304289
}
305290

306291
impl Descriptors {
@@ -377,42 +362,34 @@ impl Descriptors {
377362

378363
/// Returns next descriptor number, which was never assigned
379364
fn unused(&self) -> Result<u32> {
380-
match self.last_key_value() {
365+
match self.used.last_key_value() {
381366
Some((fd, _)) => {
382367
if let Some(fd) = fd.checked_add(1) {
383368
return Ok(fd);
384369
}
385-
if self.len() == u32::MAX as usize {
370+
if self.used.len() == u32::MAX as usize {
386371
return Err(types::Errno::Loop.into());
387372
}
388373
// TODO: Optimize
389374
Ok((0..u32::MAX)
390375
.rev()
391-
.find(|fd| !self.contains_key(fd))
376+
.find(|fd| !self.used.contains_key(fd))
392377
.expect("failed to find an unused file descriptor"))
393378
}
394379
None => Ok(0),
395380
}
396381
}
397382

398-
/// Removes the [Descriptor] corresponding to `fd`
399-
fn remove(&mut self, fd: types::Fd) -> Option<Descriptor> {
400-
let fd = fd.into();
401-
let desc = self.used.remove(&fd)?;
402-
self.free.push(fd);
403-
Some(desc)
404-
}
405-
406383
/// Pushes the [Descriptor] returning corresponding number.
407384
/// This operation will try to reuse numbers previously removed via [`Self::remove`]
408385
/// and rely on [`Self::unused`] if no free numbers are recorded
409386
fn push(&mut self, desc: Descriptor) -> Result<u32> {
410-
let fd = if let Some(fd) = self.free.pop() {
387+
let fd = if let Some(fd) = self.free.pop_last() {
411388
fd
412389
} else {
413390
self.unused()?
414391
};
415-
assert!(self.insert(fd, desc).is_none());
392+
assert!(self.used.insert(fd, desc).is_none());
416393
Ok(fd)
417394
}
418395
}
@@ -453,15 +430,15 @@ impl Transaction<'_> {
453430
/// Returns [`types::Errno::Badf`] if no [`Descriptor`] is found
454431
fn get_descriptor(&self, fd: types::Fd) -> Result<&Descriptor> {
455432
let fd = fd.into();
456-
let desc = self.descriptors.get(&fd).ok_or(types::Errno::Badf)?;
433+
let desc = self.descriptors.used.get(&fd).ok_or(types::Errno::Badf)?;
457434
Ok(desc)
458435
}
459436

460437
/// Borrows [`File`] corresponding to `fd`
461438
/// if it describes a [`Descriptor::File`]
462439
fn get_file(&self, fd: types::Fd) -> Result<&File> {
463440
let fd = fd.into();
464-
match self.descriptors.get(&fd) {
441+
match self.descriptors.used.get(&fd) {
465442
Some(Descriptor::File(file)) => Ok(file),
466443
_ => Err(types::Errno::Badf.into()),
467444
}
@@ -471,7 +448,7 @@ impl Transaction<'_> {
471448
/// if it describes a [`Descriptor::File`]
472449
fn get_file_mut(&mut self, fd: types::Fd) -> Result<&mut File> {
473450
let fd = fd.into();
474-
match self.descriptors.get_mut(&fd) {
451+
match self.descriptors.used.get_mut(&fd) {
475452
Some(Descriptor::File(file)) => Ok(file),
476453
_ => Err(types::Errno::Badf.into()),
477454
}
@@ -485,7 +462,7 @@ impl Transaction<'_> {
485462
/// Returns [`types::Errno::Spipe`] if the descriptor corresponds to stdio
486463
fn get_seekable(&self, fd: types::Fd) -> Result<&File> {
487464
let fd = fd.into();
488-
match self.descriptors.get(&fd) {
465+
match self.descriptors.used.get(&fd) {
489466
Some(Descriptor::File(file)) => Ok(file),
490467
Some(
491468
Descriptor::Stdin { .. } | Descriptor::Stdout { .. } | Descriptor::Stderr { .. },
@@ -518,7 +495,7 @@ impl Transaction<'_> {
518495
/// if it describes a [`Descriptor::Directory`]
519496
fn get_dir_fd(&self, fd: types::Fd) -> Result<Resource<filesystem::Descriptor>> {
520497
let fd = fd.into();
521-
match self.descriptors.get(&fd) {
498+
match self.descriptors.used.get(&fd) {
522499
Some(Descriptor::Directory { fd, .. }) => Ok(fd.borrowed()),
523500
_ => Err(types::Errno::Badf.into()),
524501
}
@@ -1327,11 +1304,13 @@ impl wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiP1Ctx {
13271304
_memory: &mut GuestMemory<'_>,
13281305
fd: types::Fd,
13291306
) -> Result<(), types::Error> {
1330-
let desc = self
1331-
.transact()?
1332-
.descriptors
1333-
.remove(fd)
1334-
.ok_or(types::Errno::Badf)?;
1307+
let desc = {
1308+
let fd = fd.into();
1309+
let mut st = self.transact()?;
1310+
let desc = st.descriptors.used.remove(&fd).ok_or(types::Errno::Badf)?;
1311+
st.descriptors.free.insert(fd);
1312+
desc
1313+
};
13351314
match desc {
13361315
Descriptor::Stdin { stream, .. } => {
13371316
streams::HostInputStream::drop(&mut self.as_io_impl(), stream)
@@ -1830,8 +1809,17 @@ impl wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiP1Ctx {
18301809
to: types::Fd,
18311810
) -> Result<(), types::Error> {
18321811
let mut st = self.transact()?;
1833-
let desc = st.descriptors.remove(from).ok_or(types::Errno::Badf)?;
1834-
st.descriptors.insert(to.into(), desc);
1812+
let from = from.into();
1813+
let btree_map::Entry::Occupied(desc) = st.descriptors.used.entry(from) else {
1814+
return Err(types::Errno::Badf.into());
1815+
};
1816+
let to = to.into();
1817+
if from != to {
1818+
let desc = desc.remove();
1819+
st.descriptors.free.insert(from);
1820+
st.descriptors.free.remove(&to);
1821+
st.descriptors.used.insert(to, desc);
1822+
}
18351823
Ok(())
18361824
}
18371825

crates/wasi/tests/all/p2/async_.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ async fn preview1_remove_nonempty_directory() {
231231
.await
232232
.unwrap()
233233
}
234+
#[ignore = "panics"]
234235
#[test_log::test(tokio::test(flavor = "multi_thread"))]
235236
async fn preview1_renumber() {
236237
run(PREVIEW1_RENUMBER_COMPONENT, false).await.unwrap()

crates/wasi/tests/all/p2/sync.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ fn preview1_remove_directory() {
187187
fn preview1_remove_nonempty_directory() {
188188
run(PREVIEW1_REMOVE_NONEMPTY_DIRECTORY_COMPONENT, false).unwrap()
189189
}
190+
#[ignore = "panics"]
190191
#[test_log::test]
191192
fn preview1_renumber() {
192193
run(PREVIEW1_RENUMBER_COMPONENT, false).unwrap()

0 commit comments

Comments
 (0)