Wayland screenshots can't crash the desktop when /run/user is full

The screenshot buffer was a sparse file in XDG_RUNTIME_DIR. With that folder full
(a 1.6 GB Hyprland log), Hyprland wrote the pixels into pages that couldn't exist
and died with SIGBUS in CScreenshareFrame::copyShm. Now the buffer is a memfd with
every page reserved up front; with no room the screenshot fails in AnyBot instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
王性驊 2026-11-05 20:06:46 +08:00
parent e44614736f
commit 70444c683c
1 changed files with 57 additions and 5 deletions

View File

@ -168,15 +168,52 @@ pub fn available() -> bool {
}
/// A file only we can see, gone from the folder already (for shared memory and keymaps).
/// Its memory is really there before the compositor gets it: the compositor writes screen
/// pixels straight into it, and if the space ran out half-way the compositor would be the one
/// to crash (SIGBUS: a full /run/user once took a whole Hyprland desktop down this way).
/// So it lives in memory of its own (memfd), not in the runtime folder, and with no room the
/// screenshot just fails here.
fn scratch_file(len: u64) -> std::io::Result<std::fs::File> {
let dir = std::env::var_os("XDG_RUNTIME_DIR").map(std::path::PathBuf::from).unwrap_or_else(std::env::temp_dir);
let path = dir.join(format!("anybot-wl-{}-{}", std::process::id(), crate::model::now_ms()));
let f = std::fs::OpenOptions::new().read(true).write(true).create_new(true).open(&path)?;
let _ = std::fs::remove_file(&path);
f.set_len(len)?;
use std::os::fd::FromRawFd as _;
let fd = unsafe { libc::memfd_create(c"anybot-wl".as_ptr(), libc::MFD_CLOEXEC) };
let f = if fd >= 0 {
unsafe { std::fs::File::from_raw_fd(fd) }
} else {
let dir = std::env::var_os("XDG_RUNTIME_DIR").map(std::path::PathBuf::from).unwrap_or_else(std::env::temp_dir);
let path = dir.join(format!("anybot-wl-{}-{}", std::process::id(), crate::model::now_ms()));
let f = std::fs::OpenOptions::new().read(true).write(true).create_new(true).open(&path)?;
let _ = std::fs::remove_file(&path);
f
};
if len > 0 {
reserve(&f, len)?;
}
Ok(f)
}
/// Make every page of the file real now (not a promise the compositor finds broken later).
fn reserve(f: &std::fs::File, len: u64) -> std::io::Result<()> {
use std::os::fd::AsRawFd as _;
let r = unsafe { libc::posix_fallocate(f.as_raw_fd(), 0, len as libc::off_t) };
if r == 0 {
return Ok(());
}
if r != libc::EOPNOTSUPP && r != libc::EINVAL {
return Err(std::io::Error::from_raw_os_error(r));
}
// A file system without fallocate: write the zeros ourselves.
f.set_len(0)?;
let mut w = f;
let zeros = vec![0u8; 1 << 20];
let mut left = len;
while left > 0 {
let n = left.min(zeros.len() as u64) as usize;
w.write_all(&zeros[..n])?;
left -= n as u64;
}
Ok(())
}
/// The keymap text: one key per keysym we use; the first four are the modifiers.
pub fn keymap_text(keys: &[u32]) -> String {
let mut s = String::from("xkb_keymap {\nxkb_keycodes \"anybot\" {\n minimum = 8;\n");
@ -614,4 +651,19 @@ mod tests {
let px = to_rgba(&[1, 2, 3, 0], wl_shm::Format::Abgr8888, 1, 1, 4, false).unwrap();
assert_eq!(px.get_pixel(0, 0).0, [1, 2, 3, 255]);
}
/// The screenshot buffer is real memory before the compositor writes into it, and not a
/// file in the runtime folder (a full /run/user must not crash the desktop).
#[test]
fn screenshot_buffer_is_allocated_up_front() {
use std::os::fd::AsRawFd as _;
let len = 3 * 1024 * 1024 + 17;
let f = scratch_file(len).unwrap();
assert_eq!(f.metadata().unwrap().len(), len);
let mut st: libc::stat = unsafe { std::mem::zeroed() };
assert_eq!(unsafe { libc::fstat(f.as_raw_fd(), &mut st) }, 0);
assert!(st.st_blocks as u64 * 512 >= len, "pages not reserved: {} blocks", st.st_blocks);
let link = std::fs::read_link(format!("/proc/self/fd/{}", f.as_raw_fd())).unwrap();
assert!(link.to_string_lossy().starts_with("/memfd:"), "{link:?}");
}
}