subsecond/lib.rs
1#![allow(clippy::needless_doctest_main)]
2//! # Subsecond: Hot-patching for Rust
3//!
4//! Subsecond is a library that enables hot-patching for Rust applications. This allows you to change
5//! the code of a running application without restarting it. This is useful for game engines, servers,
6//! and other long-running applications where the typical edit-compile-run cycle is too slow.
7//!
8//! Subsecond also implements a technique we call "ThinLinking" which makes compiling Rust code
9//! significantly faster in development mode, which can be used outside of hot-patching.
10//!
11//! # Usage
12//!
13//! Subsecond is designed to be as simple for both application developers and library authors.
14//!
15//! Simply call your existing functions with [`call`] and Subsecond will automatically detour
16//! that call to the latest version of the function.
17//!
18//! ```rust
19//! for x in 0..5 {
20//! subsecond::call(|| {
21//! println!("Hello, world! {}", x);
22//! });
23//! }
24//! ```
25//!
26//! To actually load patches into your application, a third-party tool that implements the Subsecond
27//! compiler and protocol is required. Subsecond is built and maintained by the Dioxus team, so we
28//! suggest using the dioxus CLI tool to use subsecond.
29//!
30//! To install the Dioxus CLI, we recommend using [`cargo binstall`](https://crates.io/crates/cargo-binstall):
31//!
32//! ```sh
33//! cargo binstall dioxus-cli
34//! ```
35//!
36//! The Dioxus CLI provides several tools for development. To run your application with Subsecond enabled,
37//! use `dx serve` - this takes the same arguments as `cargo run` but will automatically hot-reload your
38//! application when changes are detected.
39//!
40//! As of Dioxus 0.7, "--hotpatch" is required to use hotpatching while Subsecond is still experimental.
41//!
42//! ```sh
43//! dx serve --hotpatch
44//! ```
45//!
46//! ## How it works
47//!
48//! Subsecond works by detouring function calls through a jump table. This jump table contains the latest
49//! version of the program's function pointers, and when a function is called, Subsecond will look up
50//! the function in the jump table and call that instead.
51//!
52//! Unlike libraries like [detour](https://crates.io/crates/detour), Subsecond *does not* modify your
53//! process memory. Patching pointers is wildly unsafe and can lead to crashes and undefined behavior.
54//!
55//! Instead, an external tool compiles only the parts of your project that changed, links them together
56//! using the addresses of the functions in your running program, and then sends the new jump table to
57//! your application. Subsecond then applies the patch and continues running. Since Subsecond doesn't
58//! modify memory, the program must have a runtime integration to handle the patching.
59//!
60//! If the framework you're using doesn't integrate with subsecond, you can rely on the fact that calls
61//! to stale [`call`] instances will emit a safe panic that is automatically caught and retried
62//! by the next [`call`] instance up the callstack.
63//!
64//! Subsecond is only enabled when debug_assertions are enabled so you can safely ship your application
65//! with Subsecond enabled without worrying about the performance overhead.
66//!
67//! ## Workspace support
68//!
69//! Subsecond currently only patches the "tip" crate - ie the crate in which your `main.rs` is located.
70//! Changes to crates outside this crate will be ignored, which can be confusing. We plan to add full
71//! workspace support in the future, but for now be aware of this limitation. Crate setups that have
72//! a `main.rs` importing a `lib.rs` won't patch sensibly since the crate becomes a library for itself.
73//!
74//! This is due to limitations in rustc itself where the build-graph is non-deterministic and changes
75//! to functions that forward generics can cause a cascade of codegen changes.
76//!
77//! ## Globals, statics, and thread-locals
78//!
79//! Subsecond *does* support hot-reloading of globals, statics, and thread locals. However, there are several limitations:
80//!
81//! - You may add new globals at runtime, but their destructors will never be called.
82//! - Globals are tracked across patches, but renames are considered to be *new* globals.
83//! - Changes to static initializers will not be observed.
84//!
85//! Subsecond purposefully handles statics this way since many libraries like Dioxus and Tokio rely
86//! on persistent global runtimes.
87//!
88//! HUGE WARNING: Currently, thread-locals in the "tip" crate (the one being patched) will seemingly
89//! reset to their initial value on new patches. This is because we don't currently bind thread-locals
90//! in the patches to their original addresses in the main program. If you rely on thread-locals heavily
91//! in your tip crate, you should be aware of this. Sufficiently complex setups might crash or even
92//! segfault. We plan to fix this in the future, but for now, you should be aware of this limitation.
93//!
94//! ## Struct layout and alignment
95//!
96//! Subsecond currently does not support hot-reloading of structs. This is because the generated code
97//! assumes a particular layout and alignment of the struct. If layout or alignment change and new
98//! functions are called referencing an old version of the struct, the program will crash.
99//!
100//! To mitigate this, framework authors can integrate with Subsecond to either dispose of the old struct
101//! or to re-allocate the struct in a way that is compatible with the new layout. This is called "re-instancing."
102//!
103//! In practice, frameworks that implement subsecond patching properly will throw out the old state
104//! and thus you should never witness a segfault due to misalignment or size changes. Frameworks are
105//! encouraged to aggressively dispose of old state that might cause size and alignment changes.
106//!
107//! We'd like to lift this limitation in the future by providing utilities to re-instantiate structs,
108//! but for now it's up to the framework authors to handle this. For example, Dioxus apps simply throw
109//! out the old state and rebuild it from scratch.
110//!
111//! ## Pointer versioning
112//!
113//! Currently, Subsecond does not "version" function pointers. We have plans to provide this metadata
114//! so framework authors can safely memoize changes without much runtime overhead. Frameworks like
115//! Dioxus and Bevy circumvent this issue by using the TypeID of structs passed to hot functions as
116//! well as the `ptr_address` method on [`HotFn`] to determine if the function pointer has changed.
117//!
118//! Currently, the `ptr_address` method will always return the most up-to-date version of the function
119//! even if the function contents itself did not change. In essence, this is equivalent to a version
120//! of the function where every function is considered "new." This means that framework authors who
121//! integrate re-instancing in their apps might dispose of old state too aggressively. For now, this
122//! is the safer and more practical approach.
123//!
124//! ## Nesting Calls
125//!
126//! Subsecond calls are designed to be nested. This provides clean integration points to know exactly
127//! where a hooked function is called.
128//!
129//! The highest level call is `fn main()` though by default this is not hooked since initialization code
130//! tends to be side-effectual and modify global state. Instead, we recommend wrapping the hot-patch
131//! points manually with [`call`].
132//!
133//! ```rust
134//! fn main() {
135//! // Changes to the `for` loop will cause an unwind to this call.
136//! subsecond::call(|| {
137//! for x in 0..5 {
138//! // Changes to the `println!` will be isolated to this call.
139//! subsecond::call(|| {
140//! println!("Hello, world! {}", x);
141//! });
142//! }
143//! });
144//! }
145//! ```
146//!
147//! The goal here is to provide granular control over where patches are applied to limit loss of state
148//! when new code is loaded.
149//!
150//! ## Applying patches
151//!
152//! When running under the Dioxus CLI, the `dx serve` command will automatically apply patches when
153//! changes are detected. Patches are delivered over the [Dioxus Devtools](https://crates.io/crates/dioxus-devtools)
154//! websocket protocol and received by corresponding websocket.
155//!
156//! If you're using Subsecond in your own application that doesn't have a runtime integration, you can
157//! build an integration using the [`apply_patch`] function. This function takes a `JumpTable` which
158//! the dioxus-cli crate can generate.
159//!
160//! To add support for the Dioxus Devtools protocol to your app, you can use the [dioxus-devtools](https://crates.io/crates/dioxus-devtools)
161//! crate which provides a `connect` method that will automatically apply patches to your application.
162//!
163//! Unfortunately, one design quirk of Subsecond is that running apps need to communicate the address
164//! of `main` to the patcher. This is due to a security technique called [ASLR](https://en.wikipedia.org/wiki/Address_space_layout_randomization)
165//! which randomizes the address of functions in memory. See the subsecond-harness and subsecond-cli
166//! for more details on how to implement the protocol.
167//!
168//! ## ThinLink
169//!
170//! ThinLink is a program linker for Rust that is designed to be used with Subsecond. It implements
171//! the powerful patching system that Subsecond uses to hot-reload Rust applications.
172//!
173//! ThinLink is simply a wrapper around your existing linker but with extra features:
174//!
175//! - Automatic dynamic linking to dependencies
176//! - Generation of Subsecond jump tables
177//! - Diffing of object files for function invalidation
178//!
179//! Because ThinLink performs very to little actual linking, it drastically speeds up traditional Rust
180//! development. With a development-optimized profile, ThinLink can shrink an incremental build to less than 500ms.
181//!
182//! ThinLink is automatically integrated into the Dioxus CLI though it's currently not available as
183//! a standalone tool.
184//!
185//! ## Limitations
186//!
187//! Subsecond is a powerful tool but it has several limitations. We talk about them above, but here's
188//! a quick summary:
189//!
190//! - Struct hot reloading requires instancing or unwinding
191//! - Statics are tracked but not destructed
192//!
193//! ## Platform support
194//!
195//! Subsecond works across all major platforms:
196//!
197//! - Android (arm64-v8a, armeabi-v7a)
198//! - iOS (arm64)
199//! - Linux (x86_64, aarch64)
200//! - macOS (x86_64, aarch64)
201//! - Windows (x86_64, arm64)
202//! - WebAssembly (wasm32)
203//!
204//! If you have a new platform you'd like to see supported, please open an issue on the Subsecond repository.
205//! We are keen to add support for new platforms like wasm64, riscv64, and more.
206//!
207//! Note that iOS device is currently not supported due to code-signing requirements. We hope to fix
208//! this in the future, but for now you can use the simulator to test your app.
209//!
210//! ## Adding the Subsecond badge to your project
211//!
212//! If you're a framework author and want your users to know that your library supports Subsecond, you
213//! can add the Subsecond badge to your README! Users will know that your library is hot-reloadable and
214//! can be used with Subsecond.
215//!
216//! [](https://crates.io/crates/subsecond)
217//!
218//! ```markdown
219//! [](https://crates.io/crates/subsecond)
220//! ```
221//!
222//! ## License
223//!
224//! Subsecond and ThinLink are licensed under the MIT license. See the LICENSE file for more information.
225//!
226//! ## Supporting this work
227//!
228//! Subsecond is a project by the Dioxus team. If you'd like to support our work, please consider
229//! [sponsoring us on GitHub](https://github.com/sponsors/DioxusLabs) or eventually deploying your
230//! apps with Dioxus Deploy (currently under construction).
231
232pub use subsecond_types::JumpTable;
233
234use std::{
235 backtrace,
236 mem::transmute,
237 panic::AssertUnwindSafe,
238 sync::{Arc, Mutex, atomic::AtomicPtr},
239};
240
241/// Call a given function with hot-reloading enabled. If the function's code changes, `call` will use
242/// the new version of the function. If code *above* the function changes, this will emit a panic
243/// that forces an unwind to the next [`call`] instance.
244///
245/// WASM/rust does not support unwinding, so [`call`] will not track dependency graph changes.
246/// If you are building a framework for use on WASM, you will need to use `Subsecond::HotFn` directly.
247///
248/// However, if you wrap your calling code in a future, you *can* simply drop the future which will
249/// cause `drop` to execute and get something similar to unwinding. Not great if refcells are open.
250pub fn call<O>(mut f: impl FnMut() -> O) -> O {
251 // Only run in debug mode - the rest of this function will dissolve away
252 if !cfg!(debug_assertions) {
253 return f();
254 }
255
256 let mut hotfn = HotFn::current(f);
257 loop {
258 let res = std::panic::catch_unwind(AssertUnwindSafe(|| hotfn.call(())));
259
260 // If the call succeeds just return the result, otherwise we try to handle the panic if its our own.
261 let err = match res {
262 Ok(res) => return res,
263 Err(err) => err,
264 };
265
266 // If this is our panic then let's handle it, otherwise we just resume unwinding
267 let Some(_hot_payload) = err.downcast_ref::<HotFnPanic>() else {
268 std::panic::resume_unwind(err);
269 };
270 }
271}
272
273// We use an AtomicPtr with a leaked JumpTable and Relaxed ordering to give us a global jump table
274// with very little overhead. Reading this amounts of a Relaxed atomic load which basically
275// is no overhead. We might want to look into using a thread_local with a stop-the-world approach
276// just in case multiple threads try to call the jump table before synchronization with the runtime.
277// For Dioxus purposes, this is not a big deal, but for libraries like bevy which heavily rely on
278// multithreading, it might become an issue.
279static APP_JUMP_TABLE: AtomicPtr<JumpTable> = AtomicPtr::new(std::ptr::null_mut());
280static HOTRELOAD_HANDLERS: Mutex<Vec<Arc<dyn Fn() + Send + Sync>>> = Mutex::new(Vec::new());
281
282/// Register a function that will be called whenever a patch is applied.
283///
284/// This handler will be run immediately after the patch library is loaded into the process and the
285/// JumpTable has been set.
286pub fn register_handler(handler: Arc<dyn Fn() + Send + Sync + 'static>) {
287 HOTRELOAD_HANDLERS.lock().unwrap().push(handler);
288}
289
290/// Get the current jump table, if it exists.
291///
292/// This will return `None` if no jump table has been set yet.
293///
294/// # Safety
295///
296/// The `JumpTable` returned here is a pointer into a leaked box. While technically this reference is
297/// valid, we might change the implementation to invalidate the pointer between hotpatches.
298///
299/// You should only use this lifetime in temporary contexts - not *across* hotpatches!
300pub unsafe fn get_jump_table() -> Option<&'static JumpTable> {
301 let ptr = APP_JUMP_TABLE.load(std::sync::atomic::Ordering::Relaxed);
302 if ptr.is_null() {
303 return None;
304 }
305
306 Some(unsafe { &*ptr })
307}
308unsafe fn commit_patch(table: JumpTable) {
309 APP_JUMP_TABLE.store(
310 Box::into_raw(Box::new(table)),
311 std::sync::atomic::Ordering::Relaxed,
312 );
313 HOTRELOAD_HANDLERS
314 .lock()
315 .unwrap()
316 .clone()
317 .iter()
318 .for_each(|handler| {
319 handler();
320 });
321}
322
323/// A panic issued by the [`call`] function if the caller would be stale if called. This causes
324/// an unwind to the next [`call`] instance that can properly handle the panic and retry the call.
325///
326/// This technique allows Subsecond to provide hot-reloading of codebases that don't have a runtime integration.
327#[derive(Debug)]
328pub struct HotFnPanic {
329 _backtrace: backtrace::Backtrace,
330}
331
332/// A pointer to a hot patched function
333#[non_exhaustive]
334#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
335pub struct HotFnPtr(pub u64);
336
337impl HotFnPtr {
338 /// Create a new [`HotFnPtr`].
339 ///
340 /// The safe way to get one is through [`HotFn::ptr_address`].
341 ///
342 /// # Safety
343 ///
344 /// The underlying `u64` must point to a valid function.
345 pub unsafe fn new(index: u64) -> Self {
346 Self(index)
347 }
348}
349
350/// A hot-reloadable function.
351///
352/// To call this function, use the [`HotFn::call`] method. This will automatically use the latest
353/// version of the function from the JumpTable.
354pub struct HotFn<A, M, F>
355where
356 F: HotFunction<A, M>,
357{
358 inner: F,
359 _marker: std::marker::PhantomData<(A, M)>,
360}
361
362impl<A, M, F: HotFunction<A, M>> HotFn<A, M, F> {
363 /// Create a new [`HotFn`] instance with the current function.
364 ///
365 /// Whenever you call [`HotFn::call`], it will use the current function from the [`JumpTable`].
366 pub const fn current(f: F) -> HotFn<A, M, F> {
367 HotFn {
368 inner: f,
369 _marker: std::marker::PhantomData,
370 }
371 }
372
373 /// Call the function with the given arguments.
374 ///
375 /// This will unwrap the [`HotFnPanic`] panic, propagating up to the next [`HotFn::call`].
376 ///
377 /// If you want to handle the panic yourself, use [`HotFn::try_call`].
378 pub fn call(&mut self, args: A) -> F::Return {
379 self.try_call(args).unwrap()
380 }
381
382 /// Get the address of the function in memory which might be different than the original.
383 ///
384 /// This is useful for implementing a memoization strategy to safely preserve state across
385 /// hot-patches. If the ptr_address of a function did not change between patches, then the
386 /// state that exists "above" the function is still valid.
387 ///
388 /// Note that Subsecond does not track this state over time, so it's up to the runtime integration
389 /// to track this state and diff it.
390 pub fn ptr_address(&self) -> HotFnPtr {
391 if size_of::<F>() == size_of::<fn() -> ()>() {
392 let ptr: usize = unsafe { std::mem::transmute_copy(&self.inner) };
393 return HotFnPtr(ptr as u64);
394 }
395
396 let known_fn_ptr = <F as HotFunction<A, M>>::call_it as *const () as usize;
397 if let Some(jump_table) = unsafe { get_jump_table() } {
398 if let Some(ptr) = jump_table.map.get(&(known_fn_ptr as u64)).cloned() {
399 return HotFnPtr(ptr);
400 }
401 }
402
403 HotFnPtr(known_fn_ptr as u64)
404 }
405
406 /// Attempt to call the function with the given arguments.
407 ///
408 /// If this function is stale and can't be updated in place (ie, changes occurred above this call),
409 /// then this function will emit an [`HotFnPanic`] which can be unwrapped and handled by next [`call`]
410 /// instance.
411 pub fn try_call(&mut self, args: A) -> Result<F::Return, HotFnPanic> {
412 if !cfg!(debug_assertions) {
413 return Ok(self.inner.call_it(args));
414 }
415
416 unsafe {
417 // Try to handle known function pointers. This is *really really* unsafe, but due to how
418 // rust trait objects work, it's impossible to make an arbitrary usize-sized type implement Fn()
419 // since that would require a vtable pointer, pushing out the bounds of the pointer size.
420 if size_of::<F>() == size_of::<fn() -> ()>() {
421 return Ok(self.inner.call_as_ptr(args));
422 }
423
424 // Handle trait objects. This will occur for sizes other than usize. Normal rust functions
425 // become ZST's and thus their <T as SomeFn>::call becomes a function pointer to the function.
426 //
427 // For non-zst (trait object) types, then there might be an issue. The real call function
428 // will likely end up in the vtable and will never be hot-reloaded since signature takes self.
429 if let Some(jump_table) = get_jump_table() {
430 let known_fn_ptr = <F as HotFunction<A, M>>::call_it as *const () as u64;
431 if let Some(ptr) = jump_table.map.get(&known_fn_ptr).cloned() {
432 // The type sig of the cast should match the call_it function
433 // Technically function pointers need to be aligned, but that alignment is 1 so we're good
434 let call_it = transmute::<*const (), fn(&F, A) -> F::Return>(ptr as _);
435 return Ok(call_it(&self.inner, args));
436 }
437 }
438
439 Ok(self.inner.call_it(args))
440 }
441 }
442
443 /// Attempt to call the function with the given arguments, using the given [`HotFnPtr`].
444 ///
445 /// You can get a [`HotFnPtr`] from [`Self::ptr_address`].
446 ///
447 /// If this function is stale and can't be updated in place (ie, changes occurred above this call),
448 /// then this function will emit an [`HotFnPanic`] which can be unwrapped and handled by next [`call`]
449 /// instance.
450 ///
451 /// # Safety
452 ///
453 /// The [`HotFnPtr`] must be to a function whose arguments layouts haven't changed.
454 pub unsafe fn try_call_with_ptr(
455 &mut self,
456 ptr: HotFnPtr,
457 args: A,
458 ) -> Result<F::Return, HotFnPanic> {
459 if !cfg!(debug_assertions) {
460 return Ok(self.inner.call_it(args));
461 }
462
463 unsafe {
464 // Try to handle known function pointers. This is *really really* unsafe, but due to how
465 // rust trait objects work, it's impossible to make an arbitrary usize-sized type implement Fn()
466 // since that would require a vtable pointer, pushing out the bounds of the pointer size.
467 if size_of::<F>() == size_of::<fn() -> ()>() {
468 return Ok(self.inner.call_as_ptr(args));
469 }
470
471 // Handle trait objects. This will occur for sizes other than usize. Normal rust functions
472 // become ZST's and thus their <T as SomeFn>::call becomes a function pointer to the function.
473 //
474 // For non-zst (trait object) types, then there might be an issue. The real call function
475 // will likely end up in the vtable and will never be hot-reloaded since signature takes self.
476 // The type sig of the cast should match the call_it function
477 // Technically function pointers need to be aligned, but that alignment is 1 so we're good
478 let call_it = transmute::<*const (), fn(&F, A) -> F::Return>(ptr.0 as _);
479 Ok(call_it(&self.inner, args))
480 }
481 }
482}
483
484/// Apply the patch using a given jump table.
485///
486/// # Safety
487///
488/// This function is unsafe because it detours existing functions in memory. This is *wildly* unsafe,
489/// especially if the JumpTable is malformed. Only run this if you know what you're doing.
490///
491/// If the pointers are incorrect, function type signatures will be incorrect and the program will crash,
492/// sometimes in a way that requires a restart of your entire computer. Be careful.
493///
494/// # Warning
495///
496/// This function will load the library and thus allocates. In cannot be used when the program is
497/// stopped (ie in a signal handler).
498///
499/// # Returns
500///
501/// On success, returns the list of host (pre-patch) function pointers that were
502/// rewritten by this patch — already shifted to the live ASLR slide, so they can
503/// be compared against `fn_name as *const ()` values held in Whisker's runtime
504/// `component_owners` map. The Strategy C hot-reload path (Whisker A6+) uses
505/// this list to find live component owners whose body is now stale and re-mount
506/// them.
507pub unsafe fn apply_patch(mut table: JumpTable) -> Result<Vec<*const ()>, PatchError> {
508 unsafe {
509 // On non-wasm platforms we can just use libloading and the known aslr offsets to load the library
510 #[cfg(any(unix, windows))]
511 {
512 // on android we try to circumvent permissions issues by copying the library to a memmap and then libloading that
513 #[cfg(target_os = "android")]
514 let lib = Box::leak(Box::new(android_memmap_dlopen(&table.lib)?));
515
516 #[cfg(not(target_os = "android"))]
517 let lib = Box::leak(Box::new({
518 match libloading::Library::new(&table.lib) {
519 Ok(lib) => lib,
520 Err(err) => return Err(PatchError::Dlopen(err.to_string())),
521 }
522 }));
523
524 // Whisker fork: anchor on `whisker_aslr_anchor` rather than `main`.
525 //
526 // Upstream uses `main` as the cross-library ASLR anchor. That works
527 // for Dioxus, where `main` is the PIE process entry and unique in
528 // the linker namespace. Whisker ships the user crate as a `dylib`
529 // loaded via Android `System.loadLibrary`, where the namespace can
530 // contain several `main` symbols (`app_process64`'s, stale patch
531 // memfds', etc.) — `dlsym(RTLD_DEFAULT, "main")` then returns the
532 // wrong one and the computed ASLR slide is garbage.
533 //
534 // `whisker_aslr_anchor` is synthesised by `#[whisker::main]` and
535 // is unique to the user's `.so`, so the dlsym lookup is
536 // unambiguous regardless of namespace order.
537 let old_offset = aslr_reference() - table.aslr_reference as usize;
538
539 let new_offset = {
540 // Leak the library. dlopen is basically a no-op on many platforms and if we even try to drop it,
541 // some code might be called (ie drop) that results in really bad crashes (restart your computer...)
542 //
543 // The CLI (whisker-dev-server) coordinates by ensuring
544 // `whisker_aslr_anchor` is in the export list of every patch.
545 lib.get::<*const ()>(b"whisker_aslr_anchor")
546 .ok()
547 .unwrap()
548 .try_as_raw_ptr()
549 .unwrap()
550 .wrapping_byte_sub(table.new_base_address as usize) as usize
551 };
552
553 // Modify the jump table to be relative to the base address of the loaded library
554 table.map = table
555 .map
556 .iter()
557 .map(|(k, v)| {
558 (
559 (*k as usize + old_offset) as u64,
560 (*v as usize + new_offset) as u64,
561 )
562 })
563 .collect();
564
565 // Snapshot the host-side (post-slide) function pointers BEFORE
566 // committing the patch — `commit_patch` consumes `table`. Whisker's
567 // remount logic compares these against `fn as *const ()` values
568 // stashed in its component-owner registry.
569 let patched: Vec<*const ()> = table.map.keys().map(|k| *k as *const ()).collect();
570
571 commit_patch(table);
572 return Ok(patched);
573 };
574
575 // The non-wasm path above returns inside the block; for wasm we drop
576 // through to the wasm-specific code below which never returns Ok with
577 // a list (patches are async-applied). Surface an empty list in that
578 // case rather than restructuring the existing wasm flow.
579 #[allow(unreachable_code)]
580 {
581 let _unused: Vec<*const ()> = Vec::new();
582 }
583
584 // On wasm, we need to download the module, compile it, and then run it.
585 #[cfg(target_arch = "wasm32")]
586 wasm_bindgen_futures::spawn_local(async move {
587 use js_sys::{
588 ArrayBuffer, Object, Reflect,
589 WebAssembly::{self, Instance, Memory, Module, Table},
590 };
591 use wasm_bindgen::JsValue;
592 use wasm_bindgen::UnwrapThrowExt;
593 use wasm_bindgen::prelude::*;
594 use wasm_bindgen_futures::JsFuture;
595
596 let funcs: Table = wasm_bindgen::function_table().unchecked_into();
597 let memory: Memory = wasm_bindgen::memory().unchecked_into();
598 let exports: Object = wasm_bindgen::exports().unchecked_into();
599
600 let path = table.lib.to_str().unwrap();
601 if !path.ends_with(".wasm") {
602 return;
603 }
604
605 // Fetch + decode the patch wasm. Both awaits are pure I/O — they
606 // touch no shared state, so the future is safe to drop here.
607 let response: web_sys::Response =
608 JsFuture::from(web_sys::window().unwrap_throw().fetch_with_str(&path))
609 .await
610 .unwrap()
611 .unchecked_into();
612 if !response.ok() {
613 panic!(
614 "Failed to patch wasm module at {} - response failed with: {}",
615 path,
616 response.status_text()
617 );
618 }
619 let dl_bytes: ArrayBuffer = JsFuture::from(response.array_buffer().unwrap())
620 .await
621 .unwrap()
622 .unchecked_into();
623
624 // Pre-compile. This is the slow part — V8 hands it to a worker —
625 // and yields the longest. The result is a Module with no side
626 // effects on host JS state, so cancelling here is also safe.
627 let module: Module = JsFuture::from(WebAssembly::compile(dl_bytes.unchecked_ref()))
628 .await
629 .unwrap()
630 .unchecked_into();
631
632 // ── HOST-STATE-MUTATING SECTION ───────────────────────────────
633 //
634 // Below we grow shared linear memory and the indirect function
635 // table, then async-instantiate the patch into them and commit
636 // the new jump table. There IS one `.await` for the instantiate
637 // (we can't avoid it: Chrome disallows synchronous
638 // `new WebAssembly.Instance` on the main thread for modules
639 // larger than 8MB, and patches routinely cross that), but it's
640 // safe — the original race we fixed wasn't about yielding here:
641 //
642 // * `memory.grow` and `funcs.grow` each return their PRIOR
643 // length atomically. Concurrent `apply_patch` tasks therefore
644 // each get a unique, non-overlapping `memory_base` /
645 // `table_base`, so two patches can't land on the same region.
646 // * Host code can't observe the half-instantiated patch: the
647 // new memory pages are zero, the new table slots are null,
648 // and the jump table isn't committed until the very end of
649 // this block, so nothing redirects through the new slots.
650 // * The original bug — using `memory.buffer().byteLength()`
651 // captured before the awaits, which returned 0 if the buffer
652 // had been detached by a concurrent grow — is gone because
653 // we derive `memory_base` from `memory.grow()`'s return
654 // value instead.
655 // * Cancellation between grow and `commit_patch` leaks memory
656 // pages and table slots, but doesn't corrupt anything.
657 const PAGE_SIZE: u32 = 64 * 1024;
658 let patch_pages = (dl_bytes.byte_length() as f64 / PAGE_SIZE as f64).ceil() as u32 + 1;
659
660 // Use grow's return value (the prior page count) to derive
661 // memory_base. Atomic w.r.t. concurrent grows, unlike reading
662 // memory.buffer().byteLength().
663 let prev_pages = memory.grow(patch_pages);
664 let memory_base = (prev_pages + 1) * PAGE_SIZE;
665
666 // grow returns the prior table length, which is __table_base.
667 let table_base = funcs.grow(table.ifunc_count as u32).unwrap();
668
669 // Rebase the jump table entries onto the patch's table slot range.
670 for v in table.map.values_mut() {
671 *v += table_base as u64;
672 }
673
674 // Build the env import object: copy every host export through and
675 // add __memory_base / __table_base globals so the patch's PIC
676 // code resolves correctly.
677 let env = Object::new();
678 for key in Object::keys(&exports) {
679 Reflect::set(&env, &key, &Reflect::get(&exports, &key).unwrap()).unwrap();
680 }
681 for (name, value) in [("__table_base", table_base), ("__memory_base", memory_base)] {
682 let descriptor = Object::new();
683 Reflect::set(&descriptor, &"value".into(), &"i32".into()).unwrap();
684 Reflect::set(&descriptor, &"mutable".into(), &false.into()).unwrap();
685 let global = WebAssembly::Global::new(&descriptor, &value.into()).unwrap();
686 Reflect::set(&env, &name.into(), &global.into()).unwrap();
687 }
688 let imports = Object::new();
689 Reflect::set(&imports, &"env".into(), &env).unwrap();
690
691 // Async instantiation of the precompiled module. We use the
692 // (Module, imports) form of `WebAssembly.instantiate`, which
693 // resolves directly to an `Instance` (not `{module, instance}`).
694 // This is the no-size-limit path; the synchronous
695 // `new WebAssembly.Instance` constructor is capped at 8MB on
696 // Chrome's main thread.
697 let instance: Instance =
698 JsFuture::from(WebAssembly::instantiate_module(&module, &imports))
699 .await
700 .unwrap()
701 .unchecked_into();
702 let inst_exports: Object = instance.exports();
703
704 // Run the patch's relocation thunks and constructors. Order
705 // matters: data relocs first (write memory_base- and table_base-
706 // relative pointers into the patch's data segment), then global
707 // relocs (adjust GOT.func.internal globals by __table_base —
708 // wasm-ld synthesizes those as element-segment-relative offsets),
709 // then ctors. `dyn_into` instead of `unchecked_into` so missing
710 // exports just no-op rather than throwing.
711 for func_name in [
712 "__wasm_apply_data_relocs",
713 "__wasm_apply_global_relocs",
714 "__wasm_call_ctors",
715 ] {
716 if let Ok(val) = Reflect::get(&inst_exports, &func_name.into()) {
717 if let Ok(func) = val.dyn_into::<js_sys::Function>() {
718 _ = func.call0(&JsValue::undefined());
719 }
720 }
721 }
722
723 unsafe { commit_patch(table) };
724 });
725
726 // wasm path applies the patch asynchronously inside the spawned
727 // future. We have no synchronous patched-pointer list to return.
728 Ok(Vec::new())
729 }
730}
731
732#[derive(Debug, PartialEq, thiserror::Error)]
733pub enum PatchError {
734 /// The patch failed to apply.
735 ///
736 /// This returns a string instead of the Dlopen error type so we don't need to bring the libloading
737 /// dependency into the public API.
738 #[error("Failed to load library: {0}")]
739 Dlopen(String),
740
741 /// The patch failed to apply on Android, most likely due to a permissions issue.
742 #[error("Failed to load library on Android: {0}")]
743 AndroidMemfd(String),
744}
745
746/// This function returns the address of the main function in the current executable. This is used as
747/// an anchor to reference the current executable's base address.
748///
749/// The point here being that we have a stable address both at runtime and compile time, making it
750/// possible to calculate the ASLR offset from within the process to correct the jump table.
751///
752/// It should only be called from the main executable *first* and not from a shared library since it
753/// self-initializes.
754#[doc(hidden)]
755pub fn aslr_reference() -> usize {
756 #[cfg(target_family = "wasm")]
757 return 0;
758
759 #[cfg(not(target_family = "wasm"))]
760 unsafe {
761 use std::ffi::c_void;
762 use std::sync::atomic::{AtomicPtr, Ordering};
763
764 // Cache the resolved anchor address. Edition 2024 makes a
765 // reference to a `static mut` a hard error (`static_mut_refs`);
766 // an `AtomicPtr` is the clean equivalent and keeps the
767 // self-initializing-on-first-call behavior intact.
768 static MAIN_PTR: AtomicPtr<c_void> = AtomicPtr::new(std::ptr::null_mut());
769
770 let mut ptr = MAIN_PTR.load(Ordering::Relaxed);
771 if ptr.is_null() {
772 // Whisker fork: anchor on `whisker_aslr_anchor` rather than
773 // `main` so the dlsym/GetProcAddress lookup cannot collide
774 // with system binaries' `main` (e.g. Android's
775 // `app_process64`). See the matching comment in
776 // `apply_patch` above. The symbol is emitted by
777 // `#[whisker::main]` and is unique to the user's `.so`.
778 #[cfg(unix)]
779 {
780 ptr = libc::dlsym(libc::RTLD_DEFAULT, c"whisker_aslr_anchor".as_ptr() as _);
781 }
782
783 #[cfg(windows)]
784 {
785 unsafe extern "system" {
786 fn GetModuleHandleA(lpModuleName: *const i8) -> *mut std::ffi::c_void;
787 fn GetProcAddress(
788 hModule: *mut std::ffi::c_void,
789 lpProcName: *const i8,
790 ) -> *mut std::ffi::c_void;
791 }
792
793 ptr = GetProcAddress(
794 GetModuleHandleA(std::ptr::null()),
795 c"whisker_aslr_anchor".as_ptr() as _,
796 ) as _;
797 }
798
799 MAIN_PTR.store(ptr, Ordering::Relaxed);
800 }
801
802 ptr as usize
803 }
804}
805
806/// On Android, we can't dlopen libraries that aren't placed inside /data/data/<package_name>/lib/
807///
808/// If the device isn't rooted, then we can't push the library there.
809/// This is a workaround to copy the library to a memfd and then dlopen it.
810///
811/// I haven't tested it on device yet, so if if it doesn't work, then we can simply revert to using
812/// "adb root" and then pushing the library to the /data/data folder instead of the tmp folder.
813///
814/// Android provides us a flag when calling dlopen to use a file descriptor instead of a path, presumably
815/// because they want to support this.
816/// - https://developer.android.com/ndk/reference/group/libdl
817/// - https://developer.android.com/ndk/reference/structandroid/dlextinfo
818#[cfg(target_os = "android")]
819unsafe fn android_memmap_dlopen(file: &std::path::Path) -> Result<libloading::Library, PatchError> {
820 use std::ffi::{CStr, CString, c_void};
821 use std::os::fd::{AsRawFd, BorrowedFd};
822 use std::ptr;
823
824 #[repr(C)]
825 struct ExtInfo {
826 flags: u64,
827 reserved_addr: *const c_void,
828 reserved_size: libc::size_t,
829 relro_fd: libc::c_int,
830 library_fd: libc::c_int,
831 library_fd_offset: libc::off64_t,
832 library_namespace: *const c_void,
833 }
834
835 unsafe extern "C" {
836 fn android_dlopen_ext(
837 filename: *const libc::c_char,
838 flags: libc::c_int,
839 ext_info: *const ExtInfo,
840 ) -> *const c_void;
841 }
842
843 use memmap2::MmapAsRawDesc;
844 use std::os::unix::prelude::{FromRawFd, IntoRawFd};
845
846 let contents = std::fs::read(file)
847 .map_err(|e| PatchError::AndroidMemfd(format!("Failed to read file: {}", e)))?;
848 let mut mfd = memfd::MemfdOptions::default()
849 .create("subsecond-patch")
850 .map_err(|e| PatchError::AndroidMemfd(format!("Failed to create memfd: {}", e)))?;
851 mfd.as_file()
852 .set_len(contents.len() as u64)
853 .map_err(|e| PatchError::AndroidMemfd(format!("Failed to set memfd length: {}", e)))?;
854
855 let raw_fd = mfd.into_raw_fd();
856
857 let mut map = memmap2::MmapMut::map_mut(raw_fd)
858 .map_err(|e| PatchError::AndroidMemfd(format!("Failed to map memfd: {}", e)))?;
859 map.copy_from_slice(&contents);
860 let map = map
861 .make_exec()
862 .map_err(|e| PatchError::AndroidMemfd(format!("Failed to make memfd executable: {}", e)))?;
863
864 let filename = c"/subsecond-patch";
865
866 let info = ExtInfo {
867 flags: 0x10, // ANDROID_DLEXT_USE_LIBRARY_FD
868 reserved_addr: ptr::null(),
869 reserved_size: 0,
870 relro_fd: 0,
871 library_fd: raw_fd,
872 library_fd_offset: 0,
873 library_namespace: ptr::null(),
874 };
875
876 let flags = libloading::os::unix::RTLD_LAZY | libloading::os::unix::RTLD_LOCAL;
877
878 let handle = libloading::os::unix::with_dlerror(
879 || {
880 let ptr = android_dlopen_ext(filename.as_ptr() as _, flags, &info);
881 if ptr.is_null() {
882 return None;
883 } else {
884 return Some(ptr);
885 }
886 },
887 |err| err.to_str().unwrap_or_default().to_string(),
888 )
889 .map_err(|e| {
890 PatchError::AndroidMemfd(format!(
891 "android_dlopen_ext failed: {}",
892 e.unwrap_or_default()
893 ))
894 })?;
895
896 let lib = unsafe { libloading::os::unix::Library::from_raw(handle as *mut c_void) };
897 let lib: libloading::Library = lib.into();
898 Ok(lib)
899}
900
901/// A trait that enables types to be hot-patched.
902///
903/// This trait is only implemented for FnMut types which naturally includes function pointers and
904/// closures that can be re-ran. FnOnce closures are currently not supported since the hot-patching
905/// system we use implies that the function can be called multiple times.
906pub trait HotFunction<Args, Marker> {
907 /// The return type of the function.
908 type Return;
909
910 /// The real function type. This is meant to be a function pointer.
911 /// When we call `call_as_ptr`, we will transmute the function to this type and call it.
912 type Real;
913
914 /// Call the HotFunction with the given arguments.
915 ///
916 /// # Why
917 ///
918 /// "rust-call" isn't stable, so we wrap the underlying call with our own, giving it a stable vtable entry.
919 /// This is more important than it seems since this function becomes "real" and can be hot-patched.
920 fn call_it(&mut self, args: Args) -> Self::Return;
921
922 /// Call the HotFunction as if it were a function pointer.
923 ///
924 /// # Safety
925 ///
926 /// This is only safe if the underlying type is a function (function pointer or virtual/fat pointer).
927 /// Using this will use the JumpTable to find the patched function and call it.
928 unsafe fn call_as_ptr(&mut self, _args: Args) -> Self::Return;
929}
930
931macro_rules! impl_hot_function {
932 (
933 $(
934 ($marker:ident, $($arg:ident),*)
935 ),*
936 ) => {
937 $(
938 /// A marker type for the function.
939 /// This is hidden with the intention to seal this trait.
940 #[doc(hidden)]
941 pub struct $marker;
942
943 impl<T, $($arg,)* R> HotFunction<($($arg,)*), $marker> for T
944 where
945 T: FnMut($($arg),*) -> R,
946 {
947 type Return = R;
948 type Real = fn($($arg),*) -> R;
949
950 fn call_it(&mut self, args: ($($arg,)*)) -> Self::Return {
951 #[allow(non_snake_case)]
952 let ( $($arg,)* ) = args;
953 self($($arg),*)
954 }
955
956 unsafe fn call_as_ptr(&mut self, args: ($($arg,)*)) -> Self::Return {
957 unsafe {
958 if let Some(jump_table) = get_jump_table() {
959 let real = std::mem::transmute_copy::<Self, Self::Real>(&self) as *const ();
960
961 // Android implements MTE / pointer tagging and we need to preserve the tag.
962 // If we leave the tag, then indexing our jump table will fail and patching won't work (or crash!)
963 // This is only implemented on 64-bit platforms since pointer tagging is not available on 32-bit platforms
964 // In dev, Dioxus disables MTE to work around this issue, but we still handle it anyways.
965 #[cfg(all(target_pointer_width = "64", target_os = "android"))] let nibble = real as u64 & 0xFF00_0000_0000_0000;
966 #[cfg(all(target_pointer_width = "64", target_os = "android"))] let real = real as u64 & 0x00FFF_FFF_FFFF_FFFF;
967
968 #[cfg(target_pointer_width = "64")] let real = real as u64;
969
970 // No nibble on 32-bit platforms, but we still need to assume u64 since the host always writes 64-bit addresses
971 #[cfg(target_pointer_width = "32")] let real = real as u64;
972
973 // Whisker fix for pointer-sized *capturing* closures.
974 //
975 // The `size_of::<F>() == size_of::<fn()>()` gate in
976 // `try_call` routes any closure that is coincidentally
977 // pointer-sized through this `call_as_ptr` path — which
978 // assumes `F` is a bare function pointer and recovers its
979 // address via `transmute_copy`. But a `#[component]` body
980 // closure that captures exactly one 8-byte value (the
981 // common case: a single signal / `NodeId`) is *also*
982 // pointer-sized, and `transmute_copy` then yields the
983 // captured VALUE (e.g. a slotmap key like `0x1_0000_0001`),
984 // not a code address. The lookup misses and the stale
985 // pre-patch body runs — "patch applied" but no visual
986 // change.
987 //
988 // When the transmute-recovered key isn't in the table,
989 // fall back to the closure's own `call_it`
990 // monomorphisation address — a real code symbol that the
991 // JumpTable carries (the same key the non-pointer-sized
992 // `try_call` path uses). For a genuine fn pointer the
993 // first lookup already hits, so this only adds a fallback.
994 // Genuine bare fn pointer: `real` is its code
995 // address and is in the table. Call it as
996 // `Self::Real` (a plain `fn(args)`, no `self`).
997 if let Some(ptr) = jump_table.map.get(&real).cloned() {
998 // Re-apply the nibble - though this might not be required (we aren't calling malloc for a new pointer)
999 #[cfg(all(target_pointer_width = "64", target_os = "android"))] let ptr: u64 = ptr | nibble;
1000
1001 #[cfg(target_pointer_width = "64")] let ptr: u64 = ptr;
1002 #[cfg(target_pointer_width = "32")] let ptr: u32 = ptr as u32;
1003
1004 // Macro-rules requires unpacking the tuple before we call it
1005 #[allow(non_snake_case)]
1006 let ( $($arg,)* ) = args;
1007
1008
1009 #[cfg(target_pointer_width = "64")]
1010 type PtrWidth = u64;
1011 #[cfg(target_pointer_width = "32")]
1012 type PtrWidth = u32;
1013
1014 return std::mem::transmute::<PtrWidth, Self::Real>(ptr)($($arg),*);
1015 }
1016
1017 // Whisker fix: `real` missed because this is a
1018 // pointer-sized *capturing* closure, not a bare fn
1019 // pointer — `transmute_copy` yielded the captured
1020 // value (e.g. a single signal's `NodeId`), not a code
1021 // address. Dispatch on the closure's own `call_it`
1022 // monomorphisation address instead — a real symbol the
1023 // JumpTable carries — calling it WITH `&mut self` since
1024 // `call_it` takes `self`.
1025 let call_it_key =
1026 <Self as HotFunction<($($arg,)*), $marker>>::call_it
1027 as *const () as u64;
1028 if let Some(ptr) = jump_table.map.get(&call_it_key).cloned() {
1029 let patched = std::mem::transmute::<
1030 *const (),
1031 fn(&mut Self, ($($arg,)*)) -> Self::Return,
1032 >(ptr as *const ());
1033 return patched(self, args);
1034 }
1035 }
1036
1037 self.call_it(args)
1038 }
1039 }
1040 }
1041 )*
1042 };
1043}
1044
1045impl_hot_function!(
1046 (Fn0Marker,),
1047 (Fn1Marker, A),
1048 (Fn2Marker, A, B),
1049 (Fn3Marker, A, B, C),
1050 (Fn4Marker, A, B, C, D),
1051 (Fn5Marker, A, B, C, D, E),
1052 (Fn6Marker, A, B, C, D, E, F),
1053 (Fn7Marker, A, B, C, D, E, F, G),
1054 (Fn8Marker, A, B, C, D, E, F, G, H),
1055 (Fn9Marker, A, B, C, D, E, F, G, H, I)
1056);