Document unsafe contracts and stop reusing poisoned state
This commit is contained in:
1 parent
a7fcd69897
commit
f2e728b503
46 files changed
+377
-45
No files matched your search
@@ -55,6 +55,8 @@ fn stream(_: &()) -> impl futures::Stream<Item = String> + use<> {
|
||||
}
|
||||
|
||||
fn watch(send: tokio::sync::mpsc::Sender<String>, cancel: UnixStream) -> io::Result<()> {
|
||||
// SAFETY: timerfd_create takes scalar arguments; owned_fd adopts its result only after
|
||||
// checking success.
|
||||
let timer = owned_fd(unsafe {
|
||||
libc::timerfd_create(libc::CLOCK_REALTIME, libc::TFD_CLOEXEC | libc::TFD_NONBLOCK)
|
||||
})?;
|
||||
@@ -91,6 +93,8 @@ fn watch(send: tokio::sync::mpsc::Sender<String>, cancel: UnixStream) -> io::Res
|
||||
},
|
||||
];
|
||||
loop {
|
||||
// SAFETY: fds is writable for exactly fds.len() pollfd entries and remains alive
|
||||
// through poll.
|
||||
if unsafe { libc::poll(fds.as_mut_ptr(), fds.len() as _, -1) } >= 0 {
|
||||
break;
|
||||
}
|
||||
@@ -117,6 +121,8 @@ fn owned_fd(fd: libc::c_int) -> io::Result<OwnedFd> {
|
||||
if fd < 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
// SAFETY: Callers pass a newly created descriptor, checked above for success, and
|
||||
// transfer its sole ownership here.
|
||||
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
|
||||
}
|
||||
}
|
||||
@@ -133,6 +139,8 @@ fn arm(timer: &OwnedFd, deadline: i128) -> io::Result<bool> {
|
||||
},
|
||||
};
|
||||
let flags = libc::TFD_TIMER_ABSTIME | libc::TFD_TIMER_CANCEL_ON_SET;
|
||||
// SAFETY: timer owns the descriptor and spec is a fully initialized itimerspec; a null old-
|
||||
// value pointer is permitted.
|
||||
if unsafe { libc::timerfd_settime(timer.as_raw_fd(), flags, &spec, std::ptr::null_mut()) } < 0 {
|
||||
let error = io::Error::last_os_error();
|
||||
if error.raw_os_error() == Some(libc::ECANCELED) {
|
||||
@@ -148,6 +156,8 @@ fn arm(timer: &OwnedFd, deadline: i128) -> io::Result<bool> {
|
||||
|
||||
fn read_timer(timer: &OwnedFd) -> io::Result<()> {
|
||||
let mut expirations = 0u64;
|
||||
// SAFETY: expirations is writable for the eight bytes requested, and timer retains the
|
||||
// descriptor.
|
||||
let result = unsafe { libc::read(timer.as_raw_fd(), (&mut expirations as *mut u64).cast(), 8) };
|
||||
if result < 0 {
|
||||
let error = io::Error::last_os_error();
|
||||
@@ -241,6 +251,8 @@ struct ZoneWatch {
|
||||
|
||||
impl ZoneWatch {
|
||||
fn new(paths: &[PathBuf]) -> io::Result<Self> {
|
||||
// SAFETY: inotify_init1 takes scalar flags; owned_fd checks the result before adopting
|
||||
// the descriptor.
|
||||
let fd = owned_fd(unsafe { libc::inotify_init1(libc::IN_CLOEXEC | libc::IN_NONBLOCK) })?;
|
||||
let mut names = Vec::new();
|
||||
for path in paths {
|
||||
@@ -257,6 +269,8 @@ impl ZoneWatch {
|
||||
| libc::IN_ATTRIB
|
||||
| libc::IN_MOVE_SELF
|
||||
| libc::IN_DELETE_SELF;
|
||||
// SAFETY: fd owns the inotify descriptor and parent is a live NUL-terminated CString
|
||||
// for the call.
|
||||
let wd = unsafe { libc::inotify_add_watch(fd.as_raw_fd(), parent.as_ptr(), mask) };
|
||||
if wd >= 0 {
|
||||
names.push((wd, name.to_owned()));
|
||||
@@ -269,6 +283,8 @@ impl ZoneWatch {
|
||||
let mut buffer = [0u8; 8192];
|
||||
let mut changed = false;
|
||||
loop {
|
||||
// SAFETY: buffer is writable for buffer.len() bytes and self.fd owns the descriptor
|
||||
// throughout read.
|
||||
let size = unsafe {
|
||||
libc::read(
|
||||
self.fd.as_raw_fd(),
|
||||
@@ -291,6 +307,8 @@ impl ZoneWatch {
|
||||
}
|
||||
let mut offset = 0;
|
||||
while offset + std::mem::size_of::<libc::inotify_event>() <= size as usize {
|
||||
// SAFETY: The loop bounds check leaves a complete inotify_event header in buffer;
|
||||
// read_unaligned permits its byte alignment.
|
||||
let event = unsafe {
|
||||
(buffer.as_ptr().add(offset) as *const libc::inotify_event).read_unaligned()
|
||||
};
|
||||
@@ -440,6 +458,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn absolute_timer_and_subscription_drop_wake_without_recurring_poll() {
|
||||
// SAFETY: timerfd_create takes scalar arguments; owned_fd adopts its result only after
|
||||
// checking success.
|
||||
let timer = owned_fd(unsafe {
|
||||
libc::timerfd_create(libc::CLOCK_REALTIME, libc::TFD_CLOEXEC | libc::TFD_NONBLOCK)
|
||||
})
|
||||
@@ -450,6 +470,7 @@ mod tests {
|
||||
events: libc::POLLIN,
|
||||
revents: 0,
|
||||
};
|
||||
// SAFETY: poll is a live writable pollfd and the entry count is one.
|
||||
assert_eq!(unsafe { libc::poll(&mut poll, 1, 1000) }, 1);
|
||||
read_timer(&timer).unwrap();
|
||||
// Reading an already drained timer is harmless (EAGAIN).
|
||||
|
||||
@@ -113,7 +113,7 @@ impl Files {
|
||||
};
|
||||
// Filesystem/icon work must not hold the picker's read lock.
|
||||
let matcher = Matcher::new(query);
|
||||
let mut icons = icons.lock().unwrap_or_else(|error| error.into_inner());
|
||||
let mut icons = icons.lock().ok();
|
||||
paths
|
||||
.into_iter()
|
||||
.take_while(|_| !cancel.cancelled())
|
||||
@@ -123,7 +123,7 @@ impl Files {
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let icon = Some(icons.file(&path));
|
||||
let icon = icons.as_mut().map(|icons| icons.file(&path));
|
||||
let subtitle = format!("File · {}", path.parent().unwrap_or(&path).display());
|
||||
let path: Arc<std::path::Path> = path.into();
|
||||
Row {
|
||||
@@ -167,5 +167,23 @@ mod tests {
|
||||
let matches = files.query("launcher-note");
|
||||
assert!(matches.iter().any(|row| row.title == "launcher-note.txt"));
|
||||
assert!(files.query("no-such-file-xyz").is_empty());
|
||||
|
||||
let icons = std::sync::Mutex::new(super::super::icons::FileIcons::default());
|
||||
std::thread::scope(|scope| {
|
||||
let failed = scope
|
||||
.spawn(|| {
|
||||
let _guard = icons.lock().expect("fresh icon cache");
|
||||
panic!("simulate an icon worker failure");
|
||||
})
|
||||
.join();
|
||||
assert!(failed.is_err());
|
||||
});
|
||||
let matches = files.search("launcher-note", &Default::default(), &icons);
|
||||
assert!(matches.iter().any(|row| row.title == "launcher-note.txt"));
|
||||
assert!(matches.iter().all(|row| row.icon.is_none()));
|
||||
assert!(
|
||||
icons.is_poisoned(),
|
||||
"search must not reuse the poisoned cache"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -276,6 +276,8 @@ mod tests {
|
||||
if done.is_err() {
|
||||
// Reap a stuck test child before reporting a failure. The reader
|
||||
// still owns Child, so this PID cannot have been reused.
|
||||
// SAFETY: kill has no pointer arguments; the reader still owns the unreaped child,
|
||||
// preventing PID reuse.
|
||||
unsafe { libc::kill(pid as libc::pid_t, libc::SIGKILL) };
|
||||
}
|
||||
|
||||
|
||||
@@ -146,9 +146,10 @@ fn wait_for_poll(shared: &PollState) -> Option<u64> {
|
||||
}
|
||||
|
||||
fn wake_status(shared: &PollState) {
|
||||
let mut state = shared.0.lock().unwrap_or_else(|error| error.into_inner());
|
||||
state.4 = state.4.wrapping_add(1);
|
||||
shared.1.notify_one();
|
||||
if let Ok(mut state) = shared.0.lock() {
|
||||
state.4 = state.4.wrapping_add(1);
|
||||
shared.1.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
impl Service {
|
||||
@@ -959,6 +960,26 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_wake_does_not_reuse_state_after_a_worker_panics() {
|
||||
let shared = Arc::new((Mutex::new((0, false, None, false, 0)), Condvar::new()));
|
||||
let worker = shared.clone();
|
||||
assert!(
|
||||
thread::spawn(move || {
|
||||
let _guard = worker.0.lock().expect("fresh polling state");
|
||||
panic!("simulate a failed status worker");
|
||||
})
|
||||
.join()
|
||||
.is_err()
|
||||
);
|
||||
|
||||
wake_status(&shared);
|
||||
assert!(
|
||||
shared.0.is_poisoned(),
|
||||
"wake must not recover partially updated state"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polling_waits_for_a_write_and_uses_the_completed_generation() {
|
||||
let shared = Arc::new((Mutex::new((7, true, None, false, 0)), Condvar::new()));
|
||||
|
||||
@@ -159,12 +159,15 @@ fn effects_lifecycle(advertise: bool) {
|
||||
use wayland_client::Proxy;
|
||||
// Match window::run: each native callback lends the same display through
|
||||
// a fresh borrowed backend. Neither wrapper owns the actual display.
|
||||
// SAFETY: connection retains the live display until the borrowed backend and surface are
|
||||
// dropped.
|
||||
let borrowed = wayland_client::Connection::from_backend(unsafe {
|
||||
wayland_client::backend::Backend::from_foreign_display(
|
||||
connection.backend().display_ptr(),
|
||||
)
|
||||
});
|
||||
assert_ne!(borrowed, connection);
|
||||
// SAFETY: surface is a live wl_surface on the same display retained by connection.
|
||||
let id = unsafe {
|
||||
wayland_client::backend::ObjectId::from_ptr(
|
||||
wayland_client::protocol::wl_surface::WlSurface::interface(),
|
||||
|
||||
Reference in new issue
Block a user