Skip to main content

nub_host_kvm/sandbox/
initialized_multi_use.rs

1/*
2Copyright 2025  The Hyperlight Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17use std::sync::atomic::{Ordering, fence};
18use std::sync::{Arc, Condvar, Mutex};
19use std::thread::{self, JoinHandle};
20use std::time::{Duration, Instant};
21
22use nub_arch_x86_abi::{
23    CapHash as AbiCapHash, FN_ID_NUB_INVOKE_WORKER, FN_ID_NUB_PUT_CAP, InvocationResult,
24    InvokePacket, PARALLEL_INVOKE_STATUS_DONE, PARALLEL_INVOKE_STATUS_EMPTY,
25    PARALLEL_INVOKE_STATUS_EVICT_JIT_READY, PARALLEL_INVOKE_STATUS_READY,
26    PARALLEL_INVOKE_STATUS_RUNNING, PARALLEL_INVOKE_STATUS_STARTING, PARALLEL_INVOKE_STATUS_STOP,
27};
28use nub_host_common::rpc::{ArchivedResponse, Request};
29use rkyv::util::AlignedVec;
30use std::collections::HashSet;
31use tracing::{Span, instrument};
32
33use super::host_funcs::FunctionRegistry;
34use crate::HyperlightError;
35use crate::Result;
36use crate::hypervisor::InterruptHandle;
37use crate::hypervisor::hyperlight_vm::HyperlightVm;
38use crate::hypervisor::virtual_machine::VcpuLane;
39use crate::mem::mgr::SandboxMemoryManager;
40use crate::mem::shared_mem::HostSharedMemory;
41use crate::metrics::{
42    METRIC_GUEST_ERROR, METRIC_GUEST_ERROR_LABEL_CODE, maybe_time_and_emit_guest_call,
43};
44
45/// A fully initialized sandbox that can execute guest functions multiple times.
46///
47/// Guest functions can be called repeatedly while maintaining state between calls.
48///
49/// Post-Stage-F: the upstream `snapshot()` / `restore()` / `map_file_cow()`
50/// rollback machinery is gone along with the CoW PT marking that backed it.
51/// If a guest call fails for any reason, drop the sandbox and build a new
52/// one — that's the only recovery path now (and the one `nub` already used).
53pub struct MultiUseSandbox {
54    /// Unique identifier for this sandbox instance
55    id: u64,
56    pub(crate) host_funcs: Arc<Mutex<FunctionRegistry>>,
57    pub(crate) mem_mgr: Arc<Mutex<SandboxMemoryManager<HostSharedMemory>>>,
58    vm: Arc<HyperlightVm>,
59    control_lock: Mutex<()>,
60    invoke_workers: Mutex<Option<Arc<ParallelInvokeWorkers>>>,
61    /// Host-side record of every blob hash this sandbox has successfully
62    /// published, so `put_object_with_hash` can short-circuit an idempotent
63    /// re-put without a roundtrip + hash walk through the guest.
64    ///
65    /// This *replaces* an earlier design that directly dereferenced the
66    /// guest's heap-resident `CacheDirectory` hashbrown table from the host
67    /// (the deleted `GuestCacheReader`). That was unsound: the guest is built
68    /// for `x86_64-unknown-none` (no SSE2 → hashbrown's generic **8-byte**
69    /// `Group`) while the host has SSE2 (**16-byte** `Group`). The host's
70    /// probe read 8 control bytes *past* the guest's control array, so once
71    /// the table grew beyond one group an absent-key lookup could walk off the
72    /// end ("went past end of probe sequence") or, worse, silently match the
73    /// wrong entry. A hashbrown table simply cannot be shared by direct memory
74    /// access across two binaries with different SIMD `Group` widths.
75    ///
76    /// The host set is sound only under a **personality obligation**:
77    /// publication is permanent — the guest store must retain every object
78    /// it has accepted for the sandbox's lifetime, never evicting under
79    /// capacity or memory pressure (stated on
80    /// `nub::personality::LocalKernel::put_object`; a pressure-evicting
81    /// store MUST NOT be driven through this cache). javm satisfies it
82    /// structurally: caps are keyed by content hash and
83    /// `CacheDirectory::put_cap` only ever `entry().or_insert()`s — blobs
84    /// are never evicted (only the *instances* tier is swept). A miss
85    /// falls through to the idempotent `put_cap` RPC, so even blobs the
86    /// guest published on its own (e.g. via `derive_spawn`) are handled
87    /// correctly — just without the short-circuit.
88    published_blobs: Mutex<HashSet<AbiCapHash>>,
89}
90
91impl MultiUseSandbox {
92    /// Move an `UninitializedSandbox` into a new `MultiUseSandbox` instance.
93    ///
94    /// This function is not equivalent to doing an `evolve` from uninitialized
95    /// to initialized, and is purposely not exposed publicly outside the crate
96    /// (as a `From` implementation would be)
97    #[instrument(skip_all, parent = Span::current(), level = "Trace")]
98    pub(super) fn from_uninit(
99        host_funcs: Arc<Mutex<FunctionRegistry>>,
100        mgr: SandboxMemoryManager<HostSharedMemory>,
101        vm: HyperlightVm,
102    ) -> MultiUseSandbox {
103        Self {
104            id: super::snapshot::SANDBOX_CONFIGURATION_COUNTER.fetch_add(1, Ordering::Relaxed),
105            host_funcs,
106            mem_mgr: Arc::new(Mutex::new(mgr)),
107            vm: Arc::new(vm),
108            control_lock: Mutex::new(()),
109            invoke_workers: Mutex::new(None),
110            published_blobs: Mutex::new(HashSet::new()),
111        }
112    }
113
114    /// Returns this sandbox's unique id.
115    pub fn id(&self) -> u64 {
116        self.id
117    }
118
119    /// Fixed vCPU pool size configured for this sandbox.
120    pub fn vcpu_count(&self) -> Result<usize> {
121        let mem_mgr = self
122            .mem_mgr
123            .lock()
124            .map_err(|_| crate::new_error!("sandbox memory manager mutex poisoned"))?;
125        Ok(mem_mgr.layout.get_vcpu_count())
126    }
127
128    /// Call a guest function by `fn_id` with a raw byte payload.
129    /// Returns the response payload bytes on success.
130    ///
131    /// Wire format: the host serialises a
132    /// [`nub_host_common::rpc::Request`] (rkyv) carrying `fn_id` and
133    /// `payload`, ships it via the input data ring, the guest decodes
134    /// + dispatches + writes a `Response` to the output ring, and we
135    /// read + check `status` before returning the inner payload.
136    ///
137    /// Changes made to the sandbox during execution are persisted.
138    /// On failure the sandbox should be dropped and rebuilt.
139    #[instrument(err(Debug), skip(self, payload), parent = Span::current())]
140    pub fn call_raw(&self, fn_id: u32, payload: &[u8]) -> Result<Vec<u8>> {
141        maybe_time_and_emit_guest_call("call_raw", || {
142            let mut workers = self
143                .invoke_workers
144                .lock()
145                .map_err(|_| crate::new_error!("parallel invoke worker mutex poisoned"))?;
146            self.stop_invoke_workers_locked(&mut workers)?;
147            let _control = self
148                .control_lock
149                .lock()
150                .map_err(|_| crate::new_error!("sandbox control mutex poisoned"))?;
151            self.call_guest_function_by_id_on_locked(VcpuLane::PRIMARY, fn_id, payload)
152        })
153    }
154
155    /// Serialized control-plane call on a selected vCPU lane. This still uses
156    /// the legacy shared input/output rings and therefore must not be used as
157    /// the concurrent invoke mechanism; it exists to validate and bootstrap
158    /// non-primary lanes. Concurrent invokes use the per-lane worker slots.
159    #[instrument(err(Debug), skip(self, payload), parent = Span::current())]
160    pub fn call_raw_on_vcpu(
161        &self,
162        vcpu_index: usize,
163        fn_id: u32,
164        payload: &[u8],
165    ) -> Result<Vec<u8>> {
166        let lane = VcpuLane::new(vcpu_index);
167        maybe_time_and_emit_guest_call("call_raw_on_vcpu", || {
168            let mut workers = self
169                .invoke_workers
170                .lock()
171                .map_err(|_| crate::new_error!("parallel invoke worker mutex poisoned"))?;
172            self.stop_invoke_workers_locked(&mut workers)?;
173            let _control = self
174                .control_lock
175                .lock()
176                .map_err(|_| crate::new_error!("sandbox control mutex poisoned"))?;
177            self.call_guest_function_by_id_on_locked(lane, fn_id, payload)
178        })
179    }
180
181    fn call_guest_function_by_id_on_locked(
182        &self,
183        lane: VcpuLane,
184        fn_id: u32,
185        payload: &[u8],
186    ) -> Result<Vec<u8>> {
187        // ===== KILL() TIMING POINT 1 =====
188        // Clear any stale cancellation from a previous guest function call or if kill() was called too early.
189        // Any kill() that completed (even partially) BEFORE this line has NO effect on this call.
190        self.vm.clear_cancel();
191
192        let res = (|| {
193            let req = Request {
194                fn_id,
195                payload: payload.to_vec(),
196            };
197            let req_bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&req)
198                .map_err(|e| crate::new_error!("rkyv-serialize Request: {e}"))?;
199
200            let mut mem_mgr = self
201                .mem_mgr
202                .lock()
203                .map_err(|_| crate::new_error!("sandbox memory manager mutex poisoned"))?;
204
205            mem_mgr.write_guest_function_call_raw(req_bytes.as_slice())?;
206
207            let dispatch_res = if lane == VcpuLane::PRIMARY {
208                self.vm
209                    .dispatch_call_from_host(&mut mem_mgr, &self.host_funcs)
210            } else {
211                self.vm
212                    .dispatch_call_from_host_on(lane, &mut mem_mgr, &self.host_funcs)
213            };
214
215            if let Err(e) = dispatch_res {
216                let (error, _should_poison) = e.promote();
217                return Err(error);
218            }
219
220            let raw_resp = mem_mgr.read_guest_function_call_result_raw()?;
221
222            let mut aligned = AlignedVec::<16>::with_capacity(raw_resp.len());
223            aligned.extend_from_slice(&raw_resp);
224
225            let resp = rkyv::access::<ArchivedResponse, rkyv::rancor::Error>(aligned.as_slice())
226                .map_err(|e| crate::new_error!("rkyv-access Response: {e}"))?;
227
228            let status = resp.status.to_native();
229            if status != 0 {
230                let msg = resp
231                    .error_msg
232                    .as_ref()
233                    .map(|s| s.as_str().to_string())
234                    .unwrap_or_else(|| format!("guest fn_id={fn_id} returned status {status}"));
235                metrics::counter!(
236                    METRIC_GUEST_ERROR,
237                    METRIC_GUEST_ERROR_LABEL_CODE => status.to_string()
238                )
239                .increment(1);
240                return Err(HyperlightError::GuestError(
241                    hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode::GuestError,
242                    msg,
243                ));
244            }
245
246            Ok(resp.payload.as_slice().to_vec())
247        })();
248
249        // Clear partial abort bytes so they don't leak across calls.
250        let mut mem_mgr = self
251            .mem_mgr
252            .lock()
253            .map_err(|_| crate::new_error!("sandbox memory manager mutex poisoned"))?;
254        mem_mgr.abort_buffer.clear();
255
256        if res.is_err() {
257            mem_mgr.clear_io_buffers();
258        }
259
260        res
261    }
262
263    /// Submit an invoke packet through the per-lane parallel worker slots.
264    ///
265    /// Workers are started lazily and remain hot for subsequent invoke calls.
266    /// The legacy raw RPC channel remains the serialized control plane.
267    /// User job waits deliberately have no host-side timeout; without a
268    /// cancellation API, the lane must stay reserved until the guest reports
269    /// completion or the worker exits.
270    pub fn invoke_cached_parallel(
271        &self,
272        job_id: u64,
273        packet: &InvokePacket,
274    ) -> Result<InvocationResult> {
275        let (workers, lane) = loop {
276            let workers = self.ensure_invoke_workers()?;
277            if let Some(lane) = workers.try_acquire_lane()? {
278                break (workers, lane);
279            }
280            if let Some(lane_idx) = workers.reserve_unstarted_lane()? {
281                match self.start_invoke_worker_lane(lane_idx) {
282                    Ok(handle) => workers.install_started_lane(lane_idx, handle)?,
283                    Err(e) => {
284                        workers.release_start_reservation(lane_idx)?;
285                        return Err(e);
286                    }
287                }
288                continue;
289            }
290            if let Some(lane) = workers.acquire_lane()? {
291                break (workers, lane);
292            }
293        };
294        let lane_idx = lane.index();
295
296        let slot = {
297            let mem_mgr = self
298                .mem_mgr
299                .lock()
300                .map_err(|_| crate::new_error!("sandbox memory manager mutex poisoned"))?;
301            mem_mgr.parallel_invoke_slot_host_ptr(lane_idx)?
302        };
303
304        // SAFETY: `lane` is a live `LaneLease`, so this host thread has
305        // exclusive host ownership of the slot until the function returns. The
306        // guest worker communicates through the ABI atomics in the same slot.
307        unsafe {
308            let status = (*slot).status.load(Ordering::Acquire);
309            if status != PARALLEL_INVOKE_STATUS_EMPTY {
310                return Err(crate::new_error!(
311                    "parallel invoke lane {} was not empty before submit (status={})",
312                    lane_idx,
313                    status
314                ));
315            }
316            (*slot).job_id.store(job_id, Ordering::Relaxed);
317            core::ptr::addr_of_mut!((*slot).packet).write_volatile(*packet);
318            fence(Ordering::Release);
319            (*slot)
320                .status
321                .store(PARALLEL_INVOKE_STATUS_READY, Ordering::Release);
322        }
323
324        loop {
325            let done = unsafe {
326                match (*slot).status.load(Ordering::Acquire) {
327                    PARALLEL_INVOKE_STATUS_DONE => {
328                        fence(Ordering::Acquire);
329                        let result = core::ptr::addr_of!((*slot).result).read_volatile();
330                        (*slot)
331                            .status
332                            .store(PARALLEL_INVOKE_STATUS_EMPTY, Ordering::Release);
333                        Some(result)
334                    }
335                    PARALLEL_INVOKE_STATUS_READY | PARALLEL_INVOKE_STATUS_RUNNING => None,
336                    other => {
337                        return Err(crate::new_error!(
338                            "parallel invoke lane {} entered unexpected status {}",
339                            lane_idx,
340                            other
341                        ));
342                    }
343                }
344            };
345            if let Some(result) = done {
346                return Ok(result);
347            }
348            if let Some(worker_result) = workers.take_finished_result(lane_idx) {
349                let detail = match worker_result {
350                    Ok(()) => "clean worker exit".to_string(),
351                    Err(e) => e,
352                };
353                return Err(crate::new_error!(
354                    "parallel invoke worker lane {} exited while job {} was pending: {}",
355                    lane_idx,
356                    job_id,
357                    detail
358                ));
359            }
360            thread::yield_now();
361        }
362    }
363
364    /// Bench-only: evict guest JIT caches while keeping the hot invoke worker
365    /// pool alive. The legacy raw RPC path stops workers before entering the
366    /// shared control ring; cold benchmarks call this every iteration, so using
367    /// the worker slot protocol avoids measuring worker teardown/startup.
368    ///
369    /// All lanes are reserved first. That preserves the eviction invariant: no
370    /// frame runtime can be live while image arenas and templates are dropped.
371    pub fn evict_jit_all_parallel(&self) -> Result<()> {
372        maybe_time_and_emit_guest_call("evict_jit_all_parallel", || {
373            let workers = self.ensure_invoke_workers()?;
374            let lanes = workers.acquire_all_lanes()?;
375            let Some(control_lane) = lanes.first().map(LaneLease::index) else {
376                return Ok(());
377            };
378
379            let slot = {
380                let mem_mgr = self
381                    .mem_mgr
382                    .lock()
383                    .map_err(|_| crate::new_error!("sandbox memory manager mutex poisoned"))?;
384                mem_mgr.parallel_invoke_slot_host_ptr(control_lane)?
385            };
386
387            // SAFETY: `lanes` contains a live lease for every started lane, so
388            // no host invoke can use `control_lane` while this control command
389            // is in flight. The guest worker observes the ABI atomic status.
390            unsafe {
391                let status = (*slot).status.load(Ordering::Acquire);
392                if status != PARALLEL_INVOKE_STATUS_EMPTY {
393                    return Err(crate::new_error!(
394                        "parallel control lane {} was not empty before evict (status={})",
395                        control_lane,
396                        status
397                    ));
398                }
399                (*slot).job_id.store(0, Ordering::Relaxed);
400                fence(Ordering::Release);
401                (*slot)
402                    .status
403                    .store(PARALLEL_INVOKE_STATUS_EVICT_JIT_READY, Ordering::Release);
404            }
405
406            loop {
407                let done = unsafe {
408                    match (*slot).status.load(Ordering::Acquire) {
409                        PARALLEL_INVOKE_STATUS_DONE => {
410                            fence(Ordering::Acquire);
411                            (*slot)
412                                .status
413                                .store(PARALLEL_INVOKE_STATUS_EMPTY, Ordering::Release);
414                            true
415                        }
416                        PARALLEL_INVOKE_STATUS_EVICT_JIT_READY | PARALLEL_INVOKE_STATUS_RUNNING => {
417                            false
418                        }
419                        other => {
420                            return Err(crate::new_error!(
421                                "parallel control lane {} entered unexpected status {}",
422                                control_lane,
423                                other
424                            ));
425                        }
426                    }
427                };
428                if done {
429                    return Ok(());
430                }
431                if let Some(worker_result) = workers.take_finished_result(control_lane) {
432                    let detail = match worker_result {
433                        Ok(()) => "clean worker exit".to_string(),
434                        Err(e) => e,
435                    };
436                    return Err(crate::new_error!(
437                        "parallel invoke worker lane {} exited during evict_jit_all: {}",
438                        control_lane,
439                        detail
440                    ));
441                }
442                thread::yield_now();
443            }
444        })
445    }
446
447    fn ensure_invoke_workers(&self) -> Result<Arc<ParallelInvokeWorkers>> {
448        let mut guard = self
449            .invoke_workers
450            .lock()
451            .map_err(|_| crate::new_error!("parallel invoke worker mutex poisoned"))?;
452        if let Some(workers) = guard.as_ref().cloned() {
453            return Ok(workers);
454        }
455
456        let vcpu_count = self.vcpu_count()?;
457        let first_handle = self.start_invoke_worker_lane(0)?;
458        let workers = Arc::new(ParallelInvokeWorkers::new(vcpu_count, 0, first_handle));
459        *guard = Some(workers.clone());
460        Ok(workers)
461    }
462
463    fn stop_invoke_workers_locked(
464        &self,
465        guard: &mut Option<Arc<ParallelInvokeWorkers>>,
466    ) -> Result<()> {
467        let Some(workers) = guard.as_ref().cloned() else {
468            return Ok(());
469        };
470
471        let started_lanes = workers.mark_stopping_and_wait_idle()?;
472        for lane in started_lanes {
473            {
474                let mem_mgr = self
475                    .mem_mgr
476                    .lock()
477                    .map_err(|_| crate::new_error!("sandbox memory manager mutex poisoned"))?;
478                mem_mgr.write_parallel_invoke_status(lane, PARALLEL_INVOKE_STATUS_STOP)?;
479            }
480            workers
481                .join_lane(lane, Duration::from_secs(5))?
482                .map_err(|e| crate::new_error!("parallel invoke worker lane {lane}: {e}"))?;
483        }
484
485        *guard = None;
486        Ok(())
487    }
488
489    fn start_invoke_worker_lane(&self, lane: usize) -> Result<InvokeWorkerHandle> {
490        let _control = self
491            .control_lock
492            .lock()
493            .map_err(|_| crate::new_error!("sandbox control mutex poisoned"))?;
494
495        {
496            let mut mem_mgr = self
497                .mem_mgr
498                .lock()
499                .map_err(|_| crate::new_error!("sandbox memory manager mutex poisoned"))?;
500            mem_mgr.write_parallel_invoke_status(lane, PARALLEL_INVOKE_STATUS_STARTING)?;
501            let req = Request {
502                fn_id: FN_ID_NUB_INVOKE_WORKER,
503                payload: (lane as u32).to_le_bytes().to_vec(),
504            };
505            let req_bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&req)
506                .map_err(|e| crate::new_error!("rkyv-serialize worker Request: {e}"))?;
507            mem_mgr.write_guest_function_call_raw(req_bytes.as_slice())?;
508        }
509
510        let vm = self.vm.clone();
511        let mem_mgr = self.mem_mgr.clone();
512        let host_funcs = self.host_funcs.clone();
513        let handle = thread::Builder::new()
514            .name(format!("nub-vcpu-worker-{lane}"))
515            .spawn(move || {
516                let lane = VcpuLane::new(lane);
517                vm.dispatch_call_from_host_on_shared(lane, &mem_mgr, &host_funcs)
518                    .map_err(|e| format!("dispatch worker lane {}: {e}", lane.index()))?;
519                let mut mem_mgr = mem_mgr
520                    .lock()
521                    .map_err(|_| "sandbox memory manager mutex poisoned".to_string())?;
522                let _ = mem_mgr
523                    .read_guest_function_call_result_raw()
524                    .map_err(|e| format!("read worker shutdown response: {e}"))?;
525                Ok(())
526            })
527            .map_err(|e| crate::new_error!("spawn invoke worker lane {lane}: {e}"))?;
528
529        let deadline = Instant::now() + Duration::from_secs(5);
530        loop {
531            let status = {
532                let mem_mgr = self
533                    .mem_mgr
534                    .lock()
535                    .map_err(|_| crate::new_error!("sandbox memory manager mutex poisoned"))?;
536                mem_mgr.read_parallel_invoke_status(lane)?
537            };
538            if status == PARALLEL_INVOKE_STATUS_EMPTY {
539                return Ok(handle);
540            }
541            if handle.is_finished() {
542                let worker_result = handle
543                    .join()
544                    .map_err(|_| crate::new_error!("invoke worker lane {lane} panicked"))?;
545                return Err(crate::new_error!(
546                    "invoke worker lane {} exited during startup: {}",
547                    lane,
548                    worker_result
549                        .err()
550                        .unwrap_or_else(|| "clean exit before startup handshake".to_string())
551                ));
552            }
553            if Instant::now() >= deadline {
554                return Err(crate::new_error!(
555                    "invoke worker lane {} timed out during startup (status={})",
556                    lane,
557                    status
558                ));
559            }
560            thread::yield_now();
561        }
562    }
563
564    /// Publish a serialized state object into the guest's
565    /// heap-resident object store via the [`FN_ID_NUB_PUT_CAP`] RPC.
566    ///
567    /// `bytes` is the personality-encoded object (JAVM: an
568    /// rkyv-encoded `Cap`), shipped opaquely via [`Self::call_raw`];
569    /// the guest-computed content hash is read back. The guest-side
570    /// personality decodes, validates, hashes, and inserts the object
571    /// into its store.
572    ///
573    /// Encode/decode failures are surfaced as
574    /// `HyperlightError::Error`. A sentinel response (all-`0xFF`
575    /// hash) from the guest is also turned into an error.
576    pub fn put_object(&self, bytes: &[u8]) -> Result<AbiCapHash> {
577        let resp = self.call_raw(FN_ID_NUB_PUT_CAP, bytes)?;
578        if resp.len() != 32 {
579            return Err(crate::new_error!(
580                "put_object: expected 32-byte hash response, got {}",
581                resp.len()
582            ));
583        }
584        let mut hash: AbiCapHash = [0u8; 32];
585        hash.copy_from_slice(&resp);
586        // The guest's put handler returns `0xFF * 32` on decode/conv
587        // failure. Surface as a typed error so callers don't observe
588        // a fake hash.
589        if hash == [0xFFu8; 32] {
590            return Err(crate::new_error!(
591                "put_object: guest reported decode/conversion failure (sentinel response)"
592            ));
593        }
594        Ok(hash)
595    }
596
597    /// Pre-hashed put: idempotent fast path that short-circuits the
598    /// full [`Self::put_object`] RPC when this sandbox has already
599    /// published `hash`.
600    ///
601    /// Behaviour:
602    ///
603    /// - If `hash` is in the host-side `published_blobs` set,
604    ///   return immediately — we already shipped this object and
605    ///   publication is permanent (the personality obligation on
606    ///   `published_blobs`), so the guest still holds it. The
607    ///   `serialize` closure is never called: no encode, no VMEXIT, no
608    ///   guest decode + hash walk + store insert. This is the hot path
609    ///   for bench loops that re-publish the same object graph every
610    ///   iteration.
611    /// - Otherwise, call `serialize()` (personality encode; JAVM: rkyv
612    ///   of `Cap`, which fails on unresolved `CapHashOrRef::Ref`
613    ///   handles), ship `put_object`, debug-assert the returned hash
614    ///   matches `hash`, and record it.
615    ///
616    /// We deliberately do **not** check the guest's store directly:
617    /// the guest's directory is a hashbrown table built with a
618    /// different SIMD `Group` width than the host's hashbrown (see
619    /// `published_blobs`), so a host-side deref of it is unsound.
620    pub fn put_object_with_hash(
621        &self,
622        hash: AbiCapHash,
623        serialize: impl FnOnce() -> std::result::Result<Vec<u8>, String>,
624    ) -> Result<()> {
625        {
626            let published_blobs = self
627                .published_blobs
628                .lock()
629                .map_err(|_| crate::new_error!("published blob set mutex poisoned"))?;
630            if published_blobs.contains(&hash) {
631                return Ok(());
632            }
633        }
634
635        let bytes =
636            serialize().map_err(|e| crate::new_error!("put_object_with_hash: encode: {e}"))?;
637        let got = self.put_object(&bytes)?;
638        debug_assert_eq!(
639            got, hash,
640            "put_object_with_hash: guest-computed hash differs from claimed hash"
641        );
642
643        self.published_blobs
644            .lock()
645            .map_err(|_| crate::new_error!("published blob set mutex poisoned"))?
646            .insert(hash);
647        Ok(())
648    }
649
650    /// Returns a handle for interrupting guest execution.
651    pub fn interrupt_handle(&self) -> Arc<dyn InterruptHandle> {
652        self.vm.interrupt_handle()
653    }
654}
655
656type InvokeWorkerResult = std::result::Result<(), String>;
657type InvokeWorkerHandle = JoinHandle<InvokeWorkerResult>;
658
659struct ParallelInvokeWorkers {
660    lane_count: usize,
661    state: Mutex<ParallelInvokeWorkerState>,
662    ready: Condvar,
663    handles: Mutex<Vec<Option<InvokeWorkerHandle>>>,
664}
665
666struct ParallelInvokeWorkerState {
667    available: Vec<usize>,
668    started: Vec<bool>,
669    stopping: bool,
670}
671
672struct LaneLease {
673    lane: usize,
674    workers: Arc<ParallelInvokeWorkers>,
675}
676
677impl ParallelInvokeWorkers {
678    fn new(lane_count: usize, first_lane: usize, first_handle: InvokeWorkerHandle) -> Self {
679        let mut handles = Vec::with_capacity(lane_count);
680        handles.resize_with(lane_count, || None);
681        handles[first_lane] = Some(first_handle);
682        let mut started = vec![false; lane_count];
683        started[first_lane] = true;
684        Self {
685            lane_count,
686            state: Mutex::new(ParallelInvokeWorkerState {
687                available: vec![first_lane],
688                started,
689                stopping: false,
690            }),
691            ready: Condvar::new(),
692            handles: Mutex::new(handles),
693        }
694    }
695
696    fn try_acquire_lane(self: &Arc<Self>) -> Result<Option<LaneLease>> {
697        let mut state = self
698            .state
699            .lock()
700            .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
701        if state.stopping {
702            return Ok(None);
703        }
704        Ok(state.available.pop().map(|lane| LaneLease {
705            lane,
706            workers: self.clone(),
707        }))
708    }
709
710    fn acquire_lane(self: &Arc<Self>) -> Result<Option<LaneLease>> {
711        let mut state = self
712            .state
713            .lock()
714            .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
715        loop {
716            if state.stopping {
717                return Ok(None);
718            }
719            if let Some(lane) = state.available.pop() {
720                return Ok(Some(LaneLease {
721                    lane,
722                    workers: self.clone(),
723                }));
724            }
725            state = self
726                .ready
727                .wait(state)
728                .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
729        }
730    }
731
732    fn reserve_unstarted_lane(&self) -> Result<Option<usize>> {
733        let mut state = self
734            .state
735            .lock()
736            .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
737        if state.stopping {
738            return Ok(None);
739        }
740        for lane in 0..self.lane_count {
741            if !state.started[lane] {
742                state.started[lane] = true;
743                return Ok(Some(lane));
744            }
745        }
746        Ok(None)
747    }
748
749    fn install_started_lane(&self, lane: usize, handle: InvokeWorkerHandle) -> Result<()> {
750        {
751            let mut handles = self
752                .handles
753                .lock()
754                .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
755            if lane >= handles.len() || handles[lane].is_some() {
756                return Err(crate::new_error!(
757                    "parallel invoke worker lane {} already has a handle",
758                    lane
759                ));
760            }
761            handles[lane] = Some(handle);
762        }
763        self.release_lane(lane);
764        Ok(())
765    }
766
767    fn release_start_reservation(&self, lane: usize) -> Result<()> {
768        let mut state = self
769            .state
770            .lock()
771            .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
772        if lane < state.started.len() {
773            state.started[lane] = false;
774        }
775        self.ready.notify_all();
776        Ok(())
777    }
778
779    fn acquire_all_lanes(self: &Arc<Self>) -> Result<Vec<LaneLease>> {
780        let mut state = self
781            .state
782            .lock()
783            .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
784        loop {
785            if state.stopping {
786                return Err(crate::new_error!("parallel invoke workers are stopping"));
787            }
788            let started_count = state.started.iter().filter(|&&started| started).count();
789            if state.available.len() == started_count {
790                return Ok(state
791                    .available
792                    .drain(..)
793                    .map(|lane| LaneLease {
794                        lane,
795                        workers: self.clone(),
796                    })
797                    .collect());
798            }
799            state = self
800                .ready
801                .wait(state)
802                .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
803        }
804    }
805
806    fn release_lane(&self, lane: usize) {
807        let mut state = self.state.lock().expect("parallel worker mutex poisoned");
808        state.available.push(lane);
809        self.ready.notify_all();
810    }
811
812    fn mark_stopping_and_wait_idle(&self) -> Result<Vec<usize>> {
813        let mut state = self
814            .state
815            .lock()
816            .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
817        state.stopping = true;
818        self.ready.notify_all();
819        let started_count = state.started.iter().filter(|&&started| started).count();
820        while state.available.len() != started_count {
821            state = self
822                .ready
823                .wait(state)
824                .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
825        }
826        Ok(state
827            .started
828            .iter()
829            .enumerate()
830            .filter_map(|(lane, &started)| started.then_some(lane))
831            .collect())
832    }
833
834    fn take_finished_result(&self, lane: usize) -> Option<InvokeWorkerResult> {
835        let mut handles = self.handles.lock().expect("parallel worker mutex poisoned");
836        let handle = handles.get_mut(lane)?.take_if(|h| h.is_finished())?;
837        Some(match handle.join() {
838            Ok(result) => result,
839            Err(_) => Err("worker thread panicked".to_string()),
840        })
841    }
842
843    fn join_lane(&self, lane: usize, timeout: Duration) -> Result<InvokeWorkerResult> {
844        let handle = self
845            .handles
846            .lock()
847            .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?
848            .get_mut(lane)
849            .and_then(Option::take);
850        let Some(handle) = handle else {
851            return Ok(Ok(()));
852        };
853        join_invoke_worker_handle(lane, handle, timeout)
854    }
855}
856
857fn join_invoke_worker_handle(
858    lane: usize,
859    handle: InvokeWorkerHandle,
860    timeout: Duration,
861) -> Result<InvokeWorkerResult> {
862    let deadline = Instant::now() + timeout;
863    while !handle.is_finished() {
864        if Instant::now() >= deadline {
865            return Err(crate::new_error!(
866                "parallel invoke worker lane {} timed out during stop",
867                lane
868            ));
869        }
870        thread::yield_now();
871    }
872
873    Ok(match handle.join() {
874        Ok(result) => result,
875        Err(_) => Err("worker thread panicked".to_string()),
876    })
877}
878
879impl LaneLease {
880    fn index(&self) -> usize {
881        self.lane
882    }
883}
884
885impl Drop for LaneLease {
886    fn drop(&mut self) {
887        self.workers.release_lane(self.lane);
888    }
889}
890
891impl std::fmt::Debug for MultiUseSandbox {
892    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
893        f.debug_struct("MultiUseSandbox").finish()
894    }
895}