1use std::io::{self, Read, Write};
29use std::path::{Path, PathBuf};
30use std::process::{Child, Command, Stdio};
31use std::sync::Arc;
32use std::sync::mpsc::{self, RecvTimeoutError};
33use std::thread;
34use std::time::{Instant, UNIX_EPOCH};
35
36use url::Url;
37
38use crate::error::Error;
39use crate::resolve::ProxyStep;
40
41use super::policy::PacPolicy;
42use super::result::parse_find_proxy_result;
43use super::{PacEvaluator, PacScript, budget, hostfn};
44
45const MAGIC: [u8; 6] = *b"PWPAC\0";
46const VERSION: u16 = 2;
47
48const EVALUATE: u8 = 1;
50const ANSWER: u8 = 2;
51const LOOKUP: u8 = 3;
53const DONE: u8 = 4;
54const FAILED: u8 = 5;
55const SANDBOX: u8 = 6;
56
57const SECCOMP: &str = "seccomp";
60
61const MAX_WORKER_FRAME: usize = 1024 * 1024;
66
67#[cfg(windows)]
73const WORKER_COMMIT_LIMIT: usize = 512 * 1024 * 1024;
74#[cfg(target_os = "linux")]
75const WORKER_ADDRESS_SPACE_LIMIT: u64 = 1024 * 1024 * 1024;
76
77#[derive(Debug, Clone)]
107pub struct SubprocessEvaluator {
108 worker: PathBuf,
109 policy: PacPolicy,
110 allow_unsandboxed: bool,
111}
112
113impl SubprocessEvaluator {
114 #[must_use]
116 pub fn new(worker: impl Into<PathBuf>, policy: PacPolicy) -> Self {
117 Self {
118 worker: worker.into(),
119 policy,
120 allow_unsandboxed: false,
121 }
122 }
123
124 #[must_use]
129 pub fn allow_unsandboxed(mut self) -> Self {
130 self.allow_unsandboxed = true;
131 self
132 }
133
134 #[must_use]
136 pub fn worker(&self) -> &Path {
137 &self.worker
138 }
139
140 #[must_use]
142 pub fn policy(&self) -> &PacPolicy {
143 &self.policy
144 }
145}
146
147impl PacEvaluator for SubprocessEvaluator {
148 fn evaluate(&self, script: &PacScript, url: &Url, host: &str) -> Result<Vec<ProxyStep>, Error> {
149 let url = crate::pac::sanitize_url(url);
150 let timeout = self.policy.timeout();
151 if let Some(timeout) = timeout.filter(|timeout| timeout.is_zero()) {
152 return Err(Error::PacTimeout { timeout });
153 }
154 let deadline = timeout.and_then(|timeout| Instant::now().checked_add(timeout));
155 let slot =
158 Arc::new(
159 budget::SLOTS
160 .acquire(deadline)
161 .ok_or_else(|| Error::PacSaturated {
162 timeout: timeout.unwrap_or_default(),
163 limit: budget::SLOTS.limit(),
164 })?,
165 );
166
167 let mut worker = Worker::start(&self.worker)?;
168 let request = evaluate_request(script.source(), url.as_str(), host, &self.policy);
169 let (input, output) = worker.pipes();
170 let policy = self.policy;
171 let allow_unsandboxed = self.allow_unsandboxed;
172 let (sender, receiver) = mpsc::sync_channel(1);
173 let thread_slot = Arc::clone(&slot);
178 let spawned = thread::Builder::new()
179 .name("proxy-watch-pac-worker".to_owned())
180 .spawn(move || {
181 let _slot = thread_slot;
182 let _ = sender.send(converse(
183 output,
184 input,
185 &request,
186 &policy,
187 allow_unsandboxed,
188 ));
189 });
190 if let Err(source) = spawned {
191 return Err(Error::io("spawning the PAC worker thread", source));
192 }
193
194 let outcome = match deadline {
195 Some(deadline) => {
196 receiver.recv_timeout(deadline.saturating_duration_since(Instant::now()))
197 }
198 None => receiver.recv().map_err(|_| RecvTimeoutError::Disconnected),
199 };
200 match outcome {
201 Ok(Ok(raw)) => parse_find_proxy_result(&raw),
202 Ok(Err(error)) => {
203 #[cfg(target_os = "linux")]
204 if worker.killed_by_seccomp() {
205 return Err(Error::pac_evaluation(
206 "the PAC worker's seccomp filter killed it: the script led it to a \
207 system call outside what an evaluation needs",
208 ));
209 }
210 Err(error)
211 }
212 Err(RecvTimeoutError::Timeout) => Err(Error::PacTimeout {
213 timeout: timeout.unwrap_or_default(),
214 }),
215 Err(RecvTimeoutError::Disconnected) => Err(Error::pac_evaluation(
216 "the PAC worker thread ended without producing a result",
217 )),
218 }
219 }
220}
221
222struct Worker {
226 child: Child,
227 #[cfg(windows)]
229 _job: Option<job::Job>,
230}
231
232impl Worker {
233 fn start(path: &Path) -> Result<Self, Error> {
234 if !path.is_absolute() {
237 return Err(Error::io(
238 "starting the PAC worker",
239 io::Error::new(
240 io::ErrorKind::InvalidInput,
241 "the worker path is not absolute",
242 ),
243 ));
244 }
245 let mut command = Command::new(path);
246 command
247 .stdin(Stdio::piped())
248 .stdout(Stdio::piped())
249 .stderr(Stdio::null())
250 .env_clear();
251 #[cfg(windows)]
252 if let Some(value) = std::env::var_os("SystemRoot") {
253 command.env("SystemRoot", value);
254 }
255 if let Some(directory) = path.parent().filter(|dir| !dir.as_os_str().is_empty()) {
256 command.current_dir(directory);
257 }
258 #[cfg(target_os = "linux")]
259 limit_address_space(&mut command);
260
261 let child = command
262 .spawn()
263 .map_err(|source| Error::io("starting the PAC worker", source))?;
264 #[cfg(windows)]
267 let mut child = child;
268 #[cfg(windows)]
269 let _job = match job::confine(&child) {
270 Ok(job) => Some(job),
271 Err(source) => {
272 let _ = child.kill();
273 let _ = child.wait();
274 return Err(Error::io("limiting the PAC worker's memory", source));
275 }
276 };
277 Ok(Self {
278 child,
279 #[cfg(windows)]
280 _job,
281 })
282 }
283
284 fn pipes(&mut self) -> (std::process::ChildStdin, std::process::ChildStdout) {
285 let (Some(input), Some(output)) = (self.child.stdin.take(), self.child.stdout.take())
286 else {
287 unreachable!("both pipes are requested in `start` and taken once");
288 };
289 (input, output)
290 }
291
292 #[cfg(target_os = "linux")]
295 fn killed_by_seccomp(&mut self) -> bool {
296 use std::os::unix::process::ExitStatusExt;
297
298 let _ = self.child.kill();
299 self.child
300 .wait()
301 .is_ok_and(|status| status.signal() == Some(libc::SIGSYS))
302 }
303}
304
305impl Drop for Worker {
306 fn drop(&mut self) {
307 let _ = self.child.kill();
308 let _ = self.child.wait();
309 }
310}
311
312#[cfg(target_os = "linux")]
313fn limit_address_space(command: &mut Command) {
314 use std::os::unix::process::CommandExt;
315
316 let limit = libc::rlimit {
317 rlim_cur: WORKER_ADDRESS_SPACE_LIMIT as libc::rlim_t,
318 rlim_max: WORKER_ADDRESS_SPACE_LIMIT as libc::rlim_t,
319 };
320 unsafe {
323 command.pre_exec(move || {
324 if libc::setrlimit(libc::RLIMIT_AS, &limit) == 0 {
325 Ok(())
326 } else {
327 Err(io::Error::last_os_error())
328 }
329 });
330 }
331}
332
333#[cfg(windows)]
334mod job {
335 use std::io;
336 use std::os::windows::io::AsRawHandle;
337 use std::process::Child;
338
339 use windows::Win32::Foundation::{CloseHandle, HANDLE};
340 use windows::Win32::System::JobObjects::{
341 AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
342 JOB_OBJECT_LIMIT_PROCESS_MEMORY, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
343 JobObjectExtendedLimitInformation, SetInformationJobObject,
344 };
345 use windows::core::PCWSTR;
346
347 use super::WORKER_COMMIT_LIMIT;
348
349 pub(super) struct Job(HANDLE);
350
351 impl Drop for Job {
352 fn drop(&mut self) {
353 let _ = unsafe { CloseHandle(self.0) };
355 }
356 }
357
358 pub(super) fn confine(child: &Child) -> io::Result<Job> {
361 let error = |error: windows::core::Error| io::Error::from_raw_os_error(error.code().0);
362 let job = Job(unsafe { CreateJobObjectW(None, PCWSTR::null()) }.map_err(error)?);
364 let mut limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
365 limits.BasicLimitInformation.LimitFlags =
366 JOB_OBJECT_LIMIT_PROCESS_MEMORY | JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
367 limits.ProcessMemoryLimit = WORKER_COMMIT_LIMIT;
368 unsafe {
370 SetInformationJobObject(
371 job.0,
372 JobObjectExtendedLimitInformation,
373 (&raw const limits).cast(),
374 u32::try_from(size_of_val(&limits)).expect("a fixed-size structure"),
375 )
376 }
377 .map_err(error)?;
378 unsafe { AssignProcessToJobObject(job.0, HANDLE(child.as_raw_handle())) }.map_err(error)?;
380 Ok(job)
381 }
382
383 #[cfg(test)]
384 mod tests {
385 use std::path::Path;
386 use std::time::{Duration, Instant};
387
388 use windows::Win32::System::JobObjects::{IsProcessInJob, QueryInformationJobObject};
389
390 use super::super::Worker;
391 use super::*;
392
393 #[test]
396 fn a_worker_runs_under_its_commit_limit_and_dies_with_its_job() {
397 let cmd = Path::new(&std::env::var_os("SystemRoot").unwrap()).join(r"System32\cmd.exe");
398 let mut worker = Worker::start(&cmd).unwrap();
399 let job = worker._job.take().unwrap();
400
401 let mut inside = windows::core::BOOL(0);
402 unsafe {
404 IsProcessInJob(
405 HANDLE(worker.child.as_raw_handle()),
406 Some(job.0),
407 &mut inside,
408 )
409 }
410 .unwrap();
411 assert!(inside.as_bool());
412 let mut limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
413 unsafe {
415 QueryInformationJobObject(
416 Some(job.0),
417 JobObjectExtendedLimitInformation,
418 (&raw mut limits).cast(),
419 u32::try_from(size_of_val(&limits)).unwrap(),
420 None,
421 )
422 }
423 .unwrap();
424 assert!(
425 limits
426 .BasicLimitInformation
427 .LimitFlags
428 .contains(JOB_OBJECT_LIMIT_PROCESS_MEMORY)
429 );
430 assert_eq!(limits.ProcessMemoryLimit, WORKER_COMMIT_LIMIT);
431
432 drop(job);
434 let started = Instant::now();
435 while worker.child.try_wait().unwrap().is_none() {
436 assert!(
437 started.elapsed() < Duration::from_secs(5),
438 "the worker outlived its job"
439 );
440 std::thread::sleep(Duration::from_millis(10));
441 }
442 }
443 }
444}
445
446fn evaluate_request(script: &str, url: &str, host: &str, policy: &PacPolicy) -> Vec<Vec<u8>> {
450 let now = policy
451 .now()
452 .map_or_else(String::new, |now| match now.duration_since(UNIX_EPOCH) {
453 Ok(after) => after.as_nanos().to_string(),
454 Err(before) => format!("-{}", before.duration().as_nanos()),
455 });
456 [
457 script.to_owned(),
458 url.to_owned(),
459 host.to_owned(),
460 policy.my_ip_address().to_string(),
461 policy.local_utc_offset().to_string(),
462 now,
463 ]
464 .map(String::into_bytes)
465 .into()
466}
467
468fn converse(
472 mut from_worker: impl Read,
473 mut to_worker: impl Write,
474 request: &[Vec<u8>],
475 policy: &PacPolicy,
476 allow_unsandboxed: bool,
477) -> Result<String, Error> {
478 let broken = |error: io::Error| {
479 Error::pac_evaluation(format!("the PAC worker broke the protocol: {error}"))
480 };
481 let unexpected = |tag: u8, fields: &[String]| {
482 broken(invalid(format!(
483 "unexpected frame {tag} with {} fields",
484 fields.len()
485 )))
486 };
487 write_header(&mut to_worker).map_err(broken)?;
488 read_header(&mut from_worker).map_err(broken)?;
489 match read_frame(&mut from_worker, MAX_WORKER_FRAME).map_err(broken)? {
490 (SANDBOX, fields) if fields.len() == 1 => {
491 if fields[0] != SECCOMP && !allow_unsandboxed {
492 return Err(Error::io(
493 "starting the PAC worker",
494 io::Error::new(
495 io::ErrorKind::PermissionDenied,
496 format!(
497 "the worker is not sandboxed ({}), and the evaluator does not \
498 allow that",
499 fields[0]
500 ),
501 ),
502 ));
503 }
504 }
505 (tag, fields) => return Err(unexpected(tag, &fields)),
506 }
507 write_frame(&mut to_worker, EVALUATE, request).map_err(broken)?;
508 loop {
509 let (tag, mut fields) = read_frame(&mut from_worker, MAX_WORKER_FRAME).map_err(broken)?;
510 match (tag, fields.len()) {
511 (LOOKUP, 1) => {
512 let host = fields.remove(0);
513 let answer = hostfn::resolve_ipv4(&host, policy)
514 .map_or_else(Vec::new, |address| address.to_string().into_bytes());
515 write_frame(&mut to_worker, ANSWER, &[answer]).map_err(broken)?;
516 }
517 (DONE, 1) => return Ok(fields.remove(0)),
518 (FAILED, 1) => return Err(Error::pac_evaluation(fields.remove(0))),
519 _ => return Err(unexpected(tag, &fields)),
520 }
521 }
522}
523
524fn invalid(message: impl Into<String>) -> io::Error {
525 io::Error::new(io::ErrorKind::InvalidData, message.into())
526}
527
528fn write_header(to: &mut impl Write) -> io::Result<()> {
529 to.write_all(&MAGIC)?;
530 to.write_all(&VERSION.to_le_bytes())?;
531 to.flush()
532}
533
534fn read_header(from: &mut impl Read) -> io::Result<()> {
535 let mut header = [0u8; 8];
536 from.read_exact(&mut header)?;
537 if header[..6] != MAGIC {
538 return Err(invalid("not a PAC worker"));
539 }
540 let version = u16::from_le_bytes([header[6], header[7]]);
541 if version != VERSION {
542 return Err(invalid(format!(
543 "protocol version {version}, expected {VERSION}; the worker is from another release"
544 )));
545 }
546 Ok(())
547}
548
549fn write_frame(to: &mut impl Write, tag: u8, fields: &[impl AsRef<[u8]>]) -> io::Result<()> {
550 let length = |len: usize| {
551 u32::try_from(len)
552 .map(u32::to_le_bytes)
553 .map_err(|_| invalid("a frame too large to send"))
554 };
555 let mut body = vec![tag];
556 for field in fields {
557 let field = field.as_ref();
558 body.extend(length(field.len())?);
559 body.extend(field);
560 }
561 to.write_all(&length(body.len())?)?;
562 to.write_all(&body)?;
563 to.flush()
564}
565
566fn read_frame(from: &mut impl Read, max: usize) -> io::Result<(u8, Vec<String>)> {
568 let mut length = [0u8; 4];
569 from.read_exact(&mut length)?;
570 let length = u32::from_le_bytes(length) as usize;
571 if length == 0 || length > max {
572 return Err(invalid(format!("a frame of {length} bytes")));
573 }
574 let mut body = vec![0u8; length];
575 from.read_exact(&mut body)?;
576
577 let (&tag, mut rest) = body.split_first().expect("length is not zero");
578 let mut fields = Vec::new();
579 while !rest.is_empty() {
580 let (size, tail) = rest
581 .split_first_chunk::<4>()
582 .ok_or_else(|| invalid("a truncated field length"))?;
583 let size = u32::from_le_bytes(*size) as usize;
584 if size > tail.len() {
585 return Err(invalid("a field longer than its frame"));
586 }
587 let (field, tail) = tail.split_at(size);
588 fields.push(
589 String::from_utf8(field.to_vec()).map_err(|_| invalid("a field that is not UTF-8"))?,
590 );
591 rest = tail;
592 }
593 Ok((tag, fields))
594}
595
596#[cfg(pac_quickjs)]
597pub use self::worker::serve_worker;
598
599#[cfg(pac_quickjs)]
602mod worker {
603 use std::cell::RefCell;
604 use std::io::{self, StdinLock, StdoutLock};
605 use std::net::{IpAddr, Ipv4Addr};
606 use std::process::ExitCode;
607 use std::time::Duration;
608
609 use super::*;
610
611 thread_local! {
612 static PARENT: RefCell<Option<(StdinLock<'static>, StdoutLock<'static>)>> =
613 const { RefCell::new(None) };
614 }
615
616 #[must_use]
623 pub fn serve_worker() -> ExitCode {
624 let mut input = io::stdin().lock();
625 let mut output = io::stdout().lock();
626 let sandbox = confine();
627 let request = write_header(&mut output)
628 .and_then(|()| write_frame(&mut output, SANDBOX, &[sandbox]))
629 .and_then(|()| read_header(&mut input))
630 .and_then(|()| read_frame(&mut input, usize::MAX))
631 .and_then(|frame| match frame {
632 (EVALUATE, fields) if fields.len() == 6 => Ok(fields),
633 _ => Err(invalid("the first frame is not a request")),
634 })
635 .and_then(|fields| Ok((policy(&fields)?, fields)));
636 let Ok((policy, fields)) = request else {
637 return ExitCode::FAILURE;
638 };
639
640 PARENT.set(Some((input, output)));
641 let result =
642 super::super::quickjs::run_raw(&fields[0], &fields[1], &fields[2], policy, None);
643 let Some((_, mut output)) = PARENT.take() else {
644 return ExitCode::FAILURE;
645 };
646 let sent = match result {
647 Ok(raw) => write_frame(&mut output, DONE, &[raw]),
648 Err(Error::PacEvaluation { reason }) => write_frame(&mut output, FAILED, &[reason]),
652 Err(error) => write_frame(&mut output, FAILED, &[error.to_string()]),
653 };
654 if sent.is_ok() {
655 ExitCode::SUCCESS
656 } else {
657 ExitCode::FAILURE
658 }
659 }
660
661 #[cfg(all(
664 target_os = "linux",
665 any(target_arch = "x86_64", target_arch = "aarch64")
666 ))]
667 fn confine() -> String {
668 match super::seccomp::confine() {
669 Ok(()) => SECCOMP.to_owned(),
670 Err(error) => format!("the kernel refused the seccomp filter: {error}"),
671 }
672 }
673
674 #[cfg(not(all(
675 target_os = "linux",
676 any(target_arch = "x86_64", target_arch = "aarch64")
677 )))]
678 fn confine() -> String {
679 "the worker has no sandbox on this platform".to_owned()
680 }
681
682 fn policy(fields: &[String]) -> io::Result<PacPolicy> {
685 let bad = |what: &str| invalid(format!("an unreadable {what}"));
686 let my_ip: IpAddr = fields[3].parse().map_err(|_| bad("myIpAddress"))?;
687 let offset: i32 = fields[4].parse().map_err(|_| bad("UTC offset"))?;
688 let mut policy = PacPolicy::new()
689 .with_timeout(None)
690 .with_my_ip_address(my_ip)
691 .with_local_utc_offset(offset);
692 if !fields[5].is_empty() {
693 let (negative, digits) = match fields[5].strip_prefix('-') {
694 Some(digits) => (true, digits),
695 None => (false, fields[5].as_str()),
696 };
697 let nanos: u128 = digits.parse().map_err(|_| bad("clock"))?;
698 let offset = Duration::new(
699 u64::try_from(nanos / 1_000_000_000).map_err(|_| bad("clock"))?,
700 (nanos % 1_000_000_000) as u32,
701 );
702 let now = if negative {
703 UNIX_EPOCH.checked_sub(offset)
704 } else {
705 UNIX_EPOCH.checked_add(offset)
706 };
707 policy = policy.with_now(now.ok_or_else(|| bad("clock"))?);
708 }
709 Ok(policy)
710 }
711
712 pub(in crate::pac) fn ask_parent(host: &str) -> Option<Option<Ipv4Addr>> {
716 PARENT.with_borrow_mut(|parent| {
717 let (input, output) = parent.as_mut()?;
718 let answer = write_frame(output, LOOKUP, &[host])
719 .and_then(|()| read_frame(input, usize::MAX))
720 .and_then(|frame| match frame {
721 (ANSWER, fields) if fields.len() == 1 && fields[0].is_empty() => Ok(None),
722 (ANSWER, fields) if fields.len() == 1 => fields[0]
723 .parse()
724 .map(Some)
725 .map_err(|_| invalid("an unreadable address")),
726 _ => Err(invalid("a reply that is not an answer")),
727 });
728 match answer {
729 Ok(answer) => Some(answer),
730 Err(_) => std::process::exit(1),
731 }
732 })
733 }
734}
735
736#[cfg(pac_quickjs)]
737pub(super) use self::worker::ask_parent;
738
739#[cfg(all(
746 pac_quickjs,
747 target_os = "linux",
748 any(target_arch = "x86_64", target_arch = "aarch64")
749))]
750mod seccomp {
751 use std::io;
752
753 use libc::{
754 BPF_ABS, BPF_JEQ, BPF_JGE, BPF_JMP, BPF_K, BPF_LD, BPF_RET, BPF_W, SECCOMP_RET_ALLOW,
755 SECCOMP_RET_KILL_PROCESS, c_long, sock_filter, sock_fprog,
756 };
757
758 #[cfg(target_arch = "x86_64")]
760 const ARCH: u32 = 0xC000_003E;
761 #[cfg(target_arch = "aarch64")]
762 const ARCH: u32 = 0xC000_00B7;
763
764 const NR: u32 = 0;
768 const ARCH_AT: u32 = 4;
769 const fn argument(index: u32) -> u32 {
770 16 + 8 * index
771 }
772
773 const ALLOWED: &[c_long] = &[
774 libc::SYS_exit_group,
775 libc::SYS_exit,
776 libc::SYS_brk,
777 libc::SYS_munmap,
778 libc::SYS_mremap,
779 libc::SYS_madvise,
780 libc::SYS_mprotect,
781 libc::SYS_futex,
782 libc::SYS_rt_sigreturn,
783 libc::SYS_rt_sigprocmask,
784 libc::SYS_rt_sigaction,
785 libc::SYS_sigaltstack,
786 libc::SYS_clock_gettime,
787 libc::SYS_gettimeofday,
788 #[cfg(target_arch = "x86_64")]
789 libc::SYS_time,
790 libc::SYS_getrandom,
791 ];
792
793 const WITH_ARGUMENT: &[(c_long, u32, &[u32])] = &[
795 (libc::SYS_read, 0, &[0]),
796 (libc::SYS_write, 0, &[1, 2]),
797 (libc::SYS_writev, 0, &[1, 2]),
798 (libc::SYS_mmap, 4, &[u32::MAX]),
800 ];
801
802 fn statement(code: u32, k: u32) -> sock_filter {
803 jump(code, k, 0, 0)
804 }
805
806 fn jump(code: u32, k: u32, jt: usize, jf: usize) -> sock_filter {
807 let offset = |n: usize| u8::try_from(n).expect("a short filter");
808 sock_filter {
809 code: u16::try_from(code).expect("a BPF opcode"),
810 jt: offset(jt),
811 jf: offset(jf),
812 k,
813 }
814 }
815
816 pub(super) fn filter() -> Vec<sock_filter> {
817 let load = |at: u32| statement(BPF_LD | BPF_W | BPF_ABS, at);
818 let equal = |k: u32, jt: usize, jf: usize| jump(BPF_JMP | BPF_JEQ | BPF_K, k, jt, jf);
819 let allow = statement(BPF_RET | BPF_K, SECCOMP_RET_ALLOW);
820 let kill = statement(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS);
821 let mut program = vec![load(ARCH_AT), equal(ARCH, 1, 0), kill, load(NR)];
822 if cfg!(target_arch = "x86_64") {
824 program.extend([jump(BPF_JMP | BPF_JGE | BPF_K, 0x4000_0000, 0, 1), kill]);
825 }
826 for &call in ALLOWED {
827 program.extend([equal(call as u32, 0, 1), allow]);
828 }
829 for &(call, index, values) in WITH_ARGUMENT {
830 let count = values.len();
831 program.extend([equal(call as u32, 0, count + 3), load(argument(index))]);
832 for (position, &value) in values.iter().enumerate() {
833 program.push(equal(value, count - position, 0));
834 }
835 program.extend([kill, allow]);
836 }
837 program.push(kill);
838 program
839 }
840
841 pub(super) fn install(program: &[sock_filter]) -> io::Result<()> {
844 let program = sock_fprog {
845 len: u16::try_from(program.len()).expect("a short filter"),
846 filter: program.as_ptr().cast_mut(),
847 };
848 let result = unsafe {
851 if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0 {
852 return Err(io::Error::last_os_error());
853 }
854 libc::syscall(
855 libc::SYS_seccomp,
856 libc::c_ulong::from(libc::SECCOMP_SET_MODE_FILTER),
857 libc::SECCOMP_FILTER_FLAG_TSYNC,
858 &raw const program,
859 )
860 };
861 match result {
862 0 => Ok(()),
863 -1 => Err(io::Error::last_os_error()),
864 thread => Err(io::Error::other(format!(
865 "thread {thread} could not take the filter"
866 ))),
867 }
868 }
869
870 unsafe extern "C" {
871 fn tzset();
872 }
873
874 pub(super) fn confine() -> io::Result<()> {
877 unsafe { tzset() };
880 install(&filter())
881 }
882}
883
884#[cfg(test)]
885mod tests {
886 use std::io::Cursor;
887 use std::time::Duration;
888
889 use super::*;
890
891 fn from_worker(frames: &[(u8, &[&str])]) -> Vec<u8> {
893 let confined: &[(u8, &[&str])] = &[(SANDBOX, &[SECCOMP])];
894 from_unconfined(&[confined, frames].concat())
895 }
896
897 fn from_unconfined(frames: &[(u8, &[&str])]) -> Vec<u8> {
898 let mut bytes = Vec::new();
899 write_header(&mut bytes).unwrap();
900 for (tag, fields) in frames {
901 write_frame(&mut bytes, *tag, fields).unwrap();
902 }
903 bytes
904 }
905
906 fn talk(worker_said: Vec<u8>, policy: &PacPolicy) -> (Result<String, Error>, Vec<u8>) {
907 talk_allowing(worker_said, policy, false)
908 }
909
910 fn talk_allowing(
911 worker_said: Vec<u8>,
912 policy: &PacPolicy,
913 allow_unsandboxed: bool,
914 ) -> (Result<String, Error>, Vec<u8>) {
915 let mut sent = Vec::new();
916 let request = evaluate_request("script", "http://example.net/", "example.net", policy);
917 let result = converse(
918 Cursor::new(worker_said),
919 &mut sent,
920 &request,
921 policy,
922 allow_unsandboxed,
923 );
924 (result, sent)
925 }
926
927 fn broken(result: Result<String, Error>) -> String {
928 match result {
929 Err(Error::PacEvaluation { reason }) => reason,
930 other => panic!("expected a protocol failure, got {other:?}"),
931 }
932 }
933
934 #[cfg(windows)]
937 #[test]
938 fn a_worker_starts_with_an_empty_environment() {
939 let cmd = Path::new(&std::env::var_os("SystemRoot").unwrap()).join(r"System32\cmd.exe");
940 let mut worker = Worker::start(&cmd).unwrap();
941 let (mut input, mut output) = worker.pipes();
942 input.write_all(b"set\r\nexit\r\n").unwrap();
943 drop(input);
944 let mut said = String::new();
945 output.read_to_string(&mut said).unwrap();
946 let mut names: Vec<&str> = said
947 .lines()
948 .filter_map(|line| line.split_once('=').map(|(name, _)| name))
949 .filter(|name| !["COMSPEC", "PATHEXT", "PROMPT"].contains(name))
950 .collect();
951 names.sort_unstable();
952 assert_eq!(names, ["SystemRoot"], "{said}");
953 }
954
955 #[test]
956 fn an_answer_is_returned_raw_and_a_failure_as_an_evaluation_error() {
957 let policy = PacPolicy::new();
958 let (result, _) = talk(from_worker(&[(DONE, &["PROXY a:1; DIRECT"])]), &policy);
959 assert_eq!(result.unwrap(), "PROXY a:1; DIRECT");
960 let (result, _) = talk(from_worker(&[(FAILED, &["boom"])]), &policy);
961 assert!(broken(result).contains("boom"));
962 }
963
964 #[test]
968 fn a_lookup_is_answered_under_the_parent_policy() {
969 let dns = PacPolicy::new().with_dns_resolution(true);
970 for (policy, answer) in [
971 (PacPolicy::new(), ""),
972 (dns, ""),
973 (dns.with_internal_addresses(true), "127.0.0.1"),
974 ] {
975 let said = from_worker(&[(LOOKUP, &["127.0.0.1"]), (DONE, &["DIRECT"])]);
976 let (result, sent) = talk(said, &policy);
977 assert_eq!(result.unwrap(), "DIRECT");
978 let mut sent = Cursor::new(sent);
979 read_header(&mut sent).unwrap();
980 assert_eq!(read_frame(&mut sent, usize::MAX).unwrap().0, EVALUATE);
981 assert_eq!(
982 read_frame(&mut sent, usize::MAX).unwrap(),
983 (ANSWER, vec![answer.to_owned()]),
984 "{policy:?}"
985 );
986 }
987 }
988
989 #[test]
990 fn a_worker_that_breaks_the_protocol_is_an_evaluation_error() {
991 let policy = PacPolicy::new();
992 assert!(
994 broken(talk(b"Microsoft Windows\r\n".to_vec(), &policy).0).contains("not a PAC worker")
995 );
996 let mut other = MAGIC.to_vec();
998 other.extend((VERSION + 1).to_le_bytes());
999 assert!(broken(talk(other, &policy).0).contains(&format!("version {}", VERSION + 1)));
1000 assert!(
1002 broken(talk(from_unconfined(&[(DONE, &["DIRECT"])]), &policy).0)
1003 .contains("unexpected frame")
1004 );
1005 assert!(!broken(talk(from_worker(&[]), &policy).0).is_empty());
1007 let mut huge = from_worker(&[]);
1009 huge.extend(u32::MAX.to_le_bytes());
1010 assert!(broken(talk(huge, &policy).0).contains("4294967295 bytes"));
1011 let mut cut = from_worker(&[(DONE, &["DIRECT"])]);
1013 cut.pop();
1014 assert!(!broken(talk(cut, &policy).0).is_empty());
1015 for said in [
1017 from_worker(&[(9, &["x"])]),
1018 from_worker(&[(EVALUATE, &["x"])]),
1019 from_worker(&[(DONE, &["a", "b"])]),
1020 ] {
1021 assert!(broken(talk(said, &policy).0).contains("unexpected frame"));
1022 }
1023 let mut bytes = from_worker(&[]);
1025 write_frame(&mut bytes, DONE, &[[0xffu8]]).unwrap();
1026 assert!(broken(talk(bytes, &policy).0).contains("UTF-8"));
1027 }
1028
1029 #[test]
1032 fn an_unconfined_worker_gets_the_script_only_when_allowed() {
1033 let policy = PacPolicy::new();
1034 let said = from_unconfined(&[(SANDBOX, &["no sandbox here"]), (DONE, &["DIRECT"])]);
1035
1036 let (result, sent) = talk(said.clone(), &policy);
1037 match result {
1038 Err(Error::Io { source, .. }) => {
1039 assert_eq!(source.kind(), io::ErrorKind::PermissionDenied);
1040 assert!(source.to_string().contains("no sandbox here"), "{source}");
1041 }
1042 other => panic!("expected a refusal, got {other:?}"),
1043 }
1044 let mut header = Vec::new();
1045 write_header(&mut header).unwrap();
1046 assert_eq!(sent, header);
1047
1048 let (result, sent) = talk_allowing(said, &policy, true);
1049 assert_eq!(result.unwrap(), "DIRECT");
1050 let mut sent = Cursor::new(sent);
1051 read_header(&mut sent).unwrap();
1052 assert_eq!(read_frame(&mut sent, usize::MAX).unwrap().0, EVALUATE);
1053 }
1054
1055 #[cfg(all(
1059 pac_quickjs,
1060 target_os = "linux",
1061 any(target_arch = "x86_64", target_arch = "aarch64")
1062 ))]
1063 #[test]
1064 fn the_seccomp_filter_kills_every_call_it_does_not_allow() {
1065 let program = seccomp::filter();
1066 let status_after = |call: &dyn Fn()| {
1067 unsafe {
1069 let pid = libc::fork();
1070 assert!(pid >= 0, "{}", io::Error::last_os_error());
1071 if pid == 0 {
1072 if seccomp::install(&program).is_err() {
1073 libc::_exit(2);
1074 }
1075 call();
1076 libc::_exit(0);
1077 }
1078 let mut status = 0;
1079 assert_eq!(libc::waitpid(pid, &mut status, 0), pid);
1080 status
1081 }
1082 };
1083 let survives =
1084 |status: libc::c_int| libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0;
1085 let killed = |status: libc::c_int| {
1086 libc::WIFSIGNALED(status) && libc::WTERMSIG(status) == libc::SIGSYS
1087 };
1088 let null = std::ptr::null_mut::<libc::c_void>();
1089
1090 let allowed: [&dyn Fn(); 4] = [
1092 &|| {},
1093 &|| unsafe {
1094 libc::syscall(libc::SYS_read, 0, null, 0);
1095 },
1096 &|| unsafe {
1097 libc::syscall(libc::SYS_write, 2, null, 0);
1098 },
1099 &|| unsafe {
1100 libc::syscall(
1101 libc::SYS_mmap,
1102 null,
1103 4096,
1104 libc::PROT_READ,
1105 libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
1106 -1,
1107 0,
1108 );
1109 },
1110 ];
1111 for (index, call) in allowed.into_iter().enumerate() {
1112 let status = status_after(call);
1113 assert!(survives(status), "allowed call {index}: status {status:#x}");
1114 }
1115
1116 let refused: [&dyn Fn(); 5] = [
1117 &|| unsafe {
1118 libc::syscall(libc::SYS_getpid);
1119 },
1120 &|| unsafe {
1121 libc::syscall(
1122 libc::SYS_openat,
1123 libc::AT_FDCWD,
1124 c"/".as_ptr(),
1125 libc::O_RDONLY,
1126 );
1127 },
1128 &|| unsafe {
1129 libc::syscall(libc::SYS_socket, libc::AF_INET, libc::SOCK_STREAM, 0);
1130 },
1131 &|| unsafe {
1133 libc::syscall(libc::SYS_write, 3, null, 0);
1134 },
1135 &|| unsafe {
1136 libc::syscall(
1137 libc::SYS_mmap,
1138 null,
1139 4096,
1140 libc::PROT_READ,
1141 libc::MAP_PRIVATE,
1142 0,
1143 0,
1144 );
1145 },
1146 ];
1147 for (index, call) in refused.into_iter().enumerate() {
1148 let status = status_after(call);
1149 assert!(killed(status), "refused call {index}: status {status:#x}");
1150 }
1151 }
1152
1153 #[test]
1154 fn a_pinned_clock_before_the_epoch_is_sent_signed() {
1155 let policy = PacPolicy::new().with_now(UNIX_EPOCH - Duration::from_millis(1500));
1156 let request = evaluate_request("", "", "", &policy);
1157 assert_eq!(request[5], b"-1500000000");
1158 }
1159}