1use sha2::{Digest, Sha256};
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::sync::{mpsc, Arc, Mutex};
4use std::thread::JoinHandle;
5use std::time::{Duration, Instant};
6
7#[derive(Debug)]
8pub struct SandboxPending<T> {
9 evaluation: EvaluationId,
10 receiver: mpsc::Receiver<Result<T, SandboxError>>,
11}
12
13impl<T> SandboxPending<T> {
14 pub const fn new(
18 evaluation: EvaluationId,
19 receiver: mpsc::Receiver<Result<T, SandboxError>>,
20 ) -> Self {
21 Self {
22 evaluation,
23 receiver,
24 }
25 }
26
27 pub const fn evaluation(&self) -> EvaluationId {
28 self.evaluation
29 }
30
31 pub fn wait(self) -> Result<T, SandboxError> {
32 self.receiver.recv().unwrap_or_else(|_| {
33 Err(SandboxError::new(
34 SandboxErrorCode::TransportFailed,
35 "sandbox provider dropped the evaluation result",
36 ))
37 })
38 }
39}
40
41pub trait SandboxInstance {
44 fn eval(
45 &mut self,
46 evaluation: EvaluationId,
47 source: String,
48 ) -> Result<SandboxPending<String>, SandboxError>;
49 fn call(
50 &mut self,
51 evaluation: EvaluationId,
52 callable: String,
53 arguments_hta: Vec<u8>,
54 ) -> Result<SandboxPending<Vec<u8>>, SandboxError>;
55 fn cancel(&mut self, evaluation: EvaluationId) -> Result<bool, SandboxError>;
56 fn active_evaluation(&self) -> Option<EvaluationId>;
57 fn state(&self) -> SandboxState;
58 fn error(&self) -> Option<SandboxError>;
59 fn close(&mut self) -> Result<(), SandboxError>;
60}
61
62pub trait SandboxProvider {
63 fn name(&self) -> &str;
64 fn secure(&self) -> bool;
65 fn open(&self, spec: &ResolvedSandboxSpec) -> Result<Box<dyn SandboxInstance>, SandboxError>;
66}
67
68#[derive(Clone, Debug)]
69pub struct ResolvedSandboxBundle {
70 pub reference: SandboxBundleReference,
71 pub bytes: Vec<u8>,
72}
73
74#[derive(Clone, Debug)]
75pub struct ResolvedSandboxMount {
76 pub id: SessionMountId,
77 pub kind: String,
78 pub key: String,
79}
80
81#[derive(Clone, Debug)]
82pub struct ResolvedSandboxSpec {
83 pub spec: SandboxSpec,
84 pub bundles: Vec<ResolvedSandboxBundle>,
85 pub mount: Option<ResolvedSandboxMount>,
86}
87
88#[derive(Default)]
91pub struct InProcessSandboxProvider;
92
93impl SandboxProvider for InProcessSandboxProvider {
94 fn name(&self) -> &str {
95 "in-process"
96 }
97
98 fn secure(&self) -> bool {
99 false
100 }
101
102 fn open(
103 &self,
104 resolved: &ResolvedSandboxSpec,
105 ) -> Result<Box<dyn SandboxInstance>, SandboxError> {
106 resolved.spec.validate()?;
107 InProcessSandbox::open(resolved.clone()).map(|instance| Box::new(instance) as _)
108 }
109}
110
111#[derive(Clone)]
112struct ActiveEvaluation {
113 id: EvaluationId,
114 cancelled: Arc<AtomicBool>,
115}
116
117struct WorkerState {
118 state: SandboxState,
119 active: Option<ActiveEvaluation>,
120 error: Option<SandboxError>,
121}
122
123enum SandboxCommand {
124 Eval {
125 evaluation: EvaluationId,
126 source: String,
127 cancelled: Arc<AtomicBool>,
128 reply: mpsc::Sender<Result<String, SandboxError>>,
129 },
130 Call {
131 evaluation: EvaluationId,
132 callable: String,
133 arguments_hta: Vec<u8>,
134 cancelled: Arc<AtomicBool>,
135 reply: mpsc::Sender<Result<Vec<u8>, SandboxError>>,
136 },
137 Close,
138}
139
140struct InProcessSandbox {
141 commands: mpsc::Sender<SandboxCommand>,
142 worker: Option<JoinHandle<()>>,
143 shared: Arc<Mutex<WorkerState>>,
144 limits: SandboxLimits,
145}
146
147impl InProcessSandbox {
148 fn open(resolved: ResolvedSandboxSpec) -> Result<Self, SandboxError> {
149 let spec = resolved.spec;
150 let limits = spec.limits.clone();
151 let (commands, receiver) = mpsc::channel();
152 let shared = Arc::new(Mutex::new(WorkerState {
153 state: SandboxState::Open,
154 active: None,
155 error: None,
156 }));
157 let worker_shared = Arc::clone(&shared);
158 let worker = std::thread::Builder::new()
159 .name("hara-in-process-sandbox".into())
160 .stack_size(64 * 1024 * 1024)
161 .spawn(move || sandbox_worker(spec, receiver, worker_shared))
162 .map_err(|error| {
163 SandboxError::new(SandboxErrorCode::ProviderFailed, error.to_string())
164 })?;
165 Ok(Self {
166 commands,
167 worker: Some(worker),
168 shared,
169 limits,
170 })
171 }
172
173 fn begin(&self, evaluation: EvaluationId) -> Result<Arc<AtomicBool>, SandboxError> {
174 let mut shared = self.shared.lock().expect("sandbox state poisoned");
175 if shared.state != SandboxState::Open || shared.active.is_some() {
176 return Err(if shared.state == SandboxState::Running {
177 SandboxError::new(SandboxErrorCode::Busy, "sandbox is busy")
178 } else {
179 SandboxError::new(
180 SandboxErrorCode::Closed,
181 "sandbox is terminal and cannot be reused",
182 )
183 });
184 }
185 let cancelled = Arc::new(AtomicBool::new(false));
186 shared.state = SandboxState::Running;
187 shared.error = None;
188 shared.active = Some(ActiveEvaluation {
189 id: evaluation,
190 cancelled: Arc::clone(&cancelled),
191 });
192 Ok(cancelled)
193 }
194
195 fn send(&self, command: SandboxCommand) -> Result<(), SandboxError> {
196 self.commands.send(command).map_err(|_| {
197 SandboxError::new(
198 SandboxErrorCode::TransportFailed,
199 "sandbox provider command channel is closed",
200 )
201 })
202 }
203}
204
205fn sandbox_worker(
206 spec: SandboxSpec,
207 commands: mpsc::Receiver<SandboxCommand>,
208 shared: Arc<Mutex<WorkerState>>,
209) {
210 let session_spec = match SessionId::parse("SANDBOX") {
211 Ok(id) => SessionSpec::new(id, SessionAuthorityPolicy::ZERO),
212 Err(error) => {
213 finish_provider_failure(&shared, error);
214 return;
215 }
216 };
217 let mut runtime = Runtime::sandbox();
218 runtime.use_namespace(&spec.entry_namespace);
219 let mut session = Session::open(session_spec, runtime);
220 while let Ok(command) = commands.recv() {
221 match command {
222 SandboxCommand::Eval {
223 evaluation,
224 source,
225 cancelled,
226 reply,
227 } => {
228 let result = run_controlled(evaluation, &spec.limits, &cancelled, || {
229 session.eval(&source)
230 })
231 .and_then(|result| {
232 if result.len() > spec.limits.result_bytes {
233 Err(SandboxError::new(
234 SandboxErrorCode::LimitExceeded,
235 "sandbox result limit exceeded",
236 ))
237 } else {
238 Ok(result)
239 }
240 });
241 finish_evaluation(&shared, evaluation, &result);
242 let _ = reply.send(result);
243 }
244 SandboxCommand::Call {
245 evaluation,
246 callable,
247 arguments_hta,
248 cancelled,
249 reply,
250 } => {
251 let result = run_controlled(evaluation, &spec.limits, &cancelled, || {
252 #[cfg(not(target_arch = "wasm32"))]
253 {
254 session
255 .runtime_mut()?
256 .invoke_hta(&callable, &arguments_hta)
257 .map_err(|error| error.to_string())
258 }
259 #[cfg(target_arch = "wasm32")]
260 {
261 let _ = (&callable, &arguments_hta);
262 Err::<Vec<u8>, String>(
263 "sandbox HTA calls are unavailable in browser WASM".into(),
264 )
265 }
266 })
267 .and_then(|result| {
268 if result.len() > spec.limits.result_bytes {
269 Err(SandboxError::new(
270 SandboxErrorCode::LimitExceeded,
271 "sandbox result limit exceeded",
272 ))
273 } else {
274 Ok(result)
275 }
276 });
277 finish_evaluation(&shared, evaluation, &result);
278 let _ = reply.send(result);
279 }
280 SandboxCommand::Close => break,
281 }
282 }
283 session.release();
284}
285
286fn run_controlled<T>(
287 _evaluation: EvaluationId,
288 limits: &SandboxLimits,
289 cancelled: &Arc<AtomicBool>,
290 operation: impl FnOnce() -> Result<T, String>,
291) -> Result<T, SandboxError> {
292 let started = Instant::now();
293 let deadline = Duration::from_millis(limits.evaluation_ms);
294 let cancellation = Arc::clone(cancelled);
295 let result = core::with_evaluation_interrupt(
296 Rc::new(move || {
297 if cancellation.load(Ordering::Acquire) {
298 Some("SANDBOX_CANCELLED".into())
299 } else if started.elapsed() >= deadline {
300 Some("SANDBOX_TIMEOUT".into())
301 } else {
302 None
303 }
304 }),
305 operation,
306 );
307 result.map_err(|error| {
308 if error.contains("SANDBOX_CANCELLED") {
309 SandboxError::new(SandboxErrorCode::Cancelled, "sandbox evaluation cancelled")
310 } else if error.contains("SANDBOX_TIMEOUT") {
311 SandboxError::new(SandboxErrorCode::Timeout, "sandbox evaluation timed out")
312 } else if error.contains("SESSION_TRANSFER_REJECTED")
313 || error.contains("invoke-hta/result-unsupported")
314 {
315 SandboxError::new(
316 SandboxErrorCode::ResultNotTransferable,
317 "sandbox result is not transferable",
318 )
319 } else {
320 SandboxError::new(SandboxErrorCode::EvaluationFailed, error)
321 }
322 })
323}
324
325fn finish_evaluation<T>(
326 shared: &Arc<Mutex<WorkerState>>,
327 evaluation: EvaluationId,
328 result: &Result<T, SandboxError>,
329) {
330 let mut shared = shared.lock().expect("sandbox state poisoned");
331 if !shared
332 .active
333 .as_ref()
334 .is_some_and(|active| active.id == evaluation)
335 {
336 return;
337 }
338 shared.active = None;
339 match result {
340 Ok(_) => shared.state = SandboxState::Open,
341 Err(error) => {
342 shared.state = match error.code {
343 SandboxErrorCode::Cancelled => SandboxState::Cancelled,
344 _ => SandboxState::Failed,
345 };
346 shared.error = Some(error.clone());
347 }
348 }
349}
350
351fn finish_provider_failure(shared: &Arc<Mutex<WorkerState>>, message: String) {
352 let error = SandboxError::new(SandboxErrorCode::ProviderFailed, message);
353 let mut shared = shared.lock().expect("sandbox state poisoned");
354 shared.state = SandboxState::Failed;
355 shared.error = Some(error);
356 shared.active = None;
357}
358
359impl SandboxInstance for InProcessSandbox {
360 fn eval(
361 &mut self,
362 evaluation: EvaluationId,
363 source: String,
364 ) -> Result<SandboxPending<String>, SandboxError> {
365 if source.len() > self.limits.source_bytes {
366 return Err(SandboxError::new(
367 SandboxErrorCode::LimitExceeded,
368 "sandbox source limit exceeded",
369 ));
370 }
371 let cancelled = self.begin(evaluation)?;
372 let (reply, receiver) = mpsc::channel();
373 self.send(SandboxCommand::Eval {
374 evaluation,
375 source,
376 cancelled,
377 reply,
378 })?;
379 Ok(SandboxPending {
380 evaluation,
381 receiver,
382 })
383 }
384
385 fn call(
386 &mut self,
387 evaluation: EvaluationId,
388 callable: String,
389 arguments_hta: Vec<u8>,
390 ) -> Result<SandboxPending<Vec<u8>>, SandboxError> {
391 let cancelled = self.begin(evaluation)?;
392 let (reply, receiver) = mpsc::channel();
393 self.send(SandboxCommand::Call {
394 evaluation,
395 callable,
396 arguments_hta,
397 cancelled,
398 reply,
399 })?;
400 Ok(SandboxPending {
401 evaluation,
402 receiver,
403 })
404 }
405
406 fn cancel(&mut self, evaluation: EvaluationId) -> Result<bool, SandboxError> {
407 let mut shared = self.shared.lock().expect("sandbox state poisoned");
408 let Some(active) = shared.active.as_ref() else {
409 return Ok(false);
410 };
411 if active.id != evaluation {
412 return Ok(false);
413 }
414 active.cancelled.store(true, Ordering::Release);
415 shared.state = SandboxState::Cancelling;
416 Ok(true)
417 }
418
419 fn active_evaluation(&self) -> Option<EvaluationId> {
420 self.shared
421 .lock()
422 .expect("sandbox state poisoned")
423 .active
424 .as_ref()
425 .map(|active| active.id)
426 }
427
428 fn state(&self) -> SandboxState {
429 self.shared.lock().expect("sandbox state poisoned").state
430 }
431
432 fn error(&self) -> Option<SandboxError> {
433 self.shared
434 .lock()
435 .expect("sandbox state poisoned")
436 .error
437 .clone()
438 }
439
440 fn close(&mut self) -> Result<(), SandboxError> {
441 if let Some(active) = self.active_evaluation() {
442 let _ = self.cancel(active)?;
443 }
444 let _ = self.commands.send(SandboxCommand::Close);
445 if let Some(worker) = self.worker.take() {
446 worker.join().map_err(|_| {
447 SandboxError::new(
448 SandboxErrorCode::ProviderFailed,
449 "sandbox provider worker panicked",
450 )
451 })?;
452 }
453 Ok(())
454 }
455}
456
457struct Sandbox {
458 id: SandboxId,
459 provider: String,
460 secure: bool,
461 mount: Option<SessionMountId>,
462 next_evaluation_id: u64,
463 instance: Box<dyn SandboxInstance>,
464}
465
466impl Sandbox {
467 fn allocate_evaluation(&mut self) -> EvaluationId {
468 let id = EvaluationId::new(self.next_evaluation_id);
469 self.next_evaluation_id = self
470 .next_evaluation_id
471 .checked_add(1)
472 .expect("sandbox evaluation identifiers exhausted");
473 id
474 }
475}
476
477impl SessionKernel {
478 pub fn register_sandbox_provider(&mut self, provider: Rc<dyn SandboxProvider>) {
479 self.sandbox_provider_registry
480 .entries
481 .insert(provider.name().into(), provider);
482 }
483
484 pub fn open_sandbox(&mut self, spec: SandboxSpec) -> Result<SandboxId, SandboxError> {
485 spec.validate()?;
486 let provider = self
487 .sandbox_provider_registry
488 .entries
489 .get(&spec.provider)
490 .ok_or_else(|| {
491 SandboxError::new(SandboxErrorCode::ProviderNotFound, spec.provider.clone())
492 })?
493 .clone();
494 let bundles = spec
495 .bundles()
496 .iter()
497 .map(|reference| {
498 self.bundle_catalog
499 .entries
500 .get(&reference.digest)
501 .cloned()
502 .map(|bytes| (reference, bytes))
503 .ok_or_else(|| {
504 SandboxError::new(
505 SandboxErrorCode::BundleNotFound,
506 reference.digest.clone(),
507 )
508 })
509 .and_then(|(reference, bytes)| {
510 let actual = format!("sha256:{:x}", Sha256::digest(&bytes));
511 if actual == reference.digest {
512 Ok(ResolvedSandboxBundle {
513 reference: reference.clone(),
514 bytes,
515 })
516 } else {
517 Err(SandboxError::new(
518 SandboxErrorCode::BundleDigestMismatch,
519 reference.digest.clone(),
520 ))
521 }
522 })
523 })
524 .collect::<Result<Vec<_>, _>>()?;
525 let mount = spec
526 .mount()
527 .map(|id| {
528 self.mount_registry
529 .entries
530 .get(&id.get())
531 .map(|mount| ResolvedSandboxMount {
532 id,
533 kind: mount.kind.into(),
534 key: mount.key.clone(),
535 })
536 .ok_or_else(|| {
537 SandboxError::new(SandboxErrorCode::MountNotFound, id.to_string())
538 })
539 })
540 .transpose()?;
541 let secure = provider.secure();
542 let id = SandboxId(self.sandbox_registry.next_id);
543 self.sandbox_registry.next_id = self
544 .sandbox_registry
545 .next_id
546 .checked_add(1)
547 .expect("sandbox identifiers exhausted");
548 if let Some(mount) = &mount {
549 self.mount_registry
550 .entries
551 .get_mut(&mount.id.get())
552 .expect("resolved mount remains registered")
553 .attachments += 1;
554 self.mount_registry
555 .sandbox_attachments
556 .insert(id.get(), mount.id.get());
557 }
558 let resolved = ResolvedSandboxSpec {
559 spec: spec.clone(),
560 bundles,
561 mount,
562 };
563 let instance = match provider.open(&resolved) {
564 Ok(instance) => instance,
565 Err(error) => {
566 self.release_sandbox_mount(id, spec.mount());
567 return Err(error);
568 }
569 };
570 self.sandbox_registry.entries.insert(
571 id.get(),
572 Sandbox {
573 id,
574 provider: spec.provider.clone(),
575 secure,
576 mount: spec.mount(),
577 next_evaluation_id: 1,
578 instance,
579 },
580 );
581 Ok(id)
582 }
583
584 fn release_sandbox_mount(&mut self, id: SandboxId, mount: Option<SessionMountId>) {
585 let Some(mount) = mount else {
586 return;
587 };
588 if self.mount_registry.sandbox_attachments.remove(&id.get()) == Some(mount.get()) {
589 if let Some(entry) = self.mount_registry.entries.get_mut(&mount.get()) {
590 entry.attachments = entry.attachments.saturating_sub(1);
591 }
592 }
593 }
594
595 fn sandbox_mut(&mut self, id: SandboxId) -> Result<&mut Sandbox, SandboxError> {
596 self.sandbox_registry
597 .entries
598 .get_mut(&id.get())
599 .ok_or_else(|| SandboxError::new(SandboxErrorCode::NotFound, id.to_string()))
600 }
601
602 pub fn sandbox_eval(
603 &mut self,
604 id: SandboxId,
605 source: &str,
606 ) -> Result<SandboxPending<String>, SandboxError> {
607 let sandbox = self.sandbox_mut(id)?;
608 let evaluation = sandbox.allocate_evaluation();
609 sandbox.instance.eval(evaluation, source.to_owned())
610 }
611
612 pub fn sandbox_call(
613 &mut self,
614 id: SandboxId,
615 callable: &str,
616 arguments_hta: &[u8],
617 ) -> Result<SandboxPending<Vec<u8>>, SandboxError> {
618 let sandbox = self.sandbox_mut(id)?;
619 let evaluation = sandbox.allocate_evaluation();
620 sandbox
621 .instance
622 .call(evaluation, callable.to_owned(), arguments_hta.to_vec())
623 }
624
625 pub fn cancel_sandbox(&mut self, id: SandboxId) -> Result<bool, SandboxError> {
626 let sandbox = self.sandbox_mut(id)?;
627 let Some(evaluation) = sandbox.instance.active_evaluation() else {
628 return Ok(false);
629 };
630 sandbox.instance.cancel(evaluation)
631 }
632
633 pub fn cancel_sandbox_evaluation(
634 &mut self,
635 id: SandboxId,
636 evaluation: EvaluationId,
637 ) -> Result<bool, SandboxError> {
638 self.sandbox_mut(id)?.instance.cancel(evaluation)
639 }
640
641 pub fn sandbox_status(&self, id: SandboxId) -> Result<SandboxStatus, SandboxError> {
642 let sandbox = self
643 .sandbox_registry
644 .entries
645 .get(&id.get())
646 .ok_or_else(|| SandboxError::new(SandboxErrorCode::NotFound, id.to_string()))?;
647 Ok(SandboxStatus {
648 id: sandbox.id,
649 provider: sandbox.provider.clone(),
650 state: sandbox.instance.state(),
651 secure: sandbox.secure,
652 evaluation_active: sandbox.instance.active_evaluation().is_some(),
653 error: sandbox.instance.error(),
654 })
655 }
656
657 pub fn close_sandbox(&mut self, id: SandboxId) -> Result<(), SandboxError> {
658 let mut sandbox = self
659 .sandbox_registry
660 .entries
661 .remove(&id.get())
662 .ok_or_else(|| SandboxError::new(SandboxErrorCode::NotFound, id.to_string()))?;
663 let result = sandbox.instance.close();
664 self.release_sandbox_mount(id, sandbox.mount);
665 result
666 }
667}