Skip to content

Commit 09226cf

Browse files
committed
fusedev: end the connection when a uring request handler panics
A panic in a request handler unwound the uring worker thread, which dropped its ring entries while the kernel still held them and could copy the next request into the freed memory. The worker's queues were left without entries, so requests from those CPUs waited forever, and so did the request that panicked. The worker now catches the panic and stops as it does on an error: it ends the connection through the fusectl abort file, waits for the kernel to give the entries back, and then lets the panic go on. It logs the panic first, because nothing joins the worker thread's panic. A new test serves a file system whose LOOKUP panics, and checks that the lookup fails and FuseServing::wait() returns. Signed-off-by: Ilan Joselevich <personal@ilanjoselevich.com>
1 parent 94ce98c commit 09226cf

2 files changed

Lines changed: 94 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@
9292
its fusectl `abort` file. Its queues had no entries left, so requests from those CPUs waited
9393
forever, and so did requests it had taken but not answered. Now they fail, the other workers
9494
stop, and `FuseServing::wait()` returns.
95+
- A `UringFuseServing` worker whose request handler panics ends the connection the same way and
96+
waits for the kernel to give its ring entries back before the panic goes on. Before, unwinding
97+
freed entries the kernel could still copy a request into.
9598
- A `UringFuseServing` worker that stops frees its ring entries only after the kernel has given
9699
them all back, and leaks them if that takes more than a second. A worker that stopped on an
97100
error while the connection was up freed entries the kernel could still copy requests into. A

‎crates/fuse-backend-fusedev/src/uring_session.rs‎

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,9 @@ impl<F: FileSystem + Send + Sync + 'static> UringWorker<F> {
707707
registered?;
708708
// The number of entries whose command the kernel hasn't completed.
709709
let mut armed = entries.len();
710+
// A handler that panics stops the worker like an error, and the
711+
// panic goes on once the kernel has given the entries back.
712+
let mut panic = None;
710713

711714
let result = 'serve: loop {
712715
if let Err(e) = submit_and_wait(&mut ring, 1) {
@@ -743,9 +746,17 @@ impl<F: FileSystem + Send + Sync + 'static> UringWorker<F> {
743746
break 'serve Err(io::Error::from_raw_os_error(-res));
744747
}
745748

746-
if let Err(e) = self.process(entry) {
747-
warn!("uring worker: failed to handle request: {e}");
748-
entry.stage_error_reply(libc::EIO);
749+
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.process(entry)))
750+
{
751+
Ok(Ok(())) => {}
752+
Ok(Err(e)) => {
753+
warn!("uring worker: failed to handle request: {e}");
754+
entry.stage_error_reply(libc::EIO);
755+
}
756+
Err(payload) => {
757+
panic = Some(payload);
758+
break 'serve Err(io::Error::other("a request handler panicked"));
759+
}
749760
}
750761

751762
let ent: FuseUringEntInOut = entry.header().ring_ent_in_out;
@@ -783,6 +794,10 @@ impl<F: FileSystem + Send + Sync + 'static> UringWorker<F> {
783794
self.abort.abort();
784795
}
785796
release_entries(ring, entries, armed);
797+
if let Some(payload) = panic {
798+
error!("uring worker: a request handler panicked");
799+
std::panic::resume_unwind(payload);
800+
}
786801
result
787802
}
788803

@@ -1777,6 +1792,79 @@ mod tests {
17771792
assert_eq!(mount_minor(info, "/tmp/a b".as_ref()), Some(88));
17781793
}
17791794

1795+
/// A file system whose LOOKUP panics.
1796+
struct PanicFs;
1797+
1798+
impl FileSystem for PanicFs {
1799+
type Inode = u64;
1800+
type Handle = u64;
1801+
1802+
fn lookup(
1803+
&self,
1804+
_ctx: &fuse_backend_core::api::filesystem::Context,
1805+
_parent: u64,
1806+
_name: &std::ffi::CStr,
1807+
) -> io::Result<fuse_backend_core::api::filesystem::Entry> {
1808+
panic!("LOOKUP panics");
1809+
}
1810+
1811+
fn getattr(
1812+
&self,
1813+
_ctx: &fuse_backend_core::api::filesystem::Context,
1814+
inode: u64,
1815+
_handle: Option<u64>,
1816+
) -> io::Result<(fuse_backend_core::abi::fuse_abi::stat64, Duration)> {
1817+
// Safe because stat64 is plain data.
1818+
let mut st: fuse_backend_core::abi::fuse_abi::stat64 = unsafe { std::mem::zeroed() };
1819+
st.st_ino = inode;
1820+
st.st_nlink = 1;
1821+
st.st_mode = libc::S_IFDIR | 0o755;
1822+
Ok((st, Duration::ZERO))
1823+
}
1824+
}
1825+
1826+
/// A worker whose handler panics ends the connection, so the request
1827+
/// fails instead of waiting forever, and `wait()` returns.
1828+
#[test]
1829+
fn test_uring_serving_handler_panic() {
1830+
let mnt = TempDir::new().unwrap();
1831+
let server = Arc::new(Server::new(PanicFs));
1832+
server.set_uring(true);
1833+
let mut se = FuseSession::new(mnt.as_path(), "uring_panic_test", "", false).unwrap();
1834+
se.mount().unwrap();
1835+
if ConnectionAbort::new(se.mountpoint()).path.is_none() {
1836+
eprintln!("fusectl isn't mounted, skipping");
1837+
return;
1838+
}
1839+
let cfg = UringConfig {
1840+
workers: 1,
1841+
entries_per_queue: 1,
1842+
};
1843+
let serving = match UringFuseServing::new(se, server, cfg) {
1844+
Ok(serving) => Arc::new(serving),
1845+
Err(UringNotSupported) => {
1846+
eprintln!("FUSE-over-io_uring is not available, skipping");
1847+
return;
1848+
}
1849+
Err(e) => panic!("uring serving failed: {}", e),
1850+
};
1851+
1852+
let (tx, rx) = channel();
1853+
let path = mnt.as_path().join("panics");
1854+
std::thread::spawn(move || {
1855+
let _ = tx.send(std::fs::metadata(path).is_err());
1856+
});
1857+
assert_eq!(rx.recv_timeout(Duration::from_secs(5)), Ok(true));
1858+
let (tx, rx) = channel();
1859+
let waiting = serving.clone();
1860+
std::thread::spawn(move || {
1861+
waiting.wait();
1862+
let _ = tx.send(());
1863+
});
1864+
rx.recv_timeout(Duration::from_secs(5))
1865+
.expect("the worker didn't end the connection after the panic");
1866+
}
1867+
17801868
/// Ending the connection, as a failed worker does, stops every worker,
17811869
/// so `wait()` returns instead of serving on with a hanging queue.
17821870
#[test]

0 commit comments

Comments
 (0)