1use crate::{
3 MAX_PIXELS, Raster, RasterOptions,
4 mermaid::{self, Background, 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 let valid_flags = match &header[..4] {
90 b"NPV1" => header[16] <= 1,
91 b"NPV2" => header[16] <= 3,
92 _ => false,
93 };
94 if !valid_flags || !options.valid() || length == 0 || length > MAX_SOURCE {
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 background = if header[16] & 2 == 0 {
103 Background::Theme
104 } else {
105 Background::Transparent
106 };
107 let pixels = std::str::from_utf8(&source)
108 .ok()
109 .and_then(|s| mermaid::render(s, header[16] & 1 != 0, background, options));
110 if let Some(p) = pixels {
111 for n in [
112 p.width,
113 p.height,
114 p.rgba.len() as u32,
115 p.original_width,
116 p.original_height,
117 ] {
118 output.write_all(&n.to_le_bytes())?;
119 }
120 output.write_all(&p.rgba)?;
121 } else {
122 output.write_all(&[0; 20])?;
123 }
124 output.flush()?;
125 }
126 Ok(())
127}
128
129fn response(input: &mut impl Read, bounds: (u32, u32)) -> io::Result<Option<Raster>> {
130 let mut header = [0; 20];
131 input.read_exact(&mut header)?;
132 let number = |slice: &[u8]| u32::from_le_bytes(slice.try_into().unwrap());
133 let (width, height, length) = (
134 number(&header[..4]),
135 number(&header[4..8]),
136 number(&header[8..12]) as usize,
137 );
138 let original_width = number(&header[12..16]);
139 let original_height = number(&header[16..20]);
140 if width == 0 && height == 0 && length == 0 && original_width == 0 && original_height == 0 {
141 return Ok(None);
142 }
143 let pixels = u64::from(width) * u64::from(height);
144 if original_width == 0
145 || original_height == 0
146 || original_width > crate::svg::MAX_DIAGRAM_EDGE
147 || original_height > crate::svg::MAX_DIAGRAM_EDGE
148 || u64::from(original_width) * u64::from(original_height) > crate::svg::MAX_DIAGRAM_AREA
149 || width == 0
150 || height == 0
151 || width > bounds.0
152 || height > bounds.1
153 || pixels > MAX_PIXELS as u64
154 || pixels * 4 != length as u64
155 {
156 return Err(io::Error::new(
157 io::ErrorKind::InvalidData,
158 "invalid Mermaid pixels",
159 ));
160 }
161 let mut rgba = vec![0; length];
162 input.read_exact(&mut rgba)?;
163 Ok(Some(Raster {
164 original_width,
165 original_height,
166 width,
167 height,
168 rgba,
169 }))
170}
171
172type Request = (Vec<u8>, (u32, u32));
173pub struct Worker {
176 child: Child,
177 requests: Option<mpsc::SyncSender<Request>>,
178 replies: mpsc::Receiver<io::Result<Option<Raster>>>,
179 thread: Option<JoinHandle<()>>,
180 completed: usize,
181}
182impl Worker {
183 pub fn requests_completed(&self) -> usize {
184 self.completed
185 }
186 pub fn spawn(program: &Path, worker_argument: &str) -> io::Result<Self> {
187 let child = Command::new(program)
188 .arg(worker_argument)
189 .env_clear()
190 .stdin(Stdio::piped())
191 .stdout(Stdio::piped())
192 .stderr(Stdio::null())
193 .spawn()?;
194 Self::connect(child)
195 }
196
197 fn connect(mut child: Child) -> io::Result<Self> {
198 let mut input = child.stdin.take().unwrap();
199 let mut output = child.stdout.take().unwrap();
200 let (requests, incoming) = mpsc::sync_channel::<Request>(1);
201 let (replies, outgoing) = mpsc::sync_channel(1);
202 let thread = thread::Builder::new()
203 .name("mermaid-io".into())
204 .spawn(move || {
205 while let Ok((message, bounds)) = incoming.recv() {
206 let result = input
207 .write_all(&message)
208 .and_then(|()| input.flush())
209 .and_then(|()| response(&mut output, bounds));
210 let failed = result.is_err();
211 if replies.send(result).is_err() || failed {
212 break;
213 }
214 }
215 });
216 match thread {
217 Ok(thread) => Ok(Self {
218 child,
219 requests: Some(requests),
220 replies: outgoing,
221 thread: Some(thread),
222 completed: 0,
223 }),
224 Err(error) => {
225 kill_and_reap(&mut child);
226 Err(error)
227 }
228 }
229 }
230
231 pub fn render(
234 &mut self,
235 source: &str,
236 light: bool,
237 options: RasterOptions,
238 timeout: Duration,
239 cancel: &AtomicBool,
240 ) -> io::Result<Option<Raster>> {
241 self.render_with_background(source, light, Background::Theme, options, timeout, cancel)
242 }
243
244 pub fn render_with_background(
248 &mut self,
249 source: &str,
250 light: bool,
251 background: Background,
252 options: RasterOptions,
253 timeout: Duration,
254 cancel: &AtomicBool,
255 ) -> io::Result<Option<Raster>> {
256 let result = self.render_inner(source, light, background, options, timeout, cancel);
257 if result.is_err() {
258 self.stop();
259 }
260 result
261 }
262 fn render_inner(
263 &mut self,
264 source: &str,
265 light: bool,
266 background: Background,
267 options: RasterOptions,
268 timeout: Duration,
269 cancel: &AtomicBool,
270 ) -> io::Result<Option<Raster>> {
271 if self.requests.is_none() {
272 return Err(io::Error::other("Diagram worker stopped"));
273 }
274 if cancel.load(Ordering::Relaxed) {
275 return Err(io::ErrorKind::Interrupted.into());
276 }
277 if !options.valid() {
278 return Err(io::Error::new(
279 io::ErrorKind::InvalidInput,
280 "Invalid raster options",
281 ));
282 }
283 if !mermaid::admissible(source) {
284 return Ok(None);
285 }
286 if self.completed >= MAX_REQUESTS {
287 return Err(io::Error::other("Diagram worker request limit reached"));
288 }
289 let deadline = Instant::now()
290 .checked_add(timeout)
291 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid render timeout"))?;
292 let mut guard = RequestGuard::new(cancel, deadline, MAX_RESIDENT_BYTES);
293 let bounds = options.bounds;
294 let mut message = Vec::with_capacity(25 + source.len());
295 let transparent = background == Background::Transparent;
296 message.extend_from_slice(if transparent { b"NPV2" } else { b"NPV1" });
297 message.extend_from_slice(&bounds.0.to_le_bytes());
298 message.extend_from_slice(&bounds.1.to_le_bytes());
299 message.extend_from_slice(&(source.len() as u32).to_le_bytes());
300 message.push(u8::from(light) | (u8::from(transparent) << 1));
301 message.extend_from_slice(&options.scale.to_le_bytes());
302 message.extend_from_slice(source.as_bytes());
303 self.requests
304 .as_ref()
305 .unwrap()
306 .try_send((message, bounds))
307 .map_err(|_| io::Error::other("Mermaid worker unavailable"))?;
308 loop {
309 match guard.check(self.child.id(), false) {
310 #[cfg(target_os = "macos")]
312 Err(RequestError::MemoryUnavailable) => {}
313 result => result.map_err(request_error)?,
314 }
315 let remaining = guard.remaining();
316 match self
317 .replies
318 .recv_timeout(remaining.min(Duration::from_millis(20)))
319 {
320 Ok(result) => {
321 guard.check_completion().map_err(request_error)?;
324 self.completed += 1;
325 return result;
326 }
327 Err(mpsc::RecvTimeoutError::Timeout) => {}
328 Err(_) => return Err(io::Error::other("Diagram worker stopped")),
329 }
330 }
331 }
332 fn stop(&mut self) {
333 kill_and_reap(&mut self.child);
336 self.requests.take();
337 if let Some(thread) = self.thread.take() {
338 let _ = thread.join();
339 }
340 }
341}
342
343impl Drop for Worker {
344 fn drop(&mut self) {
345 self.stop();
346 }
347}
348
349fn request_error(error: RequestError) -> io::Error {
350 match error {
351 RequestError::Cancelled => io::Error::new(io::ErrorKind::Interrupted, "Diagram cancelled"),
352 RequestError::TimedOut => io::Error::new(io::ErrorKind::TimedOut, "Diagram timed out"),
353 #[cfg(target_os = "macos")]
354 RequestError::MemoryExceeded => io::Error::other("Diagram exceeded its memory limit"),
355 #[cfg(target_os = "macos")]
356 RequestError::MemoryUnavailable => io::Error::other("Cannot monitor diagram worker memory"),
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use crate::MAX_EDGE;
364 #[cfg(unix)]
365 use std::sync::Arc;
366 #[test]
367 fn worker_protocol_rejects_invalid_or_truncated_data_before_allocating() {
368 let mut data = Vec::new();
369 for n in [2u32, 1, 8, 1, 1] {
370 data.extend_from_slice(&n.to_le_bytes());
371 }
372 data.extend_from_slice(&[255, 0, 0, 128, 0, 0, 255, 255]);
373 let p = response(&mut &data[..], (MAX_EDGE, MAX_EDGE))
374 .unwrap()
375 .unwrap();
376 assert_eq!((p.width, p.height), (2, 1));
377 assert_eq!(p.rgba, data[20..]);
378 data[12..16].copy_from_slice(&10000_u32.to_le_bytes());
380 data[16..20].copy_from_slice(&1500_u32.to_le_bytes());
381 let p = response(&mut &data[..], (MAX_EDGE, MAX_EDGE))
382 .unwrap()
383 .unwrap();
384 assert_eq!((p.original_width, p.original_height), (10000, 1500));
385 assert_eq!(p.rgba.len(), 8);
386 data[12..16].copy_from_slice(&(crate::svg::MAX_DIAGRAM_EDGE + 1).to_le_bytes());
387 assert!(response(&mut &data[..], (MAX_EDGE, MAX_EDGE)).is_err());
388 data[12..16].copy_from_slice(&32000_u32.to_le_bytes());
389 data[16..20].copy_from_slice(&32000_u32.to_le_bytes());
390 assert!(response(&mut &data[..], (MAX_EDGE, MAX_EDGE)).is_err());
391 data[12..16].copy_from_slice(&1_u32.to_le_bytes());
392 data[16..20].copy_from_slice(&1_u32.to_le_bytes());
393 assert!(response(&mut &data[..27], (MAX_EDGE, MAX_EDGE)).is_err());
394 assert!(response(&mut &data[..], (1, 1)).is_err());
395 assert!(
396 response(&mut &[0; 20][..], (MAX_EDGE, MAX_EDGE))
397 .unwrap()
398 .is_none()
399 );
400 data[8..12].copy_from_slice(&u32::MAX.to_le_bytes());
401 assert!(response(&mut &data[..], (MAX_EDGE, MAX_EDGE)).is_err());
402 let mut request = Vec::from(&b"NPV1"[..]);
403 for n in [MAX_EDGE, MAX_EDGE, MAX_SOURCE as u32 + 1] {
404 request.extend_from_slice(&n.to_le_bytes());
405 }
406 request.push(0);
407 request.extend_from_slice(&1_f64.to_le_bytes());
408 let mut output = Vec::new();
409 assert!(serve(&mut &request[..], &mut output).is_err());
410 assert!(output.is_empty());
411 request[12..16].copy_from_slice(&1_u32.to_le_bytes());
414 for (magic, flags) in [(b"NPV1", 2), (b"NPV2", 4), (b"NPV2", 255)] {
415 request[..4].copy_from_slice(magic);
416 request[16] = flags;
417 let error = serve(&mut &request[..], &mut output).unwrap_err();
418 assert_eq!(error.kind(), io::ErrorKind::InvalidData);
419 assert!(output.is_empty());
420 }
421 }
422 #[cfg(unix)]
423 #[test]
424 fn a_reply_after_a_short_deadline_is_rejected() {
425 let child = Command::new("sleep")
426 .arg("30")
427 .stdin(Stdio::piped())
428 .stdout(Stdio::piped())
429 .spawn()
430 .unwrap();
431 let mut worker = Worker::connect(child).unwrap();
432 let (send, receive) = mpsc::sync_channel(1);
433 worker.replies = receive;
434 let delayed = thread::spawn(move || {
435 thread::sleep(Duration::from_millis(10));
436 let _ = send.send(Ok(None));
437 });
438 let error = worker
439 .render(
440 "flowchart LR; A-->B",
441 false,
442 RasterOptions::scaled(1.),
443 Duration::from_millis(1),
444 &AtomicBool::new(false),
445 )
446 .err()
447 .unwrap();
448 assert_eq!(error.kind(), io::ErrorKind::TimedOut);
449 assert!(worker.requests.is_none());
450 assert_eq!(worker.requests_completed(), 0);
451 delayed.join().unwrap();
452 }
453
454 #[cfg(unix)]
455 #[test]
456 fn timeouts_and_cancellation_kill_and_reap_partial_replies() {
457 for cancel_after in [None, Some(Duration::from_millis(30))] {
458 let child = Command::new("sh")
459 .args(["-c", "printf '\\001'; exec sleep 30"])
460 .stdin(Stdio::piped())
461 .stdout(Stdio::piped())
462 .stderr(Stdio::null())
463 .spawn()
464 .unwrap();
465 let mut worker = Worker::connect(child).unwrap();
466 let pid = worker.child.id();
467 let cancel = Arc::new(AtomicBool::new(false));
468 let cancellation = cancel_after.map(|duration| {
469 let cancel = cancel.clone();
470 thread::spawn(move || {
471 thread::sleep(duration);
472 cancel.store(true, Ordering::Relaxed);
473 })
474 });
475 let started = Instant::now();
476 let error = worker
477 .render(
478 "flowchart LR; A-->B",
479 false,
480 RasterOptions::scaled(1.),
481 Duration::from_millis(100),
482 &cancel,
483 )
484 .err()
485 .unwrap();
486 assert_eq!(
487 error.kind(),
488 if cancel_after.is_some() {
489 io::ErrorKind::Interrupted
490 } else {
491 io::ErrorKind::TimedOut
492 }
493 );
494 assert!(worker.requests.is_none());
495 if let Some(thread) = cancellation {
496 thread.join().unwrap();
497 }
498 assert!(started.elapsed() < Duration::from_secs(3));
499 assert_eq!(unsafe { libc::kill(pid as libc::pid_t, 0) }, -1);
501 assert_eq!(io::Error::last_os_error().raw_os_error(), Some(libc::ESRCH));
502 assert!(
503 worker
504 .render(
505 "flowchart LR; A-->B",
506 false,
507 RasterOptions::scaled(1.),
508 Duration::from_millis(100),
509 &AtomicBool::new(false)
510 )
511 .is_err()
512 );
513 }
514 }
515 #[cfg(target_os = "macos")]
518 #[test]
519 fn macos_sandbox_denies_file_reads_writes_network_and_processes() {
520 let file = std::env::temp_dir().join(format!("document-graphics-{}", std::process::id()));
521 std::fs::write(&file, b"private").unwrap();
522 let output = Command::new(std::env::current_exe().unwrap())
523 .args(["worker::tests::sandbox_probe", "--exact", "--ignored"])
524 .env("DOCUMENT_GRAPHICS_SANDBOX_PROBE", &file)
525 .output()
526 .unwrap();
527 let contents = std::fs::read(&file).unwrap();
528 std::fs::remove_file(&file).unwrap();
529 let report = String::from_utf8_lossy(&output.stdout);
530 assert!(
531 output.status.success() && report.contains("1 passed"),
532 "{report}{}",
533 String::from_utf8_lossy(&output.stderr)
534 );
535 assert_eq!(contents, b"private");
536 }
537 #[cfg(target_os = "macos")]
538 #[test]
539 #[ignore = "run in a child process by the macOS sandbox test"]
540 fn sandbox_probe() {
541 let Some(file) = std::env::var_os("DOCUMENT_GRAPHICS_SANDBOX_PROBE") else {
543 return;
544 };
545 let spawn = || Command::new("/usr/bin/true").status();
547 assert!(std::fs::read(&file).is_ok() && spawn().is_ok());
548 assert!(std::net::TcpListener::bind("127.0.0.1:0").is_ok());
549 restrict_worker().unwrap();
550 assert!(std::fs::read(&file).is_err(), "read a file");
551 assert!(std::fs::write(&file, b"changed").is_err(), "wrote a file");
552 assert!(
553 std::net::TcpListener::bind("127.0.0.1:0").is_err(),
554 "opened a socket"
555 );
556 assert!(spawn().is_err(), "started a process");
557 }
558}