Skip to content
Open
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
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ pin-project-lite = { workspace = true, optional = true }

[target.'cfg(unix)'.dependencies]
rustix = { workspace = true, features = ["mm", "process"] }
libc = { workspace = true, optional = true }

[dev-dependencies]
env_logger = { workspace = true }
Expand Down Expand Up @@ -617,6 +618,7 @@ serve = [
"dep:http-body-util",
"dep:http",
"dep:pin-project-lite",
"dep:libc",
"wasmtime-cli-flags/async",
"wasmtime-wasi-http?/p2",
]
Expand Down
5 changes: 5 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ Unreleased.

### Added

- Add `--listenfd` option to `wasmtime serve`, which allows launching wasmtime
with sockets inherited from a service manager (e.g. systemd socket units).

### Changed

- Remove non-functional `listenfd` WASI CLI option.

--------------------------------------------------------------------------------

Release notes for previous releases of Wasmtime can be found on the respective
Expand Down
4 changes: 0 additions & 4 deletions crates/cli-flags/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -516,10 +516,6 @@ wasmtime_option_group! {
pub config: Option<bool>,
/// Enable support for WASI key-value imports (experimental)
pub keyvalue: Option<bool>,
/// Inherit environment variables and file descriptors following the
/// systemd listen fd specification (UNIX only) (legacy wasip1
/// implementation only)
pub listenfd: Option<bool>,
/// Grant access to the given TCP listen socket (experimental, legacy
/// wasip1 implementation only)
#[serde(default)]
Expand Down
13 changes: 13 additions & 0 deletions src/bin/wasmtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,22 @@ impl CompletionCommand {

#[allow(unreachable_code, reason = "empty enum with all features disabled")]
fn main() -> Result<()> {
setup();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I would prefer to keep all the serve.rs-related code in serve.rs and avoid extra abstractions here (which have more #[cfg] which is more to validate, etc). I think it'd be fine to have init_inherited_fds marked unsafe, that returns the inherited sockets, and the // SAFETY ... comment on the call in serve.rs is high enough in the function that it's clear that nothing happens inbetween. That should keep everything contained without the need for more #[cfg] without compromising on safety.


return Wasmtime::parse().execute();
}

#[cfg(all(unix, feature = "serve"))]
fn setup() {
unsafe {
// Safety: This is called first in main
wasmtime_cli::init_inherited_fds()
}
}

#[cfg(not(all(unix, feature = "serve")))]
fn setup() {}

#[test]
fn verify_cli() {
use clap::CommandFactory;
Expand Down
80 changes: 62 additions & 18 deletions src/commands/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use std::{
time::{Duration, Instant},
};
use tokio::io::{self, AsyncWrite};
use tokio::net::TcpListener;
use tokio::sync::{Notify, Semaphore};
use wasmtime::component::{Component, GuestTaskId, Linker};
use wasmtime::error::Context as _;
Expand Down Expand Up @@ -121,6 +122,11 @@ pub struct ServeCommand {
#[arg(long)]
no_logging_prefix: bool,

/// Use sockets passed via the 'LISTEN_FDS' environment variable (set e.g. by systemd when
/// launching a service from socket units). Not available on Windows.
#[arg(long)]
listenfd: bool,
Comment thread
simolus3 marked this conversation as resolved.
Comment thread
simolus3 marked this conversation as resolved.

/// The WebAssembly component to run.
#[arg(value_name = "WASM", required = true)]
component: PathBuf,
Expand Down Expand Up @@ -615,25 +621,41 @@ impl ServeCommand {
});
}

let socket = match &self.addr {
SocketAddr::V4(_) => tokio::net::TcpSocket::new_v4()?,
SocketAddr::V6(_) => tokio::net::TcpSocket::new_v6()?,
let inherited_socket = if self.listenfd {
Self::inherit_socket().with_context(|| "Resolve inherited socket")?
} else {
None
};

let listener = match inherited_socket {
Some(listener) => {
eprintln!("Serving HTTP on inherited socket");
log::info!("Listening on inherited socket");

listener
}
None => {
let socket = match &self.addr {
SocketAddr::V4(_) => tokio::net::TcpSocket::new_v4()?,
SocketAddr::V6(_) => tokio::net::TcpSocket::new_v6()?,
};
// Conditionally enable `SO_REUSEADDR` depending on the current
// platform. On Unix we want this to be able to rebind an address in
// the `TIME_WAIT` state which can happen then a server is killed with
// active TCP connections and then restarted. On Windows though if
// `SO_REUSEADDR` is specified then it enables multiple applications to
// bind the port at the same time which is not something we want. Hence
// this is conditionally set based on the platform (and deviates from
// Tokio's default from always-on).
socket.set_reuseaddr(!cfg!(windows))?;
socket.bind(self.addr)?;
let listener = socket.listen(100)?;

eprintln!("Serving HTTP on http://{}/", listener.local_addr()?);
log::info!("Listening on {}", self.addr);
listener
}
};
// Conditionally enable `SO_REUSEADDR` depending on the current
// platform. On Unix we want this to be able to rebind an address in
// the `TIME_WAIT` state which can happen then a server is killed with
// active TCP connections and then restarted. On Windows though if
// `SO_REUSEADDR` is specified then it enables multiple applications to
// bind the port at the same time which is not something we want. Hence
// this is conditionally set based on the platform (and deviates from
// Tokio's default from always-on).
socket.set_reuseaddr(!cfg!(windows))?;
socket.bind(self.addr)?;
let listener = socket.listen(100)?;

eprintln!("Serving HTTP on http://{}/", listener.local_addr()?);

log::info!("Listening on {}", self.addr);

let epoch_interval = if let Some(Profile::Guest { interval, .. }) = self.run.profile {
Some(interval)
Expand Down Expand Up @@ -753,6 +775,28 @@ impl ServeCommand {

Ok(())
}

#[cfg(unix)]
fn inherit_socket() -> Result<Option<TcpListener>> {
use crate::inherited_fd::{InheritedFileDescriptor, take_inherited_fds};

let sockets = take_inherited_fds();
let Some(inherited_socket) = ({
sockets.into_iter().find_map(|fd| match fd {
InheritedFileDescriptor::TcpSocket(listener) => Some(listener),
})
}) else {
return Ok(None);
};

inherited_socket.set_nonblocking(true)?;
Ok(Some(TcpListener::from_std(inherited_socket)?))
}

#[cfg(not(unix))]
fn inherit_socket() -> Result<Option<TcpListener>> {
bail!("The --listenfd option is not available on Windows")
}
}

pin_project! {
Expand Down
3 changes: 0 additions & 3 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,9 +356,6 @@ impl RunCommon {
builder.initial_cwd(cwd);
}

if self.common.wasi.listenfd == Some(true) {
bail!("components do not support --listenfd");
}
for _ in self.compute_preopen_sockets()? {
bail!("components do not support --tcplisten");
}
Expand Down
121 changes: 121 additions & 0 deletions src/inherited_fd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use std::env;
use std::mem::{self, MaybeUninit};
use std::os::fd::AsRawFd;
use std::os::fd::FromRawFd;
use std::os::fd::RawFd;
use std::process;
use std::{
net::TcpListener,
os::fd::OwnedFd,
sync::{Mutex, OnceLock},
};

/// A file descriptor the current process has inherited from a parent process.
pub enum InheritedFileDescriptor {
/// An inherited socket verified to be a TCP socket.
TcpSocket(TcpListener),
}

static INHERITED_FDS: OnceLock<Mutex<Vec<InheritedFileDescriptor>>> = OnceLock::new();

/// Takes ownership of file descriptors this process has inherited from a parent process like a
/// service manager.
///
/// These are looked up with the [protocol from systemd](https://www.freedesktop.org/software/systemd/man/latest/sd_listen_fds.html#Notes).
/// This is used to implement socket activation for `wasmtime serve`.
///
/// # Safety
///
/// This must be called first in `main()`, before the file descriptors can be used by anything
/// else.
pub unsafe fn init_inherited_fds() {
// The logic here is taken from https://github.com/systemd/systemd/blob/main/src/libsystemd/sd-daemon/sd-daemon.c,
// the protocol is described in the "Notes" section of https://www.freedesktop.org/software/systemd/man/latest/sd_listen_fds.html#Notes.

if !env::var("LISTEN_PID")
.ok()
.and_then(|pid| pid.parse().ok())
.is_some_and(|pid: u32| pid == process::id())
{
// Not meant for this process, ignore.
return;
}

let Some(num_fds) = env::var("LISTEN_FDS").ok().and_then(|fds| fds.parse().ok()) else {
return;
};

let first_fd: RawFd = 3;
let Some(last_fd) = first_fd.checked_add(num_fds) else {
return;
};

let mut descriptors = Vec::with_capacity(num_fds as usize);
for fd in first_fd..last_fd {
let fd = unsafe {
// Safety: This is called first in main and we checked the PID, so we have exclusive
// access to this fd.
libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this use rustix::io::fcntl_setfd with error handling?

OwnedFd::from_raw_fd(fd)
};

if is_tcp_socket(&fd) {
descriptors.push(InheritedFileDescriptor::TcpSocket(fd.into()));
}
}

let _ = INHERITED_FDS.set(Mutex::new(descriptors));
}

/// Consumes inherited file descriptors obtained through [init_inherited_fds].
pub fn take_inherited_fds() -> Vec<InheritedFileDescriptor> {
let Some(fds) = INHERITED_FDS.get() else {
return Default::default();
};

let mut guard = fds.lock().unwrap();
return mem::take(&mut *guard);
}

fn is_tcp_socket(fd: &OwnedFd) -> bool {
let mut stat: MaybeUninit<libc::stat> = MaybeUninit::uninit();
if unsafe { libc::fstat(fd.as_raw_fd(), stat.as_mut_ptr()) } != 0 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this use rustix::fs::fstat for safe bindings?

return false;
}

if (unsafe { stat.assume_init() }.st_mode & libc::S_IFMT) != libc::S_IFSOCK {
return false;
}

let sa_family = unsafe {
let mut sockaddr: MaybeUninit<libc::sockaddr> = MaybeUninit::uninit();
let mut len = mem::size_of::<libc::sockaddr>() as libc::c_uint;

if libc::getsockname(fd.as_raw_fd(), sockaddr.as_mut_ptr(), &mut len) != 0 {
return false;
}
sockaddr.assume_init().sa_family
} as libc::c_int;
Comment on lines +90 to +98

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this use rustix::net::getsockname for a safe alternative?


if sa_family != libc::AF_INET && sa_family != libc::AF_INET6 {
return false;
}

let mut socket_type: libc::c_int = 0;
unsafe {
let mut type_len = mem::size_of_val(&socket_type) as libc::c_uint;

if libc::getsockopt(
fd.as_raw_fd(),
libc::SOL_SOCKET,
libc::SO_TYPE,
std::ptr::from_mut(&mut socket_type).cast(),
&mut type_len,
) != 0
{
return false;
}
}

socket_type == libc::SOCK_STREAM
Comment on lines +104 to +120

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this use rustix::net::sockopt::socket_type for a safe alternative?

}
6 changes: 6 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ pub(crate) mod common;

#[cfg(any(feature = "objdump", all(feature = "hot-blocks", target_os = "linux")))]
pub(crate) mod disas;

#[cfg(all(unix, feature = "serve"))]
pub(crate) mod inherited_fd;

#[cfg(all(unix, feature = "serve"))]
pub use inherited_fd::init_inherited_fds;
Loading