1use crate::{
3 MAX_PIXELS, Raster, RasterOptions,
4 mermaid::{self, MAX_SOURCE},
5 process::{RequestError, RequestGuard, kill_and_reap},
6};
7use std::{
8 io::{self, Read, Write},
9 path::Path,
10 process::{Child, Command, Stdio},
11 sync::{
12 atomic::{AtomicBool, Ordering},
13 mpsc,
14 },
15 thread::{self, JoinHandle},
16 time::{Duration, Instant},
17};
18pub const MAX_REQUESTS: usize = 32;
20const MAX_RESIDENT_BYTES: u64 = 256 * 1024 * 1024;
21
22pub fn worker_main() -> io::Result<()> {
24 restrict_worker()?;
25 serve(&mut io::stdin().lock(), &mut io::stdout().lock())
26}
27
28fn restrict_worker() -> io::Result<()> {
29 #[cfg(unix)]
30 {
31 for (resource, seconds) in [(libc::RLIMIT_CPU, 8), (libc::RLIMIT_CORE, 0)] {
34 let limit = libc::rlimit {
35 rlim_cur: seconds,
36 rlim_max: seconds,
37 };
38 if unsafe { libc::setrlimit(resource, &limit) } != 0 {
39 return Err(io::Error::last_os_error());
40 }
41 }
42 unsafe {
44 libc::setpriority(libc::PRIO_PROCESS, 0, 10);
45 }
46 }
47 #[cfg(target_os = "macos")]
48 {
49 use std::ffi::{CStr, c_char};
50 unsafe extern "C" {
51 fn sandbox_init(profile: *const c_char, flags: u64, error: *mut *mut c_char) -> i32;
52 fn sandbox_free_error(error: *mut c_char);
53 }
54 let profile = c"(version 1)(allow default)(deny network*)(deny file-write*)(deny file-read-data)(deny process-exec)(deny process-fork)";
57 let mut error = std::ptr::null_mut();
58 if unsafe { sandbox_init(profile.as_ptr(), 0, &mut error) } != 0 {
60 let message = if error.is_null() {
61 "Cannot restrict diagram worker".into()
62 } else {
63 unsafe { CStr::from_ptr(error) }
64 .to_string_lossy()
65 .into_owned()
66 };
67 if !error.is_null() {
68 unsafe { sandbox_free_error(error) };
69 }
70 return Err(io::Error::other(message));
71 }
72 }
73 Ok(())
74}
75
76fn serve(input: &mut impl Read, output: &mut impl Write) -> io::Result<()> {
77 for _ in 0..MAX_REQUESTS {
78 let mut header = [0; 25];
79 match input.read(&mut header[..1])? {
80 0 => return Ok(()),
81 _ => input.read_exact(&mut header[1..])?,
82 }
83 let number = |slice: &[u8]| u32::from_le_bytes(slice.try_into().unwrap());
84 let length = number(&header[12..16]) as usize;
85 let options = RasterOptions {
86 bounds: (number(&header[4..8]), number(&header[8..12])),
87 scale: f64::from_le_bytes(header[17..25].try_into().unwrap()),
88 };
89 if &header[..4] != b"NPV1"
90 || !options.valid()
91 || length == 0
92 || length > MAX_SOURCE
93 || header[16] > 1
94 {
95 return Err(io::Error::new(
96 io::ErrorKind::InvalidData,
97 "Invalid diagram request",
98 ));
99 }
100 let mut source = vec![0; length];
101 input.read_exact(&mut source)?;
102 let pixels = std::str::from_utf8(&source)
103 .ok()
104 .and_then(|s| mermaid::render(s, header[16] == 1, options));
105 if let Some(p) = pixels {
106 for n in [
107 p.width,
108 p.height,
109 p.rgba.len() as u32,
110 p.original_width,
111 p.original_height,
112 ] {
113 output.write_all(&n.to_le_bytes())?;
114 }
115 output.write_all(&p.rgba)?;
116 } else {
117 output.write_all(&[0; 20])?;
118 }
119 output.flush()?;
120 }
121 Ok(())
122}
123
124fn response(input: &mut impl Read, bounds: (u32, u32)) -> io::Result<Option<Raster>> {
125 let mut header = [0; 20];
126 input.read_exact(&mut header)?;
127 let number = |slice: &[u8]| u32::from_le_bytes(slice.try_into().unwrap());
128 let (width, height, length) = (
129 number(&header[..4]),
130 number(&header[4..8]),
131 number(&header[8..12]) as usize,
132 );
133 let original_width = number(&header[12..16]);
134 let original_height = number(&header[16..20]);
135 if width == 0 && height == 0 && length == 0 && original_width == 0 && original_height == 0 {
136 return Ok(None);
137 }
138 let pixels = u64::from(width) * u64::from(height);
139 if original_width == 0
140 || original_height == 0
141 || original_width > crate::svg::MAX_DIAGRAM_EDGE
142 || original_height > crate::svg::MAX_DIAGRAM_EDGE
143 || u64::from(original_width) * u64::from(original_height) > crate::svg::MAX_DIAGRAM_AREA
144 || width == 0
145 || height == 0
146 || width > bounds.0
147 || height > bounds.1
148 || pixels > MAX_PIXELS as u64
149 || pixels * 4 != length as u64
150 {
151 return Err(io::Error::new(
152 io::ErrorKind::InvalidData,
153 "invalid Mermaid pixels",
154 ));
155 }
156 let mut rgba = vec![0; length];
157 input.read_exact(&mut rgba)?;
158 Ok(Some(Raster {
159 original_width,
160 original_height,
161 width,
162 height,
163 rgba,
164 }))
165}
166
167type Request = (Vec<u8>, (u32, u32));
168pub struct Worker {
171 child: Child,
172 requests: Option<mpsc::SyncSender<Request>>,
173 replies: mpsc::Receiver<io::Result<Option<Raster>>>,
174 thread: Option<JoinHandle<()>>,
175 completed: usize,
176}
177impl Worker {
178 pub fn requests_completed(&self) -> usize {
179 self.completed
180 }
181 pub fn spawn(program: &Path, worker_argument: &str) -> io::Result<Self> {
182 let child = Command::new(program)
183 .arg(worker_argument)
184 .env_clear()
185 .stdin(Stdio::piped())
186 .stdout(Stdio::piped())
187 .stderr(Stdio::null())
188 .spawn()?;
189 Self::connect(child)
190 }
191
192 fn connect(mut child: Child) -> io::Result<Self> {
193 let mut input = child.stdin.take().unwrap();
194 let mut output = child.stdout.take().unwrap();
195 let (requests, incoming) = mpsc::sync_channel::<Request>(1);
196 let (replies, outgoing) = mpsc::sync_channel(1);
197 let thread = thread::Builder::new()
198 .name("mermaid-io".into())
199 .spawn(move || {
200 while let Ok((message, bounds)) = incoming.recv() {
201 let result = input
202 .write_all(&message)
203 .and_then(|()| input.flush())
204 .and_then(|()| response(&mut output, bounds));
205 let failed = result.is_err();
206 if replies.send(result).is_err() || failed {
207 break;
208 }
209 }
210 });
211 match thread {
212 Ok(thread) => Ok(Self {
213 child,
214 requests: Some(requests),
215 replies: outgoing,
216 thread: Some(thread),
217 completed: 0,
218 }),
219 Err(error) => {
220 kill_and_reap(&mut child);
221 Err(error)
222 }
223 }
224 }
225
226 pub fn render(
229 &mut self,
230 source: &str,
231 light: bool,
232 options: RasterOptions,
233 timeout: Duration,
234 cancel: &AtomicBool,
235 ) -> io::Result<Option<Raster>> {
236 let result = self.render_inner(source, light, options, timeout, cancel);
237 if result.is_err() {
238 self.stop();
239 }
240 result
241 }
242 fn render_inner(
243 &mut self,
244 source: &str,
245 light: bool,
246 options: RasterOptions,
247 timeout: Duration,
248 cancel: &AtomicBool,
249 ) -> io::Result<Option<Raster>> {
250 if self.requests.is_none() {
251 return Err(io::Error::other("Diagram worker stopped"));
252 }
253 if cancel.load(Ordering::Relaxed) {
254 return Err(io::ErrorKind::Interrupted.into());
255 }
256 if !options.valid() {
257 return Err(io::Error::new(
258 io::ErrorKind::InvalidInput,
259 "Invalid raster options",
260 ));
261 }
262 if !mermaid::admissible(source) {
263 return Ok(None);
264 }
265 if self.completed >= MAX_REQUESTS {
266 return Err(io::Error::other("Diagram worker request limit reached"));
267 }
268 let deadline = Instant::now()
269 .checked_add(timeout)
270 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid render timeout"))?;
271 let mut guard = RequestGuard::new(cancel, deadline, MAX_RESIDENT_BYTES);
272 let bounds = options.bounds;
273 let mut message = Vec::with_capacity(25 + source.len());
274 message.extend_from_slice(b"NPV1");
275 message.extend_from_slice(&bounds.0.to_le_bytes());
276 message.extend_from_slice(&bounds.1.to_le_bytes());
277 message.extend_from_slice(&(source.len() as u32).to_le_bytes());
278 message.push(u8::from(light));
279 message.extend_from_slice(&options.scale.to_le_bytes());
280 message.extend_from_slice(source.as_bytes());
281 self.requests
282 .as_ref()
283 .unwrap()
284 .try_send((message, bounds))
285 .map_err(|_| io::Error::other("Mermaid worker unavailable"))?;
286 loop {
287 match guard.check(self.child.id(), false) {
288 #[cfg(target_os = "macos")]
290 Err(RequestError::MemoryUnavailable) => {}
291 result => result.map_err(request_error)?,
292 }
293 let remaining = guard.remaining();
294 match self
295 .replies
296 .recv_timeout(remaining.min(Duration::from_millis(20)))
297 {
298 Ok(result) => {
299 guard.check_completion().map_err(request_error)?;
302 self.completed += 1;
303 return result;
304 }
305 Err(mpsc::RecvTimeoutError::Timeout) => {}
306 Err(_) => return Err(io::Error::other("Diagram worker stopped")),
307 }
308 }
309 }
310 fn stop(&mut self) {
311 kill_and_reap(&mut self.child);
314 self.requests.take();
315 if let Some(thread) = self.thread.take() {
316 let _ = thread.join();
317 }
318 }
319}
320
321impl Drop for Worker {
322 fn drop(&mut self) {
323 self.stop();
324 }
325}
326
327fn request_error(error: RequestError) -> io::Error {
328 match error {
329 RequestError::Cancelled => io::Error::new(io::ErrorKind::Interrupted, "Diagram cancelled"),
330 RequestError::TimedOut => io::Error::new(io::ErrorKind::TimedOut, "Diagram timed out"),
331 #[cfg(target_os = "macos")]
332 RequestError::MemoryExceeded => io::Error::other("Diagram exceeded its memory limit"),
333 #[cfg(target_os = "macos")]
334 RequestError::MemoryUnavailable => io::Error::other("Cannot monitor diagram worker memory"),
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use crate::MAX_EDGE;
342 #[cfg(unix)]
343 use std::sync::Arc;
344 #[test]
345 fn worker_protocol_rejects_invalid_or_truncated_data_before_allocating() {
346 let mut data = Vec::new();
347 for n in [2u32, 1, 8, 1, 1] {
348 data.extend_from_slice(&n.to_le_bytes());
349 }
350 data.extend_from_slice(&[255, 0, 0, 128, 0, 0, 255, 255]);
351 let p = response(&mut &data[..], (MAX_EDGE, MAX_EDGE))
352 .unwrap()
353 .unwrap();
354 assert_eq!((p.width, p.height), (2, 1));
355 assert_eq!(p.rgba, data[20..]);
356 data[12..16].copy_from_slice(&10000_u32.to_le_bytes());
358 data[16..20].copy_from_slice(&1500_u32.to_le_bytes());
359 let p = response(&mut &data[..], (MAX_EDGE, MAX_EDGE))
360 .unwrap()
361 .unwrap();
362 assert_eq!((p.original_width, p.original_height), (10000, 1500));
363 assert_eq!(p.rgba.len(), 8);
364 data[12..16].copy_from_slice(&(crate::svg::MAX_DIAGRAM_EDGE + 1).to_le_bytes());
365 assert!(response(&mut &data[..], (MAX_EDGE, MAX_EDGE)).is_err());
366 data[12..16].copy_from_slice(&32000_u32.to_le_bytes());
367 data[16..20].copy_from_slice(&32000_u32.to_le_bytes());
368 assert!(response(&mut &data[..], (MAX_EDGE, MAX_EDGE)).is_err());
369 data[12..16].copy_from_slice(&1_u32.to_le_bytes());
370 data[16..20].copy_from_slice(&1_u32.to_le_bytes());
371 assert!(response(&mut &data[..27], (MAX_EDGE, MAX_EDGE)).is_err());
372 assert!(response(&mut &data[..], (1, 1)).is_err());
373 assert!(
374 response(&mut &[0; 20][..], (MAX_EDGE, MAX_EDGE))
375 .unwrap()
376 .is_none()
377 );
378 data[8..12].copy_from_slice(&u32::MAX.to_le_bytes());
379 assert!(response(&mut &data[..], (MAX_EDGE, MAX_EDGE)).is_err());
380 let mut request = Vec::from(&b"NPV1"[..]);
381 for n in [MAX_EDGE, MAX_EDGE, MAX_SOURCE as u32 + 1] {
382 request.extend_from_slice(&n.to_le_bytes());
383 }
384 request.push(0);
385 request.extend_from_slice(&1_f64.to_le_bytes());
386 let mut output = Vec::new();
387 assert!(serve(&mut &request[..], &mut output).is_err());
388 assert!(output.is_empty());
389 }
390 #[cfg(unix)]
391 #[test]
392 fn a_reply_after_a_short_deadline_is_rejected() {
393 let child = Command::new("sleep")
394 .arg("30")
395 .stdin(Stdio::piped())
396 .stdout(Stdio::piped())
397 .spawn()
398 .unwrap();
399 let mut worker = Worker::connect(child).unwrap();
400 let (send, receive) = mpsc::sync_channel(1);
401 worker.replies = receive;
402 let delayed = thread::spawn(move || {
403 thread::sleep(Duration::from_millis(10));
404 let _ = send.send(Ok(None));
405 });
406 let error = worker
407 .render(
408 "flowchart LR; A-->B",
409 false,
410 RasterOptions::scaled(1.),
411 Duration::from_millis(1),
412 &AtomicBool::new(false),
413 )
414 .err()
415 .unwrap();
416 assert_eq!(error.kind(), io::ErrorKind::TimedOut);
417 assert!(worker.requests.is_none());
418 assert_eq!(worker.requests_completed(), 0);
419 delayed.join().unwrap();
420 }
421
422 #[cfg(unix)]
423 #[test]
424 fn timeouts_and_cancellation_kill_and_reap_partial_replies() {
425 for cancel_after in [None, Some(Duration::from_millis(30))] {
426 let child = Command::new("sh")
427 .args(["-c", "printf '\\001'; exec sleep 30"])
428 .stdin(Stdio::piped())
429 .stdout(Stdio::piped())
430 .stderr(Stdio::null())
431 .spawn()
432 .unwrap();
433 let mut worker = Worker::connect(child).unwrap();
434 let pid = worker.child.id();
435 let cancel = Arc::new(AtomicBool::new(false));
436 let cancellation = cancel_after.map(|duration| {
437 let cancel = cancel.clone();
438 thread::spawn(move || {
439 thread::sleep(duration);
440 cancel.store(true, Ordering::Relaxed);
441 })
442 });
443 let started = Instant::now();
444 let error = worker
445 .render(
446 "flowchart LR; A-->B",
447 false,
448 RasterOptions::scaled(1.),
449 Duration::from_millis(100),
450 &cancel,
451 )
452 .err()
453 .unwrap();
454 assert_eq!(
455 error.kind(),
456 if cancel_after.is_some() {
457 io::ErrorKind::Interrupted
458 } else {
459 io::ErrorKind::TimedOut
460 }
461 );
462 assert!(worker.requests.is_none());
463 if let Some(thread) = cancellation {
464 thread.join().unwrap();
465 }
466 assert!(started.elapsed() < Duration::from_secs(3));
467 assert_eq!(unsafe { libc::kill(pid as libc::pid_t, 0) }, -1);
469 assert_eq!(io::Error::last_os_error().raw_os_error(), Some(libc::ESRCH));
470 assert!(
471 worker
472 .render(
473 "flowchart LR; A-->B",
474 false,
475 RasterOptions::scaled(1.),
476 Duration::from_millis(100),
477 &AtomicBool::new(false)
478 )
479 .is_err()
480 );
481 }
482 }
483 #[cfg(target_os = "macos")]
486 #[test]
487 fn macos_sandbox_denies_file_reads_writes_network_and_processes() {
488 let file = std::env::temp_dir().join(format!("document-graphics-{}", std::process::id()));
489 std::fs::write(&file, b"private").unwrap();
490 let output = Command::new(std::env::current_exe().unwrap())
491 .args(["worker::tests::sandbox_probe", "--exact", "--ignored"])
492 .env("DOCUMENT_GRAPHICS_SANDBOX_PROBE", &file)
493 .output()
494 .unwrap();
495 let contents = std::fs::read(&file).unwrap();
496 std::fs::remove_file(&file).unwrap();
497 let report = String::from_utf8_lossy(&output.stdout);
498 assert!(
499 output.status.success() && report.contains("1 passed"),
500 "{report}{}",
501 String::from_utf8_lossy(&output.stderr)
502 );
503 assert_eq!(contents, b"private");
504 }
505 #[cfg(target_os = "macos")]
506 #[test]
507 #[ignore = "run in a child process by the macOS sandbox test"]
508 fn sandbox_probe() {
509 let Some(file) = std::env::var_os("DOCUMENT_GRAPHICS_SANDBOX_PROBE") else {
511 return;
512 };
513 let spawn = || Command::new("/usr/bin/true").status();
515 assert!(std::fs::read(&file).is_ok() && spawn().is_ok());
516 assert!(std::net::TcpListener::bind("127.0.0.1:0").is_ok());
517 restrict_worker().unwrap();
518 assert!(std::fs::read(&file).is_err(), "read a file");
519 assert!(std::fs::write(&file, b"changed").is_err(), "wrote a file");
520 assert!(
521 std::net::TcpListener::bind("127.0.0.1:0").is_err(),
522 "opened a socket"
523 );
524 assert!(spawn().is_err(), "started a process");
525 }
526}