Skip to main content

document_graphics/images/
worker.rs

1//! Blocking image worker client. Applications own queues, snapshots and caches.
2use super::{
3    Frame, Info, Pixels, Region, check,
4    protocol::{self, Operation, Response},
5};
6use crate::process::{RequestError, RequestGuard, kill_and_reap};
7#[cfg(unix)]
8use std::io::Write;
9use std::{fs::File, path::Path, sync::atomic::AtomicBool};
10use std::{
11    io::{self, Read},
12    process::{Child, ChildStdin, ChildStdout, Command, Stdio},
13    time::{Duration, Instant},
14};
15enum Answer {
16    Info(Info),
17    Frame(Frame),
18}
19
20const TIMEOUT: Duration = Duration::from_secs(10);
21const RSS_LIMIT: u64 = 256 * 1024 * 1024;
22pub struct Worker {
23    child: Child,
24    input: ChildStdin,
25    output: ChildStdout,
26    _source: File,
27    failed: bool,
28    info: Option<Info>,
29}
30impl Worker {
31    /// `source` must be a stable, seekable regular file supplied by the caller.
32    /// The worker never resolves a document path or snapshots a changing source.
33    pub fn spawn(executable: &Path, argument: &str, source: &File) -> Result<Self, String> {
34        if !cfg!(unix) {
35            return Err("Isolated image previews are unavailable on this platform".into());
36        }
37        let mut command = Command::new(executable);
38        command.arg(argument);
39        Self::start(command, source.try_clone().map_err(|e| e.to_string())?)
40    }
41    fn start(mut command: Command, source_file: File) -> Result<Self, String> {
42        let metadata = source_file.metadata().map_err(|e| e.to_string())?;
43        if !metadata.is_file() || metadata.len() > super::MAX_FILE_BYTES {
44            return Err("Image exceeds the 256 MiB file limit or is not a regular file".into());
45        }
46        #[cfg(unix)]
47        {
48            use std::os::{fd::AsRawFd, unix::process::CommandExt};
49            let source = source_file.as_raw_fd();
50            // SAFETY: only async-signal-safe descriptor operations before exec.
51            unsafe {
52                command.pre_exec(move || {
53                    if libc::dup2(source, 3) < 0 || libc::fcntl(3, libc::F_SETFD, 0) < 0 {
54                        return Err(io::Error::last_os_error());
55                    }
56                    Ok(())
57                });
58            }
59        }
60        let mut child = command
61            .env_clear()
62            .stdin(Stdio::piped())
63            .stdout(Stdio::piped())
64            .stderr(Stdio::null())
65            .spawn()
66            .map_err(|e| e.to_string())?;
67        let input = child.stdin.take().unwrap();
68        let output = child.stdout.take().unwrap();
69        let worker = Self {
70            child,
71            input,
72            output,
73            _source: source_file,
74            failed: false,
75            info: None,
76        };
77        #[cfg(unix)]
78        {
79            use std::os::fd::AsRawFd;
80            // SAFETY: the pipe is owned by worker; failure drops and reaps the child.
81            let result = unsafe {
82                let fd = worker.output.as_raw_fd();
83                let flags = libc::fcntl(fd, libc::F_GETFL);
84                if flags < 0 {
85                    -1
86                } else {
87                    libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK)
88                }
89            };
90            if result < 0 {
91                return Err(io::Error::last_os_error().to_string());
92            }
93        }
94        Ok(worker)
95    }
96    pub fn info(&mut self, cancel: &AtomicBool) -> Result<Info, String> {
97        match self.run(Operation::Info, cancel)? {
98            Answer::Info(info) => Ok(info),
99            _ => Err("Unexpected image response".into()),
100        }
101    }
102    pub fn render(&mut self, regions: Vec<Region>, cancel: &AtomicBool) -> Result<Frame, String> {
103        match self.run(Operation::Render(regions), cancel)? {
104            Answer::Frame(frame) => Ok(frame),
105            _ => Err("Unexpected image response".into()),
106        }
107    }
108    fn run(&mut self, operation: Operation, cancel: &AtomicBool) -> Result<Answer, String> {
109        if self.failed {
110            return Err("Image worker stopped; start a new worker".into());
111        }
112        let result = self.request(operation, cancel);
113        if result.is_err() {
114            self.failed = true;
115            kill_and_reap(&mut self.child);
116        }
117        result
118    }
119    fn request(&mut self, operation: Operation, cancel: &AtomicBool) -> Result<Answer, String> {
120        check(cancel)?;
121        if let Operation::Render(regions) = &operation {
122            super::validate_regions(self.info.as_ref().ok_or("Image metadata missing")?, regions)?;
123        }
124        protocol::write(&mut self.input, &operation).map_err(|e| e.to_string())?;
125        let mut reader = Interruptible {
126            worker: self,
127            guard: RequestGuard::new(cancel, Instant::now() + TIMEOUT, RSS_LIMIT),
128        };
129        let response: Response = protocol::read(&mut reader).map_err(|e| e.to_string())?;
130        match response {
131            Response::Info(info) => {
132                if !matches!(operation, Operation::Info) {
133                    return Err("Unexpected image metadata".into());
134                }
135                info.validate()?;
136                reader.resources(true).map_err(|e| e.to_string())?;
137                reader.worker.info = Some(info.clone());
138                Ok(Answer::Info(info))
139            }
140            Response::Pixels { regions, delay_ms } => {
141                let Operation::Render(wanted) = operation else {
142                    return Err("Unexpected image pixels".into());
143                };
144                if regions != wanted || !(11..=60_000).contains(&delay_ms) {
145                    return Err("Invalid image worker response".into());
146                }
147                let mut pixels = Vec::with_capacity(regions.len());
148                for region in regions {
149                    let mut rgba = vec![0; region.bytes()];
150                    reader.read_exact(&mut rgba).map_err(|e| e.to_string())?;
151                    pixels.push(Pixels { region, rgba });
152                }
153                reader.resources(true).map_err(|e| e.to_string())?;
154                Ok(Answer::Frame(Frame { pixels, delay_ms }))
155            }
156            Response::Error(error) => Err(error),
157        }
158    }
159}
160impl Drop for Worker {
161    fn drop(&mut self) {
162        kill_and_reap(&mut self.child);
163    }
164}
165struct Interruptible<'a> {
166    worker: &'a mut Worker,
167    guard: RequestGuard<'a>,
168}
169impl Interruptible<'_> {
170    fn resources(&mut self, force: bool) -> io::Result<()> {
171        self.guard
172            .check(self.worker.child.id(), force)
173            .map_err(|error| {
174                io::Error::other(match error {
175                    RequestError::Cancelled => super::CANCELLED,
176                    RequestError::TimedOut => {
177                        "Image decode exceeded 10 seconds; use Source/Binary or retry"
178                    }
179                    #[cfg(target_os = "macos")]
180                    RequestError::MemoryUnavailable => "Cannot monitor image worker memory",
181                    #[cfg(target_os = "macos")]
182                    RequestError::MemoryExceeded => {
183                        "Image decoder exceeded 256 MiB; overview or Binary remains available"
184                    }
185                })
186            })
187    }
188}
189impl Read for Interruptible<'_> {
190    fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
191        loop {
192            self.resources(false)?;
193            match self.worker.output.read(bytes) {
194                Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
195                    #[cfg(unix)]
196                    {
197                        use std::os::fd::AsRawFd;
198                        let mut poll = libc::pollfd {
199                            fd: self.worker.output.as_raw_fd(),
200                            events: libc::POLLIN,
201                            revents: 0,
202                        };
203                        // SAFETY: one valid pipe; wake regularly for cancellation and resource checks.
204                        if unsafe { libc::poll(&mut poll, 1, 10) } < 0 {
205                            let e = io::Error::last_os_error();
206                            if e.kind() != io::ErrorKind::Interrupted {
207                                return Err(e);
208                            }
209                        }
210                    }
211                    #[cfg(not(unix))]
212                    std::thread::sleep(Duration::from_millis(10));
213                }
214                Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
215                result => return result,
216            }
217        }
218    }
219}
220pub fn worker_main() -> Result<(), String> {
221    #[cfg(unix)]
222    {
223        #[cfg(target_os = "macos")]
224        restrict_worker()?;
225        // SAFETY: child-only process resource settings and a terminating backstop.
226        unsafe {
227            libc::setrlimit(
228                libc::RLIMIT_CORE,
229                &libc::rlimit {
230                    rlim_cur: 0,
231                    rlim_max: 0,
232                },
233            );
234            #[cfg(not(target_os = "macos"))]
235            libc::setrlimit(
236                libc::RLIMIT_AS,
237                &libc::rlimit {
238                    rlim_cur: 512 * 1024 * 1024,
239                    rlim_max: 512 * 1024 * 1024,
240                },
241            );
242            libc::alarm(12);
243        }
244        let document = super::backend::Decoder::open(std::path::Path::new("/dev/fd/3"));
245        unsafe {
246            libc::alarm(0);
247        }
248        let mut input = io::stdin().lock();
249        let mut output = io::stdout().lock();
250        loop {
251            let operation: Operation = match protocol::read(&mut input) {
252                Ok(op) => op,
253                Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(()),
254                Err(e) => return Err(e.to_string()),
255            };
256            unsafe {
257                libc::alarm(12);
258            }
259            let result = (|| {
260                let decoder = document.as_ref().map_err(Clone::clone)?;
261                match operation {
262                    Operation::Info => Ok(Answer::Info(decoder.info())),
263                    Operation::Render(regions) => decoder.render(&regions).map(Answer::Frame),
264                }
265            })();
266            unsafe {
267                libc::alarm(0);
268            }
269            match result {
270                Ok(Answer::Info(info)) => protocol::write(&mut output, &Response::Info(info)),
271                Ok(Answer::Frame(frame)) => (|| {
272                    protocol::write(
273                        &mut output,
274                        &Response::Pixels {
275                            regions: frame.pixels.iter().map(|p| p.region).collect(),
276                            delay_ms: frame.delay_ms,
277                        },
278                    )?;
279                    for pixels in frame.pixels {
280                        output.write_all(&pixels.rgba)?;
281                    }
282                    output.flush()
283                })(),
284                Err(error) => protocol::write(
285                    &mut output,
286                    &Response::Error(error.chars().take(2048).collect()),
287                ),
288            }
289            .map_err(|e| e.to_string())?;
290        }
291    }
292    #[cfg(not(unix))]
293    Err("Isolated image previews are unavailable on this platform".into())
294}
295#[cfg(target_os = "macos")]
296fn restrict_worker() -> Result<(), String> {
297    unsafe extern "C" {
298        fn sandbox_init(
299            profile: *const std::ffi::c_char,
300            flags: u64,
301            error: *mut *mut std::ffi::c_char,
302        ) -> i32;
303        fn sandbox_free_error(error: *mut std::ffi::c_char);
304    }
305    // Fail closed if the native-codec sandbox cannot be installed. Reads remain available to
306    // Core Graphics/font services; writes, networking and child execution do not.
307    let profile = c"(version 1)(allow default)(deny network*)(deny file-write*)(deny process-exec)(deny process-fork)";
308    let mut error = std::ptr::null_mut();
309    // SAFETY: constant NUL-terminated profile; error is allocated by sandbox_init.
310    let result = unsafe { sandbox_init(profile.as_ptr(), 0, &mut error) };
311    if result != 0 {
312        let message = if error.is_null() {
313            "Cannot restrict image worker".into()
314        } else {
315            unsafe { std::ffi::CStr::from_ptr(error) }
316                .to_string_lossy()
317                .into_owned()
318        };
319        unsafe {
320            sandbox_free_error(error);
321        }
322        return Err(message);
323    }
324    // SAFETY: process-local priority and core-dump resource limits.
325    unsafe {
326        libc::setpriority(libc::PRIO_PROCESS, 0, 10);
327        libc::setrlimit(
328            libc::RLIMIT_CORE,
329            &libc::rlimit {
330                rlim_cur: 0,
331                rlim_max: 0,
332            },
333        );
334    }
335    Ok(())
336}
337
338#[cfg(all(test, unix))]
339mod tests {
340    use super::*;
341    use std::sync::{
342        Arc,
343        atomic::{AtomicBool, Ordering},
344    };
345    #[test]
346    fn cancellation_kills_and_reaps_a_stuck_decoder() {
347        let file = tempfile::NamedTempFile::new().unwrap();
348        let cancel = Arc::new(AtomicBool::new(false));
349        let source_file = file.reopen().unwrap();
350        let mut command = Command::new("/bin/sleep");
351        command.arg("60");
352        let mut worker = Worker::start(command, source_file).unwrap();
353        let pid = worker.child.id();
354        let flag = cancel.clone();
355        let trigger = std::thread::spawn(move || {
356            std::thread::sleep(Duration::from_millis(30));
357            flag.store(true, Ordering::Relaxed);
358        });
359        let start = Instant::now();
360        assert_eq!(
361            worker.run(Operation::Info, &cancel).err().unwrap(),
362            super::super::CANCELLED
363        );
364        assert!(worker.failed);
365        trigger.join().unwrap();
366        assert!(start.elapsed() < Duration::from_secs(1));
367        // SAFETY: signal zero only checks that our child has been reaped.
368        assert_eq!(unsafe { libc::kill(pid as i32, 0) }, -1);
369    }
370}