1use std::process::{Command, Stdio};
28use std::time::{Duration, Instant};
29
30use crate::{Error, Language, Result};
31
32use super::executor::Executor;
33use super::ExecutionResult;
34
35#[derive(Debug, Clone)]
37pub struct SandboxedPythonExecutor {
38 interpreter: String,
40 max_output_bytes: usize,
42 blocked_modules: Vec<String>,
44 allow_file_io: bool,
46}
47
48impl Default for SandboxedPythonExecutor {
49 fn default() -> Self {
50 Self::new()
51 }
52}
53
54impl SandboxedPythonExecutor {
55 const DEFAULT_BLOCKED_MODULES: &'static [&'static str] = &[
57 "os",
59 "subprocess",
60 "sys",
61 "shutil",
62 "pathlib",
63 "glob",
64 "tempfile",
65 "socket",
67 "http",
68 "urllib",
69 "requests",
70 "aiohttp",
71 "ftplib",
72 "smtplib",
73 "ssl",
74 "code",
76 "codeop",
77 "compile",
78 "importlib",
79 "runpy",
80 "ast",
81 "dis",
82 "inspect",
83 "ctypes",
85 "cffi",
86 "multiprocessing",
87 "threading",
88 "concurrent",
89 "_thread",
90 "gc",
91 "resource",
92 "signal",
93 "io",
95 "builtins",
96 "pickle",
97 "shelve",
98 "dbm",
99 "sqlite3",
100 "pty",
102 "tty",
103 "termios",
104 "fcntl",
105 "mmap",
106 ];
107
108 #[must_use]
110 pub fn new() -> Self {
111 Self {
112 interpreter: "python3".to_string(),
113 max_output_bytes: 64 * 1024, blocked_modules: Self::DEFAULT_BLOCKED_MODULES
115 .iter()
116 .map(|&s| s.to_string())
117 .collect(),
118 allow_file_io: false,
119 }
120 }
121
122 #[must_use]
124 pub fn with_interpreter(mut self, interpreter: impl Into<String>) -> Self {
125 self.interpreter = interpreter.into();
126 self
127 }
128
129 #[must_use]
131 pub fn with_max_output(mut self, bytes: usize) -> Self {
132 self.max_output_bytes = bytes;
133 self
134 }
135
136 #[must_use]
138 pub fn with_blocked_modules(mut self, modules: &[&str]) -> Self {
139 for module in modules {
140 if !self.blocked_modules.contains(&(*module).to_string()) {
141 self.blocked_modules.push((*module).to_string());
142 }
143 }
144 self
145 }
146
147 #[must_use]
149 pub fn with_file_io(mut self, allow: bool) -> Self {
150 self.allow_file_io = allow;
151 self
152 }
153
154 #[must_use]
156 pub fn is_available(&self) -> bool {
157 Command::new(&self.interpreter)
158 .arg("--version")
159 .stdout(Stdio::null())
160 .stderr(Stdio::null())
161 .status()
162 .is_ok()
163 }
164
165 #[allow(clippy::uninlined_format_args)]
167 fn sandbox_wrapper(&self) -> String {
168 let blocked_list = self
169 .blocked_modules
170 .iter()
171 .map(|m| format!("'{m}'"))
172 .collect::<Vec<_>>()
173 .join(", ");
174
175 let open_restriction = if self.allow_file_io {
176 ""
177 } else {
178 "_sandbox_builtins.open = None\n"
179 };
180
181 format!(
182 r#"
183import sys as _sandbox_sys
184import builtins as _sandbox_builtins
185
186# Block dangerous modules
187_sandbox_blocked = set([{blocked_list}])
188
189# Remove already-loaded blocked modules from sys.modules
190for _sandbox_mod in list(_sandbox_sys.modules.keys()):
191 _sandbox_base = _sandbox_mod.split('.')[0]
192 if _sandbox_base in _sandbox_blocked and _sandbox_base != 'sys':
193 del _sandbox_sys.modules[_sandbox_mod]
194
195# Create a restricted __import__ function using default argument to capture values
196_sandbox_orig_import = _sandbox_builtins.__import__
197
198def _sandbox_import(name, globals=None, locals=None, fromlist=(), level=0,
199 _blocked=_sandbox_blocked, _orig=_sandbox_orig_import):
200 base = name.split('.')[0]
201 if base in _blocked:
202 raise ImportError(f"Module '{{name}}' is not allowed in sandbox")
203 return _orig(name, globals, locals, fromlist, level)
204
205# Replace builtins.__import__ (this is what import statements actually use)
206_sandbox_builtins.__import__ = _sandbox_import
207
208# Restrict dangerous builtins
209_sandbox_builtins.eval = None
210_sandbox_builtins.exec = None
211_sandbox_builtins.compile = None
212_sandbox_builtins.breakpoint = None
213_sandbox_builtins.help = None
214{open_restriction}
215# Capture user input lines
216_sandbox_user_input = []
217_sandbox_builtins.input = lambda *a, _inp=_sandbox_user_input: _inp.pop(0) if _inp else ''
218
219# Clean up sandbox setup from namespace
220del _sandbox_mod, _sandbox_base, _sandbox_sys, _sandbox_builtins, _sandbox_orig_import, _sandbox_blocked
221
222# User code follows:
223"#,
224 blocked_list = blocked_list,
225 open_restriction = open_restriction
226 )
227 }
228
229 fn wrap_code(&self, code: &str, input: &str) -> String {
231 let mut wrapper = self.sandbox_wrapper();
232
233 if !input.is_empty() {
235 let input_lines: Vec<_> = input
238 .lines()
239 .map(|l| format!("'{}'", l.replace('\'', "\\'")))
240 .collect();
241 let input_init = format!("_sandbox_user_input = [{}]\n", input_lines.join(", "));
242 wrapper = wrapper.replace("_sandbox_user_input = []\n", &input_init);
243 }
244
245 wrapper.push_str(code);
247
248 wrapper
249 }
250}
251
252impl Executor for SandboxedPythonExecutor {
253 fn execute(&self, code: &str, input: &str, timeout_ms: u64) -> Result<ExecutionResult> {
254 use std::sync::atomic::{AtomicU64, Ordering};
255 static COUNTER: AtomicU64 = AtomicU64::new(0);
256
257 let start = Instant::now();
258
259 let temp_dir = std::env::temp_dir();
261 let unique_id = COUNTER.fetch_add(1, Ordering::SeqCst);
262 let temp_file = temp_dir.join(format!(
263 "verificar_sandbox_{}_{}.py",
264 std::process::id(),
265 unique_id
266 ));
267
268 let sandboxed_code = self.wrap_code(code, input);
269
270 std::fs::write(&temp_file, &sandboxed_code)
271 .map_err(|e| Error::Verification(format!("Failed to write sandbox file: {e}")))?;
272
273 let mut cmd = Command::new(&self.interpreter);
274 cmd.arg("-I") .arg("-E") .arg("-S") .arg("-u") .arg(&temp_file)
279 .stdin(Stdio::null()) .stdout(Stdio::piped())
281 .stderr(Stdio::piped())
282 .env_clear(); let child = cmd.spawn().map_err(|e| {
286 let _ = std::fs::remove_file(&temp_file);
287 Error::Verification(format!("Failed to spawn sandboxed Python: {e}"))
288 })?;
289
290 let timeout = Duration::from_millis(timeout_ms);
292 let output = match wait_with_timeout(child, timeout) {
293 Ok(output) => output,
294 Err(e) => {
295 let _ = std::fs::remove_file(&temp_file);
296 return Err(e);
297 }
298 };
299
300 let _ = std::fs::remove_file(&temp_file);
302
303 let duration_ms = start.elapsed().as_millis() as u64;
304
305 let stdout = truncate_output(&output.stdout, self.max_output_bytes);
307 let stderr = truncate_output(&output.stderr, self.max_output_bytes);
308
309 Ok(ExecutionResult {
310 stdout,
311 stderr,
312 exit_code: output.status.code().unwrap_or(-1),
313 duration_ms,
314 })
315 }
316
317 fn language(&self) -> Language {
318 Language::Python
319 }
320}
321
322fn wait_with_timeout(
324 mut child: std::process::Child,
325 timeout: Duration,
326) -> Result<std::process::Output> {
327 use std::io::Read;
328 use std::sync::mpsc;
329 use std::thread;
330
331 let stdout_handle = child.stdout.take();
332 let stderr_handle = child.stderr.take();
333
334 let stdout_thread = thread::spawn(move || {
335 let mut buf = Vec::new();
336 if let Some(mut stdout) = stdout_handle {
337 let _ = stdout.read_to_end(&mut buf);
338 }
339 buf
340 });
341
342 let stderr_thread = thread::spawn(move || {
343 let mut buf = Vec::new();
344 if let Some(mut stderr) = stderr_handle {
345 let _ = stderr.read_to_end(&mut buf);
346 }
347 buf
348 });
349
350 let (tx, rx) = mpsc::channel();
351 let wait_thread = thread::spawn(move || {
352 let result = child.wait();
353 let _ = tx.send(result);
354 child
355 });
356
357 match rx.recv_timeout(timeout) {
358 Ok(Ok(status)) => {
359 let _ = wait_thread.join();
360 let stdout = stdout_thread.join().unwrap_or_default();
361 let stderr = stderr_thread.join().unwrap_or_default();
362 Ok(std::process::Output {
363 status,
364 stdout,
365 stderr,
366 })
367 }
368 Ok(Err(e)) => {
369 let _ = wait_thread.join();
370 let _ = stdout_thread.join();
371 let _ = stderr_thread.join();
372 Err(Error::Verification(format!("Wait error: {e}")))
373 }
374 Err(mpsc::RecvTimeoutError::Timeout) => {
375 if let Ok(mut child) = wait_thread.join() {
376 let _ = child.kill();
377 let _ = child.wait();
378 }
379 let _ = stdout_thread.join();
380 let _ = stderr_thread.join();
381 Err(Error::Verification(
382 "Sandbox execution timed out".to_string(),
383 ))
384 }
385 Err(mpsc::RecvTimeoutError::Disconnected) => {
386 let _ = wait_thread.join();
387 let _ = stdout_thread.join();
388 let _ = stderr_thread.join();
389 Err(Error::Verification(
390 "Sandbox thread disconnected".to_string(),
391 ))
392 }
393 }
394}
395
396fn truncate_output(data: &[u8], max_bytes: usize) -> String {
398 let s = String::from_utf8_lossy(data);
399 if s.len() <= max_bytes {
400 s.to_string()
401 } else {
402 let truncated: String = s.chars().take(max_bytes).collect();
403 format!("{truncated}\n... [output truncated at {max_bytes} bytes]")
404 }
405}
406
407#[derive(Debug, Clone)]
409pub struct SandboxConfig {
410 pub timeout_ms: u64,
412 pub max_output_bytes: usize,
414 pub max_memory_bytes: usize,
416 pub allow_network: bool,
418 pub allow_filesystem: bool,
420 pub blocked_modules: Vec<String>,
422}
423
424impl Default for SandboxConfig {
425 fn default() -> Self {
426 Self {
427 timeout_ms: 5000,
428 max_output_bytes: 64 * 1024,
429 max_memory_bytes: 128 * 1024 * 1024, allow_network: false,
431 allow_filesystem: false,
432 blocked_modules: Vec::new(),
433 }
434 }
435}
436
437impl SandboxConfig {
438 #[must_use]
440 pub fn strict() -> Self {
441 Self {
442 timeout_ms: 1000,
443 max_output_bytes: 16 * 1024,
444 max_memory_bytes: 64 * 1024 * 1024,
445 allow_network: false,
446 allow_filesystem: false,
447 blocked_modules: Vec::new(),
448 }
449 }
450
451 #[must_use]
453 pub fn lenient() -> Self {
454 Self {
455 timeout_ms: 30000,
456 max_output_bytes: 1024 * 1024,
457 max_memory_bytes: 512 * 1024 * 1024,
458 allow_network: false,
459 allow_filesystem: false,
460 blocked_modules: Vec::new(),
461 }
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468
469 fn skip_if_no_python(executor: &SandboxedPythonExecutor) -> bool {
470 if !executor.is_available() {
471 eprintln!("Python not available, skipping test");
472 true
473 } else {
474 false
475 }
476 }
477
478 #[test]
479 fn test_sandbox_basic_execution() {
480 let executor = SandboxedPythonExecutor::new();
481 if skip_if_no_python(&executor) {
482 return;
483 }
484
485 let result = executor
486 .execute("print(1 + 1)", "", 5000)
487 .expect("execution should succeed");
488
489 assert_eq!(result.stdout.trim(), "2");
490 assert_eq!(result.exit_code, 0);
491 }
492
493 #[test]
494 fn test_sandbox_blocks_os_import() {
495 let executor = SandboxedPythonExecutor::new();
496 if skip_if_no_python(&executor) {
497 return;
498 }
499
500 let result = executor
501 .execute("import os\nprint(os.getcwd())", "", 5000)
502 .expect("execution should succeed");
503
504 assert_ne!(result.exit_code, 0);
505 assert!(result.stderr.contains("not allowed") || result.stderr.contains("ImportError"));
506 }
507
508 #[test]
509 fn test_sandbox_blocks_subprocess() {
510 let executor = SandboxedPythonExecutor::new();
511 if skip_if_no_python(&executor) {
512 return;
513 }
514
515 let result = executor
516 .execute("import subprocess\nsubprocess.run(['ls'])", "", 5000)
517 .expect("execution should succeed");
518
519 assert_ne!(result.exit_code, 0);
520 assert!(result.stderr.contains("not allowed") || result.stderr.contains("ImportError"));
521 }
522
523 #[test]
524 fn test_sandbox_blocks_socket() {
525 let executor = SandboxedPythonExecutor::new();
526 if skip_if_no_python(&executor) {
527 return;
528 }
529
530 let result = executor
531 .execute("import socket", "", 5000)
532 .expect("execution should succeed");
533
534 assert_ne!(result.exit_code, 0);
535 }
536
537 #[test]
538 fn test_sandbox_blocks_eval() {
539 let executor = SandboxedPythonExecutor::new();
540 if skip_if_no_python(&executor) {
541 return;
542 }
543
544 let result = executor
545 .execute("eval('1+1')", "", 5000)
546 .expect("execution should succeed");
547
548 assert_ne!(result.exit_code, 0);
549 }
550
551 #[test]
552 fn test_sandbox_blocks_exec() {
553 let executor = SandboxedPythonExecutor::new();
554 if skip_if_no_python(&executor) {
555 return;
556 }
557
558 let result = executor
559 .execute("exec('print(1)')", "", 5000)
560 .expect("execution should succeed");
561
562 assert_ne!(result.exit_code, 0);
563 }
564
565 #[test]
566 fn test_sandbox_allows_safe_math() {
567 let executor = SandboxedPythonExecutor::new();
568 if skip_if_no_python(&executor) {
569 return;
570 }
571
572 let result = executor
573 .execute("import math\nprint(math.sqrt(16))", "", 5000)
574 .expect("execution should succeed");
575
576 assert_eq!(result.stdout.trim(), "4.0");
577 assert_eq!(result.exit_code, 0);
578 }
579
580 #[test]
581 fn test_sandbox_allows_safe_builtins() {
582 let executor = SandboxedPythonExecutor::new();
583 if skip_if_no_python(&executor) {
584 return;
585 }
586
587 let code = r#"
588x = [1, 2, 3, 4, 5]
589print(sum(x))
590print(len(x))
591print(max(x))
592print(min(x))
593"#;
594 let result = executor
595 .execute(code, "", 5000)
596 .expect("execution should succeed");
597
598 let lines: Vec<_> = result.stdout.trim().lines().collect();
599 assert_eq!(lines, vec!["15", "5", "5", "1"]);
600 }
601
602 #[test]
603 #[ignore] fn test_sandbox_timeout() {
605 let executor = SandboxedPythonExecutor::new();
606 if skip_if_no_python(&executor) {
607 return;
608 }
609
610 let result = executor.execute("while True: pass", "", 500);
611
612 assert!(result.is_err());
613 let err = result.unwrap_err().to_string();
614 assert!(err.contains("timed out") || err.contains("timeout"));
615 }
616
617 #[test]
618 fn test_sandbox_output_truncation() {
619 let executor = SandboxedPythonExecutor::new().with_max_output(100);
620 if skip_if_no_python(&executor) {
621 return;
622 }
623
624 let result = executor
625 .execute("print('x' * 1000)", "", 5000)
626 .expect("execution should succeed");
627
628 assert!(result.stdout.len() <= 150); assert!(result.stdout.contains("truncated"));
630 }
631
632 #[test]
633 fn test_sandbox_input_handling() {
634 let executor = SandboxedPythonExecutor::new();
635 if skip_if_no_python(&executor) {
636 return;
637 }
638
639 let code = r#"
640name = input()
641print(f"Hello, {name}!")
642"#;
643 let result = executor
644 .execute(code, "World", 5000)
645 .expect("execution should succeed");
646
647 assert_eq!(result.stdout.trim(), "Hello, World!");
648 }
649
650 #[test]
651 fn test_sandbox_multiple_inputs() {
652 let executor = SandboxedPythonExecutor::new();
653 if skip_if_no_python(&executor) {
654 return;
655 }
656
657 let code = r#"
658a = int(input())
659b = int(input())
660print(a + b)
661"#;
662 let result = executor
663 .execute(code, "3\n4", 5000)
664 .expect("execution should succeed");
665
666 assert_eq!(result.stdout.trim(), "7");
667 }
668
669 #[test]
670 fn test_sandbox_config_strict() {
671 let config = SandboxConfig::strict();
672 assert_eq!(config.timeout_ms, 1000);
673 assert!(!config.allow_network);
674 assert!(!config.allow_filesystem);
675 }
676
677 #[test]
678 fn test_sandbox_config_lenient() {
679 let config = SandboxConfig::lenient();
680 assert_eq!(config.timeout_ms, 30000);
681 assert!(!config.allow_network);
682 }
683
684 #[test]
685 fn test_sandbox_config_default() {
686 let config = SandboxConfig::default();
687 assert_eq!(config.timeout_ms, 5000);
688 assert_eq!(config.max_output_bytes, 64 * 1024);
689 assert_eq!(config.max_memory_bytes, 128 * 1024 * 1024);
690 assert!(!config.allow_network);
691 assert!(!config.allow_filesystem);
692 assert!(config.blocked_modules.is_empty());
693 }
694
695 #[test]
696 fn test_sandbox_config_debug() {
697 let config = SandboxConfig::default();
698 let debug = format!("{:?}", config);
699 assert!(debug.contains("SandboxConfig"));
700 }
701
702 #[test]
703 fn test_sandbox_config_clone() {
704 let config = SandboxConfig::strict();
705 let cloned = config.clone();
706 assert_eq!(cloned.timeout_ms, config.timeout_ms);
707 }
708
709 #[test]
710 fn test_sandboxed_executor_with_interpreter() {
711 let executor = SandboxedPythonExecutor::new().with_interpreter("python3.11");
712 assert_eq!(executor.interpreter, "python3.11");
713 }
714
715 #[test]
716 fn test_sandboxed_executor_with_max_output() {
717 let executor = SandboxedPythonExecutor::new().with_max_output(1024);
718 assert_eq!(executor.max_output_bytes, 1024);
719 }
720
721 #[test]
722 fn test_sandboxed_executor_with_blocked_modules() {
723 let executor = SandboxedPythonExecutor::new()
724 .with_blocked_modules(&["custom_module", "another_module"]);
725 assert!(executor
726 .blocked_modules
727 .contains(&"custom_module".to_string()));
728 assert!(executor
729 .blocked_modules
730 .contains(&"another_module".to_string()));
731 }
732
733 #[test]
734 fn test_sandboxed_executor_with_file_io() {
735 let executor = SandboxedPythonExecutor::new().with_file_io(true);
736 assert!(executor.allow_file_io);
737 }
738
739 #[test]
740 fn test_sandboxed_executor_default() {
741 let executor = SandboxedPythonExecutor::default();
742 assert_eq!(executor.interpreter, "python3");
743 assert_eq!(executor.max_output_bytes, 64 * 1024);
744 assert!(!executor.allow_file_io);
745 }
746
747 #[test]
748 fn test_sandboxed_executor_language() {
749 let executor = SandboxedPythonExecutor::new();
750 assert_eq!(executor.language(), Language::Python);
751 }
752
753 #[test]
754 fn test_sandboxed_executor_debug() {
755 let executor = SandboxedPythonExecutor::new();
756 let debug = format!("{:?}", executor);
757 assert!(debug.contains("SandboxedPythonExecutor"));
758 }
759
760 #[test]
761 fn test_truncate_output_short() {
762 let data = b"hello world";
763 let result = truncate_output(data, 100);
764 assert_eq!(result, "hello world");
765 }
766
767 #[test]
768 fn test_truncate_output_long() {
769 let data = b"hello world this is a longer string";
770 let result = truncate_output(data, 10);
771 assert!(result.len() <= 50); assert!(result.contains("truncated"));
773 }
774
775 #[test]
776 fn test_sandbox_with_file_io_enabled() {
777 let executor = SandboxedPythonExecutor::new().with_file_io(true);
778 if skip_if_no_python(&executor) {
779 return;
780 }
781
782 let code = "print('file io test')";
785 let result = executor
786 .execute(code, "", 5000)
787 .expect("execution should succeed");
788
789 assert_eq!(result.stdout.trim(), "file io test");
790 }
791
792 #[test]
793 fn test_sandbox_blocks_open_by_default() {
794 let executor = SandboxedPythonExecutor::new();
795 if skip_if_no_python(&executor) {
796 return;
797 }
798
799 let code = "open('/etc/passwd', 'r')";
801 let result = executor
802 .execute(code, "", 5000)
803 .expect("execution should succeed");
804
805 assert_ne!(result.exit_code, 0);
807 }
808
809 #[test]
810 fn test_sandbox_blocks_ctypes() {
811 let executor = SandboxedPythonExecutor::new();
812 if skip_if_no_python(&executor) {
813 return;
814 }
815
816 let result = executor
817 .execute("import ctypes", "", 5000)
818 .expect("execution should succeed");
819
820 assert_ne!(result.exit_code, 0);
821 }
822
823 #[test]
824 fn test_sandbox_blocks_sys() {
825 let executor = SandboxedPythonExecutor::new();
826 if skip_if_no_python(&executor) {
827 return;
828 }
829
830 let result = executor
831 .execute("import sys\nprint(sys.executable)", "", 5000)
832 .expect("execution should succeed");
833
834 assert_ne!(result.exit_code, 0);
836 }
837
838 #[test]
839 fn test_sandbox_empty_input() {
840 let executor = SandboxedPythonExecutor::new();
841 if skip_if_no_python(&executor) {
842 return;
843 }
844
845 let code = "x = input()\nprint(f'got: [{x}]')";
847 let result = executor
848 .execute(code, "", 5000)
849 .expect("execution should succeed");
850
851 assert_eq!(result.stdout.trim(), "got: []");
852 }
853
854 #[test]
855 fn test_sandbox_input_with_quotes() {
856 let executor = SandboxedPythonExecutor::new();
857 if skip_if_no_python(&executor) {
858 return;
859 }
860
861 let code = "x = input()\nprint(f'got: {x}')";
863 let result = executor
864 .execute(code, "hello'world", 5000)
865 .expect("execution should succeed");
866
867 assert!(result.stdout.contains("hello'world") || result.exit_code == 0);
868 }
869
870 #[test]
871 fn test_sandbox_is_available() {
872 let executor = SandboxedPythonExecutor::new();
873 let _ = executor.is_available();
875 }
876
877 #[test]
878 fn test_sandbox_with_custom_blocked_module_execution() {
879 let executor = SandboxedPythonExecutor::new().with_blocked_modules(&["json"]);
880 if skip_if_no_python(&executor) {
881 return;
882 }
883
884 let result = executor
886 .execute("import json\nprint(json.dumps({}))", "", 5000)
887 .expect("execution should succeed");
888
889 assert_ne!(result.exit_code, 0);
890 }
891}