Skip to content

[24.0.4] backport fd_renumber fixes #11278

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
12 changes: 12 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## 24.0.4

Released 2025-07-18.

### Fixed

* Fix a panic in the host caused by preview1 guests using `fd_renumber`.
[CVE-2025-53901](https://github.com/bytecodealliance/wasmtime/security/advisories/GHSA-fm79-3f68-h2fc).

* Fix a panic in the preview1 adapter caused by guests using `fd_renumber`.
[#11277](https://github.com/bytecodealliance/wasmtime/pull/11277)

## 24.0.3

Released 2025-06-24.
Expand Down
40 changes: 40 additions & 0 deletions crates/test-programs/src/bin/preview1_renumber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,46 @@ unsafe fn test_renumber(dir_fd: wasi::Fd) {
);

wasi::fd_close(fd_to).expect("closing a file");

wasi::fd_renumber(0, 0).expect("renumbering 0 to 0");
let fd_file3 = wasi::path_open(
dir_fd,
0,
"file3",
wasi::OFLAGS_CREAT,
wasi::RIGHTS_FD_READ | wasi::RIGHTS_FD_WRITE,
0,
0,
)
.expect("opening a file");
assert!(
fd_file3 > libc::STDERR_FILENO as wasi::Fd,
"file descriptor range check",
);

wasi::fd_renumber(fd_file3, 127).expect("renumbering FD to 127");
match wasi::fd_renumber(127, u32::MAX) {
Err(wasi::ERRNO_NOMEM) => {
// The preview1 adapter cannot handle more than 128 descriptors
eprintln!("fd_renumber({fd_file3}, {}) returned NOMEM", u32::MAX)
}
res => res.expect("renumbering FD to `u32::MAX`"),
}

let fd_file4 = wasi::path_open(
dir_fd,
0,
"file4",
wasi::OFLAGS_CREAT,
wasi::RIGHTS_FD_READ | wasi::RIGHTS_FD_WRITE,
0,
0,
)
.expect("opening a file");
assert!(
fd_file4 > libc::STDERR_FILENO as wasi::Fd,
"file descriptor range check",
);
}

fn main() {
Expand Down
30 changes: 17 additions & 13 deletions crates/wasi-preview1-component-adapter/src/descriptors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,8 @@ impl Descriptors {
.ok_or(wasi::ERRNO_BADF)
}

// Internal: close a fd, returning the descriptor.
fn close_(&mut self, fd: Fd) -> Result<Descriptor, Errno> {
// Close an fd.
pub fn close(&mut self, fd: Fd) -> Result<(), Errno> {
// Throw an error if closing an fd which is already closed
match self.get(fd)? {
Descriptor::Closed(_) => Err(wasi::ERRNO_BADF)?,
Expand All @@ -337,12 +337,7 @@ impl Descriptors {
let last_closed = self.closed;
let prev = std::mem::replace(self.get_mut(fd)?, Descriptor::Closed(last_closed));
self.closed = Some(fd);
Ok(prev)
}

// Close an fd.
pub fn close(&mut self, fd: Fd) -> Result<(), Errno> {
drop(self.close_(fd)?);
drop(prev);
Ok(())
}

Expand All @@ -362,11 +357,20 @@ impl Descriptors {
while self.table_len.get() as u32 <= to_fd {
self.push_closed()?;
}
// Then, close from_fd and put its contents into to_fd:
let desc = self.close_(from_fd)?;
// TODO FIXME if this overwrites a preopen, do we need to clear it from the preopen table?
*self.get_mut(to_fd)? = desc;

// Throw an error if renumbering a closed fd
match self.get(from_fd)? {
Descriptor::Closed(_) => Err(wasi::ERRNO_BADF)?,
_ => {}
}
// Close from_fd and put its contents into to_fd
if from_fd != to_fd {
// Mutate the descriptor to be closed, and push the closed fd onto the head of the linked list:
let last_closed = self.closed;
let desc = std::mem::replace(self.get_mut(from_fd)?, Descriptor::Closed(last_closed));
self.closed = Some(from_fd);
// TODO FIXME if this overwrites a preopen, do we need to clear it from the preopen table?
*self.get_mut(to_fd)? = desc;
}
Ok(())
}

Expand Down
72 changes: 30 additions & 42 deletions crates/wasi/src/preview1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,8 @@ use crate::{
FsError, IsATTY, ResourceTable, StreamError, StreamResult, WasiCtx, WasiImpl, WasiView,
};
use anyhow::{bail, Context};
use std::collections::{BTreeMap, HashSet};
use std::collections::{btree_map, BTreeMap, BTreeSet, HashSet};
use std::mem::{self, size_of, size_of_val};
use std::ops::{Deref, DerefMut};
use std::slice;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
Expand Down Expand Up @@ -318,21 +317,7 @@ struct WasiPreview1Adapter {
#[derive(Debug, Default)]
struct Descriptors {
used: BTreeMap<u32, Descriptor>,
free: Vec<u32>,
}

impl Deref for Descriptors {
type Target = BTreeMap<u32, Descriptor>;

fn deref(&self) -> &Self::Target {
&self.used
}
}

impl DerefMut for Descriptors {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.used
}
free: BTreeSet<u32>,
}

impl Descriptors {
Expand Down Expand Up @@ -409,42 +394,34 @@ impl Descriptors {

/// Returns next descriptor number, which was never assigned
fn unused(&self) -> Result<u32> {
match self.last_key_value() {
match self.used.last_key_value() {
Some((fd, _)) => {
if let Some(fd) = fd.checked_add(1) {
return Ok(fd);
}
if self.len() == u32::MAX as usize {
if self.used.len() == u32::MAX as usize {
return Err(types::Errno::Loop.into());
}
// TODO: Optimize
Ok((0..u32::MAX)
.rev()
.find(|fd| !self.contains_key(fd))
.find(|fd| !self.used.contains_key(fd))
.expect("failed to find an unused file descriptor"))
}
None => Ok(0),
}
}

/// Removes the [Descriptor] corresponding to `fd`
fn remove(&mut self, fd: types::Fd) -> Option<Descriptor> {
let fd = fd.into();
let desc = self.used.remove(&fd)?;
self.free.push(fd);
Some(desc)
}

/// Pushes the [Descriptor] returning corresponding number.
/// This operation will try to reuse numbers previously removed via [`Self::remove`]
/// and rely on [`Self::unused`] if no free numbers are recorded
fn push(&mut self, desc: Descriptor) -> Result<u32> {
let fd = if let Some(fd) = self.free.pop() {
let fd = if let Some(fd) = self.free.pop_last() {
fd
} else {
self.unused()?
};
assert!(self.insert(fd, desc).is_none());
assert!(self.used.insert(fd, desc).is_none());
Ok(fd)
}
}
Expand Down Expand Up @@ -485,15 +462,15 @@ impl Transaction<'_> {
/// Returns [`types::Errno::Badf`] if no [`Descriptor`] is found
fn get_descriptor(&self, fd: types::Fd) -> Result<&Descriptor> {
let fd = fd.into();
let desc = self.descriptors.get(&fd).ok_or(types::Errno::Badf)?;
let desc = self.descriptors.used.get(&fd).ok_or(types::Errno::Badf)?;
Ok(desc)
}

/// Borrows [`File`] corresponding to `fd`
/// if it describes a [`Descriptor::File`]
fn get_file(&self, fd: types::Fd) -> Result<&File> {
let fd = fd.into();
match self.descriptors.get(&fd) {
match self.descriptors.used.get(&fd) {
Some(Descriptor::File(file)) => Ok(file),
_ => Err(types::Errno::Badf.into()),
}
Expand All @@ -503,7 +480,7 @@ impl Transaction<'_> {
/// if it describes a [`Descriptor::File`]
fn get_file_mut(&mut self, fd: types::Fd) -> Result<&mut File> {
let fd = fd.into();
match self.descriptors.get_mut(&fd) {
match self.descriptors.used.get_mut(&fd) {
Some(Descriptor::File(file)) => Ok(file),
_ => Err(types::Errno::Badf.into()),
}
Expand All @@ -517,7 +494,7 @@ impl Transaction<'_> {
/// Returns [`types::Errno::Spipe`] if the descriptor corresponds to stdio
fn get_seekable(&self, fd: types::Fd) -> Result<&File> {
let fd = fd.into();
match self.descriptors.get(&fd) {
match self.descriptors.used.get(&fd) {
Some(Descriptor::File(file)) => Ok(file),
Some(
Descriptor::Stdin { .. } | Descriptor::Stdout { .. } | Descriptor::Stderr { .. },
Expand Down Expand Up @@ -550,7 +527,7 @@ impl Transaction<'_> {
/// if it describes a [`Descriptor::Directory`]
fn get_dir_fd(&self, fd: types::Fd) -> Result<Resource<filesystem::Descriptor>> {
let fd = fd.into();
match self.descriptors.get(&fd) {
match self.descriptors.used.get(&fd) {
Some(Descriptor::Directory { fd, .. }) => Ok(fd.borrowed()),
_ => Err(types::Errno::Badf.into()),
}
Expand Down Expand Up @@ -1360,11 +1337,13 @@ impl wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiP1Ctx {
_memory: &mut GuestMemory<'_>,
fd: types::Fd,
) -> Result<(), types::Error> {
let desc = self
.transact()?
.descriptors
.remove(fd)
.ok_or(types::Errno::Badf)?;
let desc = {
let fd = fd.into();
let mut st = self.transact()?;
let desc = st.descriptors.used.remove(&fd).ok_or(types::Errno::Badf)?;
st.descriptors.free.insert(fd);
desc
};
match desc {
Descriptor::Stdin { stream, .. } => {
streams::HostInputStream::drop(&mut self.as_wasi_impl(), stream)
Expand Down Expand Up @@ -1900,8 +1879,17 @@ impl wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiP1Ctx {
to: types::Fd,
) -> Result<(), types::Error> {
let mut st = self.transact()?;
let desc = st.descriptors.remove(from).ok_or(types::Errno::Badf)?;
st.descriptors.insert(to.into(), desc);
let from = from.into();
let btree_map::Entry::Occupied(desc) = st.descriptors.used.entry(from) else {
return Err(types::Errno::Badf.into());
};
let to = to.into();
if from != to {
let desc = desc.remove();
st.descriptors.free.insert(from);
st.descriptors.free.remove(&to);
st.descriptors.used.insert(to, desc);
}
Ok(())
}

Expand Down
Loading