Skip to main content

hara_native/runtime/
sandbox.rs

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    /// Creates a provider result tied to the evaluation allocated by the
15    /// Kernel. External providers send exactly one terminal result through the
16    /// receiver; dropping the sender is reported as a transport failure.
17    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
41/// Provider-side live sandbox. Implementations own backend launch, execution,
42/// cancellation, and termination details.
43pub 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/// Conformance-only provider. Runtime separation is logical and is not a
89/// security boundary.
90#[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}