char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
d71572b
main
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 { 19bytes : Cow < ' static , [ u8 ]>, 20blob : usize , 21manifest : Manifest , 22} 23 24# [ derive ( Clone , Serialize , Deserialize )] 25struct Manifest { 26/// SHA-256 of the V8 startup data, identifying its read-only heap. 27heap : [ u8 ; 32 ], 28layouts : Vec < Layout >, 29} 30 31# [ derive ( Clone , Serialize , Deserialize )] 32pub ( crate ) struct Layout { 33fingerprint : [ u8 ; 32 ], 34pub ( crate ) context : usize , 35pub ( crate ) state : extensions:: SnapshotState , 36} 37 38impl Snapshot { 39pub 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. 52pub unsafe fn from_bytes ( bytes : impl Into < Cow < ' static , [ u8 ]>>) ->Result < Self , SnapshotError > { 53let bytes = bytes. into (); 54if bytes. len () <HEADER || bytes[ ..8 ] !=MAGIC [ ..] { 55return Err ( SnapshotError :: InvalidArtifact ( "not an avril snapshot" )); 56} 57let field = |at : usize | u32:: from_le_bytes ( bytes[ at..at +4 ]. try_into (). unwrap ()); 58if field ( 8 ) !=FORMAT { 59return Err ( SnapshotError :: InvalidArtifact ( 60"unsupported snapshot format" , 61)); 62} 63let 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)) ?; 70let manifest = serde_json:: from_slice ( & bytes[ HEADER ..blob]) ?; 71Ok ( Self { bytes, blob, manifest}) 72} 73 74pub ( crate ) fn layout ( & self , fingerprint : & [ u8 ; 32 ]) ->Result < & Layout , SnapshotError > { 75self . manifest 76. layouts 77. iter () 78. find ( |layout| layout. fingerprint ==* fingerprint) 79. ok_or ( SnapshotError :: IncompatibleLayout ) 80} 81 82fn startup_data ( & self ) -> v8:: StartupData { 83match & self . bytes { 84Cow :: Borrowed ( bytes) =>{ 85let bytes: &' static [ u8 ] = bytes; 86Cow :: Borrowed ( & bytes[ self . blob ..]) 87} 88Cow :: 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 { 99BuiltIn , 100Snapshot ([ 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 { 109fn acquire ( heap : Heap ) ->Result < Self , SnapshotError > { 110let mut live =LIVE_HEAP . lock (). unwrap_or_else ( PoisonError :: into_inner); 111match & mut * live{ 112Some (( current, isolates)) if * current == heap =>* isolates +=1 , 113Some ( _) =>return Err ( SnapshotError :: ReadOnlyHeapInUse ), 114None =>* live =Some (( heap, 1 )), 115} 116Ok ( Self ( heap)) 117} 118} 119 120impl Drop for HeapLease { 121fn drop ( & mut self ) { 122let mut live =LIVE_HEAP . lock (). unwrap_or_else ( PoisonError :: into_inner); 123if let Some (( heap, isolates)) =& mut * live{ 124debug_assert! ( * heap ==self . 0 ); 125* isolates -=1 ; 126if * isolates ==0 { 127* live =None ; 128} 129} 130} 131} 132 133# [ derive ( Clone , Copy )] 134pub ( crate ) enum Source < ' a > { 135BuiltIn , 136Creator , 137Snapshot ( &' 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" { 143fn 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. 149let 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. 155unsafe { 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 { 161isolate : Option < v8:: OwnedIsolate >, 162// Released only once V8 has deleted the isolate: explicitly in Drop, or after 163// create_blob by field order. 164lease : Option < HeapLease >, 165creator : bool , 166} 167 168impl IsolateOwner { 169/// Rejects an incompatible read-only heap before V8 creates the isolate. 170pub ( crate ) fn new ( source : Source ) ->Result < Self , SnapshotError > { 171let lease =HeapLease :: acquire ( match source{ 172Source :: BuiltIn |Source :: Creator =>Heap :: BuiltIn , 173Source :: Snapshot ( snapshot) =>Heap :: Snapshot ( snapshot. manifest . heap ), 174}) ?; 175let references =external_references (). into (); 176let mut isolate =match source{ 177Source :: BuiltIn =>{ 178 v8:: Isolate :: new ( v8:: Isolate :: create_params (). external_references ( references)) 179} 180Source :: Creator => v8:: Isolate :: snapshot_creator ( Some ( references), None ), 181Source :: Snapshot ( snapshot) => v8:: Isolate :: new ( 182 v8:: Isolate :: create_params () 183. external_references ( references) 184. snapshot_blob ( snapshot. startup_data ()), 185), 186}; 187if !matches! ( source, Source :: Snapshot ( _)) { 188crate :: microtasks:: add_listener ( & mut isolate); 189} 190Ok ( Self { 191isolate : Some ( isolate), 192lease : Some ( lease), 193creator : matches! ( source, Source :: Creator ), 194}) 195} 196 197pub ( crate ) fn is_creator ( & self ) ->bool { 198self . creator 199} 200 201pub ( crate ) fn release_native_state ( & mut self ) { 202release_native_state ( self ); 203} 204 205fn create_blob ( mut self ) ->Option < v8:: StartupData > { 206assert! ( self . creator ); 207self . release_native_state (); 208self . isolate 209. take () 210. unwrap () 211. create_blob ( v8:: FunctionCodeHandling :: Keep ) 212} 213} 214 215impl std:: ops:: Deref for IsolateOwner { 216type Target = v8:: OwnedIsolate ; 217 218fn deref ( & self ) ->& Self :: Target { 219self . isolate . as_ref (). unwrap () 220} 221} 222 223impl std:: ops:: DerefMut for IsolateOwner { 224fn deref_mut ( & mut self ) ->& mut Self :: Target { 225self . isolate . as_mut (). unwrap () 226} 227} 228 229impl Drop for IsolateOwner { 230fn drop ( & mut self ) { 231// After create_blob, V8 has deleted the creator and the lease field releases normally. 232let Some ( isolate) =self . isolate . take () else { 233return ; 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. 237let lease =ManuallyDrop :: new ( self . lease . take ()); 238if !self . creator { 239drop ( isolate); 240drop ( ManuallyDrop :: into_inner ( lease)); 241return ; 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. 245let 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. 248let released = std:: panic:: catch_unwind ( std:: panic:: AssertUnwindSafe ( ||{ 249release_native_state ( & mut isolate) 250})); 251if 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. 255unsafe { isolate. exit () }; 256} 257let _ =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. 262if 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 ) { 272crate :: tasks:: clear ( isolate); 273 op:: clear ( isolate); 274crate :: microtasks:: clear ( isolate); 275crate :: reporting:: clear ( isolate); 276crate :: stack_trace:: clear ( isolate); 277crate :: 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 > { 286let order = extensions:: validate ( extensions) ?; 287let mut hash =Sha256 :: new (); 288field ( & mut hash, b"avril-core snapshot layout v1" ); 289 hash. update ( core_identity ()); 290let entries: Vec < _ > = order 291. iter () 292. filter_map ( |index| extensions[ * index]. entry_point ) 293. collect (); 294count ( & mut hash, entries. len ()); 295for entryin entries{ 296field ( & mut hash, entry. as_bytes ()); 297} 298let mut sources: Vec < _ > = extensions 299. iter () 300. flat_map ( |extension| extension. modules . iter (). copied ()) 301. collect (); 302 sources. sort_unstable_by_key ( |( name, _) |* name); 303count ( & mut hash, sources. len ()); 304for ( name, source) in sources{ 305field ( & mut hash, name. as_bytes ()); 306field ( & mut hash, source. as_bytes ()); 307} 308let mut ops: Vec < _ > = extensions 309. iter () 310. flat_map ( |extension| extension. ops . iter (). map ( |op| op. name )) 311. collect (); 312 ops. sort_unstable (); 313count ( & mut hash, ops. len ()); 314for namein ops{ 315field ( & mut hash, name. as_bytes ()); 316} 317Ok ( hash. finalize (). into ()) 318} 319 320fn core_identity () ->&' static [ u8 ; 32 ] { 321static IDENTITY : OnceLock <[ u8 ; 32 ]> =OnceLock :: new (); 322IDENTITY . get_or_init ( ||{ 323let mut hash =Sha256 :: new (); 324field ( & mut hash, b"avril-core" ); 325field ( & mut hash, include_str! ( "primordials.js" ). as_bytes ()); 326count ( & mut hash, extensions:: SOURCES . len ()); 327for ( name, source) in extensions:: SOURCES { 328field ( & mut hash, name. as_bytes ()); 329field ( & mut hash, source. as_bytes ()); 330} 331let modules = extensions:: native_modules (); 332count ( & mut hash, modules. len ()); 333for ( name, exports) in modules{ 334field ( & mut hash, name. as_bytes ()); 335count ( & mut hash, exports. len ()); 336for ( export, _) in exports{ 337field ( & mut hash, export. as_bytes ()); 338} 339} 340for ( group, references) in external_reference_groups () { 341field ( & mut hash, group. as_bytes ()); 342count ( & mut hash, references. len ()); 343} 344 hash. finalize (). into () 345}) 346} 347 348fn count ( hash : & mut Sha256 , count : usize ) { 349 hash. update (( countas u64 ). to_le_bytes ()); 350} 351 352fn field ( hash : & mut Sha256 , bytes : & [ u8 ]) { 353count ( hash, bytes. len ()); 354 hash. update ( bytes); 355} 356 357fn external_references () ->Vec < v8:: ExternalReference > { 358external_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 > { 374let fingerprints = layouts 375. iter () 376. map ( |options|fingerprint ( & options. extensions )) 377. collect ::< Result < Vec < _ >, _ >>() ?; 378for ( index, fingerprint) in fingerprints. iter (). enumerate () { 379if fingerprints[ ..index]. contains ( fingerprint) { 380return Err ( SnapshotError :: DuplicateLayout . into ()); 381} 382} 383let mut isolate =IsolateOwner :: new ( Source :: Creator ) ?; 384{ 385 v8:: scope!( let scope, & mut * isolate); 386let pristine = v8:: Context :: new ( scope, Default :: default ()); 387 scope. set_default_context ( pristine); 388} 389let mut captured =Vec :: with_capacity ( layouts. len ()); 390for ( options, fingerprint) in layouts. into_iter (). zip ( fingerprints) { 391let runtime =Runtime :: bootstrap ( isolate, options, crate :: ContextSource :: Fresh ) ?; 392let ( owner, context, state) =capture ( runtime) ?; 393 isolate = owner; 394 captured. push ( Layout { fingerprint, context, state}); 395} 396let blob = isolate. create_blob (). ok_or ( SnapshotError :: CreationFailed ) ?; 397let manifest = serde_json:: to_vec ( & Manifest { 398heap : Sha256 :: digest ( &* blob). into (), 399layouts : captured, 400}) 401. map_err ( SnapshotError :: Metadata ) ?; 402let length = u32:: try_from ( manifest. len ()) 403. map_err ( |_|SnapshotError :: InvalidArtifact ( "snapshot manifest is too large" )) ?; 404let 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. 411Ok ( 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 ( 417mut runtime : Runtime , 418) ->Result <( IsolateOwner , usize , extensions:: SnapshotState ), Error > { 419if runtime. has_pending_work () { 420return Err ( SnapshotError :: ActiveState ( "pending work" ). into ()); 421} 422if crate :: modules:: has_modules ( & runtime. isolate ) { 423return Err ( SnapshotError :: ActiveState ( "guest modules" ). into ()); 424} 425if crate :: reporting:: has_handlers ( & runtime. isolate ) { 426return Err ( SnapshotError :: ActiveState ( "event handlers" ). into ()); 427} 428if !runtime 429. isolate 430. get_slot ::< std:: rc:: Rc < std:: cell:: RefCell < crate :: WasmStreamingRegistry >>>() 431. unwrap () 432. borrow () 433. is_empty () 434{ 435return Err ( SnapshotError :: ActiveState ( "Wasm streaming" ). into ()); 436} 437let ( context, state) = runtime. with_context ( |scope|{ 438if let Err ( error) =remove_stack_trace_limit ( scope) ?{ 439return Some ( Err ( error)); 440} 441let state = extensions:: capture ( scope); 442let context = scope. get_current_context (); 443Some ( Ok (( scope. add_context ( context), state))) 444}) ??; 445let Runtime { context : handle, mut isolate, ..} = runtime; 446drop ( handle); 447 isolate. release_native_state (); 448Ok (( 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 <()> { 454let ( error, key) =stack_trace_limit ( scope) ?; 455let limit = v8:: Integer :: new ( scope, STACK_TRACE_LIMIT ); 456 error. create_data_property ( scope, key. into (), limit. into ()) ?; 457Some (()) 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 >> { 463let ( error, key) =stack_trace_limit ( scope) ?; 464let limit = error. get ( scope, key. into ()) ?; 465let unchanged = limit. is_int32 () && limit. int32_value ( scope) ? ==STACK_TRACE_LIMIT ; 466if !unchanged || !error. delete ( scope, key. into ()) ?{ 467return Some ( Err ( SnapshotError :: ActiveState ( "Error.stackTraceLimit" ))); 468} 469Some ( Ok (())) 470} 471 472/// V8's default `--stack-trace-limit`. 473const STACK_TRACE_LIMIT : i32 =10 ; 474 475fn stack_trace_limit < ' s >( 476scope : & mut v8:: PinScope < ' s , ' _ >, 477) ->Option <( v8:: Local < ' s , v8:: Object >, v8:: Local < ' s , v8:: String >)> { 478let global = scope. get_current_context (). global ( scope); 479let key = v8:: String :: new ( scope, "Error" ) ?; 480let error = v8:: Local ::< v8:: Object >:: try_from ( global. get ( scope, key. into ()) ?). ok () ?; 481Some (( error, v8:: String :: new ( scope, "stackTraceLimit" ) ?)) 482}