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:
parent
e44614736f
commit
70444c683c
62
src/wl.rs
62
src/wl.rs
|
|
@ -168,15 +168,52 @@ pub fn available() -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A file only we can see, gone from the folder already (for shared memory and keymaps).
|
/// 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> {
|
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);
|
use std::os::fd::FromRawFd as _;
|
||||||
let path = dir.join(format!("anybot-wl-{}-{}", std::process::id(), crate::model::now_ms()));
|
let fd = unsafe { libc::memfd_create(c"anybot-wl".as_ptr(), libc::MFD_CLOEXEC) };
|
||||||
let f = std::fs::OpenOptions::new().read(true).write(true).create_new(true).open(&path)?;
|
let f = if fd >= 0 {
|
||||||
let _ = std::fs::remove_file(&path);
|
unsafe { std::fs::File::from_raw_fd(fd) }
|
||||||
f.set_len(len)?;
|
} 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)
|
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.
|
/// The keymap text: one key per keysym we use; the first four are the modifiers.
|
||||||
pub fn keymap_text(keys: &[u32]) -> String {
|
pub fn keymap_text(keys: &[u32]) -> String {
|
||||||
let mut s = String::from("xkb_keymap {\nxkb_keycodes \"anybot\" {\n minimum = 8;\n");
|
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();
|
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]);
|
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:?}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue