char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somsnapshot: capture and restore extension layouts in one artifactd71572b

main
17.4 KiB482 linesraw
1use std::borrow::Cow;
2use std::io::Write;
3use std::mem::ManuallyDrop;
4use std::sync::{Mutex, OnceLock, PoisonError};
5
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8
9use crate::{Error, Extension, Runtime, RuntimeOptions, SnapshotError, extensions, op};
10
11const MAGIC: &[u8; 8] = b"AVRILSNP";
12const FORMAT: u32 = 1;
13const HEADER: usize = 16;
14
15/// Static extension heaps that share one V8 read-only heap, captured before
16/// live initialization or guest execution.
17#[derive(Clone)]
18pub struct Snapshot {
19    bytes: Cow<'static, [u8]>,
20    blob: usize,
21    manifest: Manifest,
22}
23
24#[derive(Clone, Serialize, Deserialize)]
25struct Manifest {
26    /// SHA-256 of the V8 startup data, identifying its read-only heap.
27    heap: [u8; 32],
28    layouts: Vec<Layout>,
29}
30
31#[derive(Clone, Serialize, Deserialize)]
32pub(crate) struct Layout {
33    fingerprint: [u8; 32],
34    pub(crate) context: usize,
35    pub(crate) state: extensions::SnapshotState,
36}
37
38impl Snapshot {
39    pub fn as_bytes(&self) -> &[u8] {
40        &self.bytes
41    }
42
43    /// Import a trusted snapshot artifact.
44    ///
45    /// # Safety
46    /// The bytes must be an intact artifact from `Runtime::create_snapshot`,
47    /// produced with the same avril-core sources, target, V8 build, and flags,
48    /// except for the generator-only random seed set by `initialize_snapshot_generator`.
49    /// A build-time generator may be a different executable; its native reference
50    /// catalog must match exactly. Only the avril container is validated here;
51    /// snapshots are not an untrusted input format.
52    pub unsafe fn from_bytes(bytes: impl Into<Cow<'static, [u8]>>) -> Result<Self, SnapshotError> {
53        let bytes = bytes.into();
54        if bytes.len() < HEADER || bytes[..8] != MAGIC[..] {
55            return Err(SnapshotError::InvalidArtifact("not an avril snapshot"));
56        }
57        let field = |at: usize| u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap());
58        if field(8) != FORMAT {
59            return Err(SnapshotError::InvalidArtifact(
60                "unsupported snapshot format",
61            ));
62        }
63        let blob = usize::try_from(field(12))
64            .ok()
65            .and_then(|length| HEADER.checked_add(length))
66            .filter(|blob| *blob <= bytes.len())
67            .ok_or(SnapshotError::InvalidArtifact(
68                "truncated snapshot manifest",
69            ))?;
70        let manifest = serde_json::from_slice(&bytes[HEADER..blob])?;
71        Ok(Self { bytes, blob, manifest })
72    }
73
74    pub(crate) fn layout(&self, fingerprint: &[u8; 32]) -> Result<&Layout, SnapshotError> {
75        self.manifest
76            .layouts
77            .iter()
78            .find(|layout| layout.fingerprint == *fingerprint)
79            .ok_or(SnapshotError::IncompatibleLayout)
80    }
81
82    fn startup_data(&self) -> v8::StartupData {
83        match &self.bytes {
84            Cow::Borrowed(bytes) => {
85                let bytes: &'static [u8] = bytes;
86                Cow::Borrowed(&bytes[self.blob..])
87            }
88            Cow::Owned(bytes) => Cow::Owned(bytes[self.blob..].to_vec()),
89        }
90        .into()
91    }
92}
93
94/// The read-only heap a V8 isolate is created from. V8 shares one read-only heap
95/// between all live isolates of a process and requires every isolate to bring a
96/// byte-identical image; separately generated artifacts never do (see SNAPSHOTS.md).
97#[derive(Clone, Copy, PartialEq, Eq)]
98enum Heap {
99    BuiltIn,
100    Snapshot([u8; 32]),
101}
102
103static LIVE_HEAP: Mutex<Option<(Heap, usize)>> = Mutex::new(None);
104
105/// Accounts for one isolate on the process read-only heap; dropped after its isolate.
106struct HeapLease(Heap);
107
108impl HeapLease {
109    fn acquire(heap: Heap) -> Result<Self, SnapshotError> {
110        let mut live = LIVE_HEAP.lock().unwrap_or_else(PoisonError::into_inner);
111        match &mut *live {
112            Some((current, isolates)) if *current == heap => *isolates += 1,
113            Some(_) => return Err(SnapshotError::ReadOnlyHeapInUse),
114            None => *live = Some((heap, 1)),
115        }
116        Ok(Self(heap))
117    }
118}
119
120impl Drop for HeapLease {
121    fn drop(&mut self) {
122        let mut live = LIVE_HEAP.lock().unwrap_or_else(PoisonError::into_inner);
123        if let Some((heap, isolates)) = &mut *live {
124            debug_assert!(*heap == self.0);
125            *isolates -= 1;
126            if *isolates == 0 {
127                *live = None;
128            }
129        }
130    }
131}
132
133#[derive(Clone, Copy)]
134pub(crate) enum Source<'a> {
135    BuiltIn,
136    Creator,
137    Snapshot(&'a Snapshot),
138}
139
140// rusty_v8 has no public query for the thread's entered isolate; it calls this
141// binding.cc export for its own OwnedIsolate drop check. Keep it this one sealed joint.
142unsafe extern "C" {
143    fn v8__Isolate__GetCurrent() -> *mut std::ffi::c_void;
144}
145
146fn is_current(isolate: &v8::Isolate) -> bool {
147    // SAFETY: UnsafeRawIsolatePtr is repr(transparent) over the isolate pointer, the
148    // same `v8::Isolate*` that GetCurrent returns; only addresses are compared.
149    let isolate = unsafe {
150        std::mem::transmute::<v8::UnsafeRawIsolatePtr, *mut std::ffi::c_void>(
151            isolate.as_raw_isolate_ptr(),
152        )
153    };
154    // SAFETY: GetCurrent only reads V8's thread-local entered isolate.
155    unsafe { v8__Isolate__GetCurrent() == isolate }
156}
157
158// Snapshot creators cannot be dropped without create_blob in rusty_v8. Keep that
159// ownership exception here, rather than spreading ManuallyDrop through Runtime.
160pub(crate) struct IsolateOwner {
161    isolate: Option<v8::OwnedIsolate>,
162    // Released only once V8 has deleted the isolate: explicitly in Drop, or after
163    // create_blob by field order.
164    lease: Option<HeapLease>,
165    creator: bool,
166}
167
168impl IsolateOwner {
169    /// Rejects an incompatible read-only heap before V8 creates the isolate.
170    pub(crate) fn new(source: Source) -> Result<Self, SnapshotError> {
171        let lease = HeapLease::acquire(match source {
172            Source::BuiltIn | Source::Creator => Heap::BuiltIn,
173            Source::Snapshot(snapshot) => Heap::Snapshot(snapshot.manifest.heap),
174        })?;
175        let references = external_references().into();
176        let mut isolate = match source {
177            Source::BuiltIn => {
178                v8::Isolate::new(v8::Isolate::create_params().external_references(references))
179            }
180            Source::Creator => v8::Isolate::snapshot_creator(Some(references), None),
181            Source::Snapshot(snapshot) => v8::Isolate::new(
182                v8::Isolate::create_params()
183                    .external_references(references)
184                    .snapshot_blob(snapshot.startup_data()),
185            ),
186        };
187        if !matches!(source, Source::Snapshot(_)) {
188            crate::microtasks::add_listener(&mut isolate);
189        }
190        Ok(Self {
191            isolate: Some(isolate),
192            lease: Some(lease),
193            creator: matches!(source, Source::Creator),
194        })
195    }
196
197    pub(crate) fn is_creator(&self) -> bool {
198        self.creator
199    }
200
201    pub(crate) fn release_native_state(&mut self) {
202        release_native_state(self);
203    }
204
205    fn create_blob(mut self) -> Option<v8::StartupData> {
206        assert!(self.creator);
207        self.release_native_state();
208        self.isolate
209            .take()
210            .unwrap()
211            .create_blob(v8::FunctionCodeHandling::Keep)
212    }
213}
214
215impl std::ops::Deref for IsolateOwner {
216    type Target = v8::OwnedIsolate;
217
218    fn deref(&self) -> &Self::Target {
219        self.isolate.as_ref().unwrap()
220    }
221}
222
223impl std::ops::DerefMut for IsolateOwner {
224    fn deref_mut(&mut self) -> &mut Self::Target {
225        self.isolate.as_mut().unwrap()
226    }
227}
228
229impl Drop for IsolateOwner {
230    fn drop(&mut self) {
231        // After create_blob, V8 has deleted the creator and the lease field releases normally.
232        let Some(isolate) = self.isolate.take() else {
233            return;
234        };
235        // A panicking disposal, such as rusty_v8's out-of-order drop check, leaves the
236        // native isolate holding V8's read-only heap; keep it accounted until exit.
237        let lease = ManuallyDrop::new(self.lease.take());
238        if !self.creator {
239            drop(isolate);
240            drop(ManuallyDrop::into_inner(lease));
241            return;
242        }
243        // Like Deno, abandon the creator rather than panic or serialize a failed heap.
244        // It keeps V8's built-in read-only heap alive, so its lease stays pinned too.
245        let mut isolate = ManuallyDrop::new(isolate);
246        // Native state may own isolates entered after the creator, so release it first,
247        // but still leave the creator if one of its destructors panics.
248        let released = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
249            release_native_state(&mut isolate)
250        }));
251        if is_current(&isolate) {
252            // V8 entered the creator on construction and only exits it in the destructor
253            // we must skip. Restore the thread's previous isolate for runtimes created earlier.
254            // SAFETY: The creator is this thread's current isolate, and it is never used again.
255            unsafe { isolate.exit() };
256        }
257        let _ = writeln!(
258            std::io::stderr(),
259            "WARNING: abandoned snapshot isolate was leaked"
260        );
261        // A second panic during unwinding would abort; the first one is already propagating.
262        if let Err(panic) = released
263            && !std::thread::panicking()
264        {
265            std::panic::resume_unwind(panic);
266        }
267    }
268}
269
270/// Drops core-owned native state, including every global handle it holds.
271fn release_native_state(isolate: &mut v8::Isolate) {
272    crate::tasks::clear(isolate);
273    op::clear(isolate);
274    crate::microtasks::clear(isolate);
275    crate::reporting::clear(isolate);
276    crate::stack_trace::clear(isolate);
277    crate::modules::clear(isolate);
278    extensions::clear(isolate);
279    isolate.remove_slot::<std::rc::Rc<std::cell::RefCell<crate::WasmStreamingRegistry>>>();
280    isolate.remove_slot::<op::RegisteredOps>();
281}
282
283/// Identity of every layout, including the core-owned modules and native catalog
284/// shared by all of them. Fields are length-prefixed under a domain tag.
285pub(crate) fn fingerprint(extensions: &[Extension]) -> Result<[u8; 32], Error> {
286    let order = extensions::validate(extensions)?;
287    let mut hash = Sha256::new();
288    field(&mut hash, b"avril-core snapshot layout v1");
289    hash.update(core_identity());
290    let entries: Vec<_> = order
291        .iter()
292        .filter_map(|index| extensions[*index].entry_point)
293        .collect();
294    count(&mut hash, entries.len());
295    for entry in entries {
296        field(&mut hash, entry.as_bytes());
297    }
298    let mut sources: Vec<_> = extensions
299        .iter()
300        .flat_map(|extension| extension.modules.iter().copied())
301        .collect();
302    sources.sort_unstable_by_key(|(name, _)| *name);
303    count(&mut hash, sources.len());
304    for (name, source) in sources {
305        field(&mut hash, name.as_bytes());
306        field(&mut hash, source.as_bytes());
307    }
308    let mut ops: Vec<_> = extensions
309        .iter()
310        .flat_map(|extension| extension.ops.iter().map(|op| op.name))
311        .collect();
312    ops.sort_unstable();
313    count(&mut hash, ops.len());
314    for name in ops {
315        field(&mut hash, name.as_bytes());
316    }
317    Ok(hash.finalize().into())
318}
319
320fn core_identity() -> &'static [u8; 32] {
321    static IDENTITY: OnceLock<[u8; 32]> = OnceLock::new();
322    IDENTITY.get_or_init(|| {
323        let mut hash = Sha256::new();
324        field(&mut hash, b"avril-core");
325        field(&mut hash, include_str!("primordials.js").as_bytes());
326        count(&mut hash, extensions::SOURCES.len());
327        for (name, source) in extensions::SOURCES {
328            field(&mut hash, name.as_bytes());
329            field(&mut hash, source.as_bytes());
330        }
331        let modules = extensions::native_modules();
332        count(&mut hash, modules.len());
333        for (name, exports) in modules {
334            field(&mut hash, name.as_bytes());
335            count(&mut hash, exports.len());
336            for (export, _) in exports {
337                field(&mut hash, export.as_bytes());
338            }
339        }
340        for (group, references) in external_reference_groups() {
341            field(&mut hash, group.as_bytes());
342            count(&mut hash, references.len());
343        }
344        hash.finalize().into()
345    })
346}
347
348fn count(hash: &mut Sha256, count: usize) {
349    hash.update((count as u64).to_le_bytes());
350}
351
352fn field(hash: &mut Sha256, bytes: &[u8]) {
353    count(hash, bytes.len());
354    hash.update(bytes);
355}
356
357fn external_references() -> Vec<v8::ExternalReference> {
358    external_reference_groups()
359        .into_iter()
360        .flat_map(|(_, references)| references)
361        .collect()
362}
363
364fn external_reference_groups() -> [(&'static str, Vec<v8::ExternalReference>); 4] {
365    [
366        ("ops", op::RegisteredOps::external_references()),
367        ("extensions", extensions::external_references()),
368        ("modules", crate::modules::external_references()),
369        ("microtasks", crate::microtasks::external_references()),
370    ]
371}
372
373pub(crate) fn create(layouts: Vec<RuntimeOptions>) -> Result<Snapshot, Error> {
374    let fingerprints = layouts
375        .iter()
376        .map(|options| fingerprint(&options.extensions))
377        .collect::<Result<Vec<_>, _>>()?;
378    for (index, fingerprint) in fingerprints.iter().enumerate() {
379        if fingerprints[..index].contains(fingerprint) {
380            return Err(SnapshotError::DuplicateLayout.into());
381        }
382    }
383    let mut isolate = IsolateOwner::new(Source::Creator)?;
384    {
385        v8::scope!(let scope, &mut *isolate);
386        let pristine = v8::Context::new(scope, Default::default());
387        scope.set_default_context(pristine);
388    }
389    let mut captured = Vec::with_capacity(layouts.len());
390    for (options, fingerprint) in layouts.into_iter().zip(fingerprints) {
391        let runtime = Runtime::bootstrap(isolate, options, crate::ContextSource::Fresh)?;
392        let (owner, context, state) = capture(runtime)?;
393        isolate = owner;
394        captured.push(Layout { fingerprint, context, state });
395    }
396    let blob = isolate.create_blob().ok_or(SnapshotError::CreationFailed)?;
397    let manifest = serde_json::to_vec(&Manifest {
398        heap: Sha256::digest(&*blob).into(),
399        layouts: captured,
400    })
401    .map_err(SnapshotError::Metadata)?;
402    let length = u32::try_from(manifest.len())
403        .map_err(|_| SnapshotError::InvalidArtifact("snapshot manifest is too large"))?;
404    let mut bytes = Vec::with_capacity(HEADER + manifest.len() + blob.len());
405    bytes.extend_from_slice(MAGIC);
406    bytes.extend_from_slice(&FORMAT.to_le_bytes());
407    bytes.extend_from_slice(&length.to_le_bytes());
408    bytes.extend_from_slice(&manifest);
409    bytes.extend_from_slice(&blob);
410    // SAFETY: These bytes were just assembled from this creator's blob.
411    Ok(unsafe { Snapshot::from_bytes(bytes) }?)
412}
413
414/// Adds one static layout as a snapshot context, then tears down its native state
415/// so the creator can bootstrap the next layout independently.
416fn capture(
417    mut runtime: Runtime,
418) -> Result<(IsolateOwner, usize, extensions::SnapshotState), Error> {
419    if runtime.has_pending_work() {
420        return Err(SnapshotError::ActiveState("pending work").into());
421    }
422    if crate::modules::has_modules(&runtime.isolate) {
423        return Err(SnapshotError::ActiveState("guest modules").into());
424    }
425    if crate::reporting::has_handlers(&runtime.isolate) {
426        return Err(SnapshotError::ActiveState("event handlers").into());
427    }
428    if !runtime
429        .isolate
430        .get_slot::<std::rc::Rc<std::cell::RefCell<crate::WasmStreamingRegistry>>>()
431        .unwrap()
432        .borrow()
433        .is_empty()
434    {
435        return Err(SnapshotError::ActiveState("Wasm streaming").into());
436    }
437    let (context, state) = runtime.with_context(|scope| {
438        if let Err(error) = remove_stack_trace_limit(scope)? {
439            return Some(Err(error));
440        }
441        let state = extensions::capture(scope);
442        let context = scope.get_current_context();
443        Some(Ok((scope.add_context(context), state)))
444    })??;
445    let Runtime { context: handle, mut isolate, .. } = runtime;
446    drop(handle);
447    isolate.release_native_state();
448    Ok((isolate, context, state))
449}
450
451/// V8 installs `Error.stackTraceLimit` only in live contexts, so static bootstrap
452/// gets V8's default to give failures locations.
453pub(crate) fn define_stack_trace_limit(scope: &mut v8::PinScope) -> Option<()> {
454    let (error, key) = stack_trace_limit(scope)?;
455    let limit = v8::Integer::new(scope, STACK_TRACE_LIMIT);
456    error.create_data_property(scope, key.into(), limit.into())?;
457    Some(())
458}
459
460/// Restored contexts receive V8's own live property, so static code must leave the
461/// default untouched for them to match cold bootstraps.
462fn remove_stack_trace_limit(scope: &mut v8::PinScope) -> Option<Result<(), SnapshotError>> {
463    let (error, key) = stack_trace_limit(scope)?;
464    let limit = error.get(scope, key.into())?;
465    let unchanged = limit.is_int32() && limit.int32_value(scope)? == STACK_TRACE_LIMIT;
466    if !unchanged || !error.delete(scope, key.into())? {
467        return Some(Err(SnapshotError::ActiveState("Error.stackTraceLimit")));
468    }
469    Some(Ok(()))
470}
471
472/// V8's default `--stack-trace-limit`.
473const STACK_TRACE_LIMIT: i32 = 10;
474
475fn stack_trace_limit<'s>(
476    scope: &mut v8::PinScope<'s, '_>,
477) -> Option<(v8::Local<'s, v8::Object>, v8::Local<'s, v8::String>)> {
478    let global = scope.get_current_context().global(scope);
479    let key = v8::String::new(scope, "Error")?;
480    let error = v8::Local::<v8::Object>::try_from(global.get(scope, key.into())?).ok()?;
481    Some((error, v8::String::new(scope, "stackTraceLimit")?))
482}