Binary patching rust hot-reloading, sub-second rebuilds, independent server/client hot-reload (#3797)

Enable runtime rust hot patching.

* roll back wry to 0.45

* cleanup rustc

* wip

* wip: fix client/server

* get the subsecond cli thing working again

* back to list of builds but with accessors

* migrate some methods to workspace, clean up impl a bit

* pass mode through build context

* use a build context, similar to cargo/rustc

* merge BuildUpdate and HandleUpdate into BuilderUpdate

* Move more resolution of args into runner

* migrate some methods to runner

* hoist out fullstack

* fix build request resolver

* yay request is cleaned up

* fixup resolution a bit

* spawn off the build!

* re-wire android autodetection

* re-wire android autodetection

* wire back up a few more things

* re-wire tooling verification

* I think it's mostly in the right condition for regular app

* add depinfo parser

* yay okay works with regular apps again

* full rebuilds are back

* rewire file wtcher

* wire patch!

* yayyyyyy patching works and is really fast

* yay android works

* clean up stdout piping

* yayyy memap on android so we don't need root

* create a static wasm jump table

* wip: global offset table for wasm

* wip... thinking about making the patches relocatable

* wip: customized relocation

* YESSSSSS RELOCATABLE WASM

* clean up impl a bit

* lil bit more cleanup

* lil bit more

* lil bit more cleanup

* lil bit more cleanup, support 32 bit platforms

* sick, wasm is completely pic

* hmmmm not quite working yet

* woooo, patches loading from rust+wasm

* integrated into the cli 😎

* condense a bit

* cleaned up request a bit

* bust fingerprints

* Make the file diffing logic a bit more sensible

* still working through a few issues, server launches but is acting weird

* fix merge conflict

* more merge issues

* remove stuff we don't want anymore

* revert name change

* wip: server not working anymore :(

* split apart "dioxus fullstack" into "dioxus server" and "dioxus web"

* fixup a few more compile errors

* grrr opening server

* simultaneous frontend and backend patching

* use rustc wrapper

* wip: passing assets to jumptable

* migrate project-like examples

* patchy  patchy server fns!

* rollback some random changes

* unwind the js files

* rollback hash

* no need for patch

* more cleanups

* lil bit more cleanup, remove some old cruft

* allow patching when wasm webpage is closed

* tiny bit more robust

* lil bit of clean up

* clean up ws code

* bit more clean ups

* condense

* undo file moves

* move back other project

* move other project

* migrate out harness and janky CLI

* fix compile

* anonymize some files, heavily document others

* lots more documentation, for posterity!

* ton more docs

* clean up the cli a bit more

* more ws cleanup

* more cleanup

* migrate build id to cli-config

* add command file handling

* random small cleanups

* wip....

* wip....

* fix: use workspace for krate discovery

* fix panic logging on serve by placing it *after* the logs

* reorder logging to be sensible on success

* bring back swc

* ws cruft

* small patches to relocs in loader

* bump krates

* hoist main funcs into the ifunc table, fixing wasm

* fix tui output for long lines

* add more linker logging

* wow, incredible, we build fat binaries, holy heckkkkk

* fix args for frontend/backend

* small cleanups

* properly send along build IDs to clients

* fix workspace compiles

* clean up jump table work a bit

* clean up logging a bit

* open existing

* fixup some commands

* open existing browser integration

* wire up the new dx serve client/server syntax

* fixup `dx run` command

* bring back some old functionality

* fix serverfn feature sets

* remove toast animation and simplify its code a bit

* less intrusive toast

* dont change build status when patching on web

* add proper cache busting to vcomponent

* clean up the patch file a bit

* more lints/checks cleaned up

* go back to TargetArgs but a BuildTargets resolved struct

* clean up more nits

* use an atomicptr for jumptable

* fix interaction with initial web load + suspense

* don't run asset system on wasm/bindgen js in dev

* reduce blast radius of PR

* use profile to determine if we're in release

* cleanup prod html template

* cleanup profiles

* fix feature resolution of fullstack

* if fullstack is explicitly false, don't use it

* light cleanups

* drop carg config2

* pass along linker args properly

* make workspace examples compile with dx again

* fewer unwraps and better error loggign

* bit more error handlign

* small cleanup

* drive-by cleanups

* use queries instead of initialize for aslr stuff

* fix hotpatch with higher opt level

* fix aslr bug from multiple clients

* fix merge conflict

* fix typos

* clippy

* fix miscompile

* fix doctest

* properly rollback wry to 0.45

* fix markdown path issues, other test issues

* fix test in router

* fix release mode test

* fix some more tests, clean up a few more items

* use fnptr instead of typeid

* clean up memozation via subsecond in core

* use "main" as sentinel

* fix imports and re-exports

* get off __rust_alloc for aslr offset detection

* wip

* wip... fixing unoptimized web

* hmmmm mmmm

* close,ish, still missing the wasmbindgen dynaimc imports

* aha! full wasm works now

- disable externref when compilng patch modules
- manually link wasm instrinsics
- displace the __wbindgen_placeholder__ table with the synthesized wbg imports

what an awful pain.

There might be a few items missing, so will test against the docsite shortly

* remove failing test

* hmmmm things aren't perfect

* from what I can tell, relocatable wasm just doesn't work with wasm-bindgen

sad

https://github.com/rustwasm/wasm-bindgen/issues/1420

* AI IS LITERALLY THE WORST

* preserve wasm snippets across compiles

* IT HOTPATCHES THE DOCSITE, WE ARE LIVING IN THE FUTURE

* remove the macos cfg-out

* fix: need to allocate 1 more page

* fix url issue

* properly span props

* properly fill in extra symbols

* light cleanups and good docs

* delete cruft, better docs

* More cleanups and simplifications

* clippy, typos

* remove harnesses

* wasm-bindgen error logging

* clean up deps

* remove cruft

* proper priv/pub

* use ifuncs for all env imports

* cruft

* use par_iter, better output

* implement a cache for the base module

* delete custom sections

* cache the main native module too

* add par iter in a few more places

* clippy, map ident

* better logging and status bars

* wip: some small windows support

* wip more windowss

* fix weird regression

* implement windows jump table

* small fixes for windows

* better windows support

* windows support pt2

* rust lld is not msvc

* whole archive the rlibs

* ignore windows and sys crates

* logging

* ohhhhhh, pop out dlls

* dedupe

* thANK GOD FAT LINKING WORKS ON WINDOWS

* hoist caching

* implement patch based on cache

* fix location for pdb

* pass data symbols, correct object

* add all symbol for windows

* add log

* treat none rva as undefined

* whoops

* handle windows calling convention

* dont use relocated arm64 stub windows

* not all are text?

* WINDOWS FUNCTION PATCHING WORKS

but issue with statics?

* use our own entry

* handle windows imp symbols

* add log

* jump to the real symbol not the layout

* whoops

* hotpatching windows works completely holy heck yay

* drop linker args crate

* disable ssg playwright for now

* light cleanups

* some cleanups / preps:
- bump to serverfn 0.8.2
- fix playwright test
- fix clippy

* use fix from #4042

* bring in other fix for liveview

* hoist some configuration

* use __aslr_reference instead of aslr_reference

* add better errors to patcherror

* fix android

* panic if patching fails

* clean up tomls

* wip: fixing wasm compile for server

* fullstack wasm again

* fix cfg

* fixup feature detection

* fix issue with features

* fix playwright!

* add barebones template test harness

* fix compatibility between the two hotreload engines

* fix preldue of server

* fix check for wasm-opt

* small fixes
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-05-06 23:08:59 -07:00
1 parent 71d85e6dd5
commit d976ca184a
142 files changed
+17982 -7156

No files matched your search

+28
View File
@@ -0,0 +1,28 @@
[package]
name = "subsecond"
edition = "2021"
version.workspace = true
authors = ["Jonathan Kelley"]
description = "A runtime hotpatching engine for Rust hot-reloading."
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
keywords = ["hotpatch", "engine", "subsecond", "dioxus", "hot-reload"]
[dependencies]
serde = { workspace = true, features = ["derive"] }
subsecond-types = { workspace = true }
thiserror = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = { workspace = true, features = ["FetchEvent", "Request", "Window", "Response", "ResponseType", "console"] }
wasm-bindgen = { workspace = true }
wasm-bindgen-futures = { workspace = true }
js-sys = { workspace = true}
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
libloading = { workspace = true}
libc = { workspace = true}
memmap2 = { workspace = true}
[target.'cfg(target_os = "android")'.dependencies]
memfd = { workspace = true}
+815
View File
@@ -0,0 +1,815 @@
#![allow(clippy::needless_doctest_main)]
//! # Subsecond: Hot-patching for Rust
//!
//! Subsecond is a library that enables hot-patching for Rust applications. This allows you to change
//! the code of a running application without restarting it. This is useful for game engines, servers,
//! and other long-running applications where the typical edit-compile-run cycle is too slow.
//!
//! Subsecond also implements a technique we call "ThinLinking" which makes compiling Rust code
//! significantly faster in development mode, which can be used outside of hot-patching.
//!
//! # Usage
//!
//! Subsecond is designed to be as simple for both application developers and library authors.
//!
//! Simply call your existing functions with [`call`] and Subsecond will automatically detour
//! that call to the latest version of the function.
//!
//! ```rust
//! for x in 0..5 {
//! subsecond::call(|| {
//! println!("Hello, world! {}", x);
//! });
//! }
//! ```
//!
//! To actually load patches into your application, a third-party tool that implements the Subsecond
//! compiler and protocol is required. Subsecond is built and maintained by the Dioxus team, so we
//! suggest using the dioxus CLI tool to use subsecond.
//!
//! To install the Dioxus CLI, we recommend using [`cargo binstall`](https://crates.io/crates/cargo-binstall):
//!
//! ```sh
//! cargo binstall dioxus-cli
//! ```
//!
//! The Dioxus CLI provides several tools for development. To run your application with Subsecond enabled,
//! use `dx serve` - this takes the same arguments as `cargo run` but will automatically hot-reload your
//! application when changes are detected.
//!
//! As of Dioxus 0.7, "--hotpatch" is required to use hotpatching while Subsecond is still experimental.
//!
//! ```sh
//! dx serve --hotpatch
//! ```
//!
//! ## How it works
//!
//! Subsecond works by detouring function calls through a jump table. This jump table contains the latest
//! version of the program's function pointers, and when a function is called, Subsecond will look up
//! the function in the jump table and call that instead.
//!
//! Unlike libraries like [detour](https://crates.io/crates/detour), Subsecond *does not* modify your
//! process memory. Patching pointers is wildly unsafe and can lead to crashes and undefined behavior.
//!
//! Instead, an external tool compiles only the parts of your project that changed, links them together
//! using the addresses of the functions in your running program, and then sends the new jump table to
//! your application. Subsecond then applies the patch and continues running. Since Subsecond doesn't
//! modify memory, the program must have a runtime integration to handle the patching.
//!
//! If the framework you're using doesn't integrate with subsecond, you can rely on the fact that calls
//! to stale [`call`] instances will emit a safe panic that is automatically caught and retried
//! by the next [`call`] instance up the callstack.
//!
//! Subsecond is only enabled when debug_assertions are enabled so you can safely ship your application
//! with Subsecond enabled without worrying about the performance overhead.
//!
//! ## Globals and statics
//!
//! Subsecond *does* support hot-reloading of globals, statics, and thread locals. However, there are several limitations:
//!
//! - You may add new globals at runtime, but their destructors will never be called.
//! - Globals are tracked across patches, but will renames are considered to be *new* globals.
//! - Changes to static initializers will not be observed.
//!
//! Subsecond purposefully handles statics this way since many libraries like Dioxus and Tokio rely
//! on persistent global runtimes.
//!
//! ## Struct layout and alignment
//!
//! Subsecond currently does not support hot-reloading of structs. This is because the generated code
//! assumes a particular layout and alignment of the struct. If layout or alignment change and new
//! functions are called referencing an old version of the struct, the program will crash.
//!
//! To mitigate this, framework authors can integrate with Subsecond to either dispose of the old struct
//! or to re-allocate the struct in a way that is compatible with the new layout. This is called "re-instancing."
//!
//! Because Subsecond performs a safe panic if a stale function is called, you should never witness
//! a crash due to a struct layout change. However, changing a struct's layout will likely cause a
//! re-instantiation of the struct and potentially a loss of state.
//!
//! We'd like to lift this limitation in the future by providing utilities to re-instantiate structs,
//! but for now it's up to the framework authors to handle this. For example, Dioxus apps simply throw
//! out the old state and rebuild it from scratch.
//!
//! ## Nesting Calls
//!
//! Subsecond calls are designed to be nested. This provides clean integration points to know exactly
//! where a hooked function is called.
//!
//! The highest level call is `fn main()` though by default this is not hooked since initialization code
//! tends to be side-effectual and modify global state. Instead, we recommend wrapping the hot-patch
//! points manually with [`call`].
//!
//! ```rust
//! fn main() {
//! // Changes to the the `for` loop will cause an unwind to this call.
//! subsecond::call(|| {
//! for x in 0..5 {
//! // Changes to the `println!` will be isolated to this call.
//! subsecond::call(|| {
//! println!("Hello, world! {}", x);
//! });
//! }
//! });
//! }
//! ```
//!
//! The goal here is to provide granular control over where patches are applied to limit loss of state
//! when new code is loaded.
//!
//! ## Applying patches
//!
//! When running under the Dioxus CLI, the `dx serve` command will automatically apply patches when
//! changes are detected. Patches are delivered over the [Dioxus Devtools](https://crates.io/crates/dioxus-devtools)
//! websocket protocol and received by corresponding websocket.
//!
//! If you're using Subsecond in your own application that doesn't have a runtime integration, you can
//! build an integration using the [`apply_patch`] function. This function takes a `JumpTable` which
//! the dioxus-cli crate can generate.
//!
//! To add support for the Dioxus Devtools protocol to your app, you can use the [dioxus-devtools](https://crates.io/crates/dioxus-devtools)
//! crate which provides a `connect` method that will automatically apply patches to your application.
//!
//! Unfortunately, one design quirk of Subsecond is that running apps need to communicate the address
//! of `main` to the patcher. This is due to a security technique called [ASLR](https://en.wikipedia.org/wiki/Address_space_layout_randomization)
//! which randomizes the address of functions in memory. See the subsecond-harness and subsecond-cli
//! for more details on how to implement the protocol.
//!
//! ## ThinLink
//!
//! ThinLink is a program linker for Rust that is designed to be used with Subsecond. It implements
//! the powerful patching system that Subsecond uses to hot-reload Rust applications.
//!
//! ThinLink is simply a wrapper around your existing linker but with extra features:
//!
//! - Automatic dynamic linking to dependencies
//! - Generation of Subsecond jump tables
//! - Diffing of object files for function invalidation
//!
//! Because ThinLink performs very to little actual linking, it drastically speeds up traditional Rust
//! development. With a development-optimized profile, ThinLink can shrink an incremental build to less than 500ms.
//!
//! ThinLink is automatically integrated into the Dioxus CLI though it's currently not available as
//! a standalone tool.
//!
//! ## Limitations
//!
//! Subsecond is a powerful tool but it has several limitations. We talk about them above, but here's
//! a quick summary:
//!
//! - Struct hot reloading requires instancing or unwinding
//! - Statics are tracked but not destructed
//!
//! ## Platform support
//!
//! Subsecond works across all major platforms:
//!
//! - Android (arm64-v8a, armeabi-v7a)
//! - iOS (arm64)
//! - Linux (x86_64, aarch64)
//! - macOS (x86_64, aarch64)
//! - Windows (x86_64, arm64)
//! - WebAssembly (wasm32)
//!
//! If you have a new platform you'd like to see supported, please open an issue on the Subsecond repository.
//! We are keen to add support for new platforms like wasm64, riscv64, and more.
//!
//! Note that iOS device is currently not supported due to code-signing requirements. We hope to fix
//! this in the future, but for now you can use the simulator to test your app.
//!
//! ## Adding the Subsecond badge to your project
//!
//! If you're a framework author and want your users to know that your library supports Subsecond, you
//! can add the Subsecond badge to your README! Users will know that your library is hot-reloadable and
//! can be used with Subsecond.
//!
//! [![Subsecond](https://img.shields.io/badge/Subsecond-Enabled-orange)](https://crates.io/crates/subsecond)
//!
//! ```markdown
//! [![Subsecond](https://img.shields.io/badge/Subsecond-Enabled-orange)](https://crates.io/crates/subsecond)
//! ```
//!
//! ## License
//!
//! Subsecond and ThinLink are licensed under the MIT license. See the LICENSE file for more information.
//!
//! ## Supporting this work
//!
//! Subsecond is a project by the Dioxus team. If you'd like to support our work, please consider
//! [sponsoring us on GitHub](https://github.com/sponsors/DioxusLabs) or eventually deploying your
//! apps with Dioxus Deploy (currently under construction).
pub use subsecond_types::JumpTable;
use std::{
backtrace,
mem::transmute,
panic::AssertUnwindSafe,
sync::{atomic::AtomicPtr, Arc, Mutex},
};
/// Call a given function with hot-reloading enabled. If the function's code changes, `call` will use
/// the new version of the function. If code *above* the function changes, this will emit a panic
/// that forces an unwind to the next [`call`] instance.
///
/// WASM/rust does not support unwinding, so [`call`] will not track dependency graph changes.
/// If you are building a framework for use on WASM, you will need to use `Subsecond::HotFn` directly.
///
/// However, if you wrap your calling code in a future, you *can* simply drop the future which will
/// cause `drop` to execute and get something similar to unwinding. Not great if refcells are open.
pub fn call<O>(mut f: impl FnMut() -> O) -> O {
// Only run in debug mode - the rest of this function will dissolve away
if !cfg!(debug_assertions) {
return f();
}
let mut hotfn = HotFn::current(f);
loop {
let res = std::panic::catch_unwind(AssertUnwindSafe(|| hotfn.call(())));
// If the call succeeds just return the result, otherwise we try to handle the panic if its our own.
let err = match res {
Ok(res) => return res,
Err(err) => err,
};
// If this is our panic then let's handle it, otherwise we just resume unwinding
let Some(_hot_payload) = err.downcast_ref::<HotFnPanic>() else {
std::panic::resume_unwind(err);
};
}
}
// We use an AtomicPtr with a leaked JumpTable and Relaxed ordering to give us a global jump table
// with very very little overhead. Reading this amounts of a Relaxed atomic load which basically
// is no overhead. We might want to look into using a thread_local with a stop-the-world approach
// just in case multiple threads try to call the jump table before synchronization with the runtime.
// For Dioxus purposes, this is not a big deal, but for libraries like bevy which heavily rely on
// multithreading, it might become an issue.
static APP_JUMP_TABLE: AtomicPtr<JumpTable> = AtomicPtr::new(std::ptr::null_mut());
static HOTRELOAD_HANDLERS: Mutex<Vec<Arc<dyn Fn() + Send + Sync>>> = Mutex::new(Vec::new());
pub fn register_handler(handler: Arc<dyn Fn() + Send + Sync + 'static>) {
HOTRELOAD_HANDLERS.lock().unwrap().push(handler);
}
fn get_jump_table() -> Option<&'static JumpTable> {
let ptr = APP_JUMP_TABLE.load(std::sync::atomic::Ordering::Relaxed);
if ptr.is_null() {
return None;
}
Some(unsafe { &*ptr })
}
unsafe fn commit_patch(table: JumpTable) {
APP_JUMP_TABLE.store(
Box::into_raw(Box::new(table)),
std::sync::atomic::Ordering::Relaxed,
);
HOTRELOAD_HANDLERS
.lock()
.unwrap()
.clone()
.iter()
.for_each(|handler| {
handler();
});
}
/// A panic issued by the [`call`] function if the caller would be stale if called. This causes
/// an unwind to the next [`call`] instance that can properly handle the panic and retry the call.
///
/// This technique allows Subsecond to provide hot-reloading of codebases that don't have a runtime integration.
#[derive(Debug)]
pub struct HotFnPanic {
_backtrace: backtrace::Backtrace,
}
/// A hot-reloadable function.
///
/// To call this function, use the [`HotFn::call`] method. This will automatically use the latest
/// version of the function from the JumpTable.
pub struct HotFn<A, M, F>
where
F: HotFunction<A, M>,
{
inner: F,
_marker: std::marker::PhantomData<(A, M)>,
}
impl<A, M, F: HotFunction<A, M>> HotFn<A, M, F> {
/// Create a new [`HotFn`] instance with the current function.
///
/// Whenever you call [`HotFn::call`], it will use the current function from the [`JumpTable`].
pub const fn current(f: F) -> HotFn<A, M, F> {
HotFn {
inner: f,
_marker: std::marker::PhantomData,
}
}
/// Call the function with the given arguments.
///
/// This will unwrap the [`HotFnPanic`] panic, propagating up to the next [`HotFn::call`].
///
/// If you want to handle the panic yourself, use [`HotFn::try_call`].
pub fn call(&mut self, args: A) -> F::Return {
self.try_call(args).unwrap()
}
/// Get the address of the function in memory which might be different than the original.
///
/// This is useful for implementing a memoization strategy to safely preserve state across
/// hot-patches. If the ptr_address of a function did not change between patches, then the
/// state that exists "above" the function is still valid.
///
/// Note that Subsecond does not track this state over time, so it's up to the runtime integration
/// to track this state and diff it.
pub fn ptr_address(&self) -> u64 {
if size_of::<F>() == size_of::<fn() -> ()>() {
let ptr: usize = unsafe { std::mem::transmute_copy(&self.inner) };
return ptr as u64;
}
let known_fn_ptr = <F as HotFunction<A, M>>::call_it as *const () as usize;
if let Some(jump_table) = get_jump_table() {
if let Some(ptr) = jump_table.map.get(&(known_fn_ptr as u64)).cloned() {
return ptr;
}
}
known_fn_ptr as u64
}
/// Attempt to call the function with the given arguments.
///
/// If this function is stale and can't be updated in place (ie, changes occurred above this call),
/// then this function will emit an [`HotFnPanic`] which can be unwrapped and handled by next [`call`]
/// instance.
pub fn try_call(&mut self, args: A) -> Result<F::Return, HotFnPanic> {
if !cfg!(debug_assertions) {
return Ok(self.inner.call_it(args));
}
unsafe {
// Try to handle known function pointers. This is *really really* unsafe, but due to how
// rust trait objects work, it's impossible to make an arbitrary usize-sized type implement Fn()
// since that would require a vtable pointer, pushing out the bounds of the pointer size.
if size_of::<F>() == size_of::<fn() -> ()>() {
return Ok(self.inner.call_as_ptr(args));
}
// Handle trait objects. This will occur for sizes other than usize. Normal rust functions
// become ZST's and thus their <T as SomeFn>::call becomes a function pointer to the function.
//
// For non-zst (trait object) types, then there might be an issue. The real call function
// will likely end up in the vtable and will never be hot-reloaded since signature takes self.
if let Some(jump_table) = get_jump_table() {
let known_fn_ptr = <F as HotFunction<A, M>>::call_it as *const () as u64;
if let Some(ptr) = jump_table.map.get(&known_fn_ptr).cloned() {
// The type sig of the cast should match the call_it function
// Technically function pointers need to be aligned, but that alignment is 1 so we're good
let call_it = transmute::<*const (), fn(&F, A) -> F::Return>(ptr as _);
return Ok(call_it(&self.inner, args));
}
}
Ok(self.inner.call_it(args))
}
}
}
/// Apply the patch using a given jump table.
///
/// # Safety
///
/// This function is unsafe because it detours existing functions in memory. This is *wildly* unsafe,
/// especially if the JumpTable is malformed. Only run this if you know what you're doing.
///
/// If the pointers are incorrect, function type signatures will be incorrect and the program will crash,
/// sometimes in a way that requires a restart of your entire computer. Be careful.
///
/// # Warning
///
/// This function will load the library and thus allocates. In cannot be used when the program is
/// stopped (ie in a signal handler).
pub unsafe fn apply_patch(mut table: JumpTable) -> Result<(), PatchError> {
// On non-wasm platforms we can just use libloading and the known aslr offsets to load the library
#[cfg(any(unix, windows))]
{
// on android we try to cirumvent permissions issues by copying the library to a memmap and then libloading that
#[cfg(target_os = "android")]
let lib = Box::leak(Box::new(android_memmap_dlopen(&table.lib)?));
#[cfg(not(target_os = "android"))]
let lib = Box::leak(Box::new({
match libloading::Library::new(&table.lib) {
Ok(lib) => lib,
Err(err) => return Err(PatchError::Dlopen(err.to_string())),
}
}));
// Use the `__aslr_reference` symbol as a sentinel for the current executable. This is basically a
// cross-platform version of `__mh_execute_header` on macOS that sets a reference point for the
// jump table.
let old_offset = __aslr_reference() - table.aslr_reference as usize;
// Use the `main` symbol as a sentinel for the loaded library. Might want to move away
// from this at some point, or make it configurable
let new_offset = unsafe {
// Leak the library. dlopen is basically a no-op on many platforms and if we even try to drop it,
// some code might be called (ie drop) that results in really bad crashes (restart your computer...)
//
// This code currently assumes "main" always makes it to the export list (which it should)
// and requires coordination from the CLI to export it.
lib.get::<*const ()>(b"main")
.ok()
.unwrap()
.try_as_raw_ptr()
.unwrap()
.wrapping_byte_sub(table.new_base_address as usize) as usize
};
// Modify the jump table to be relative to the base address of the loaded library
table.map = table
.map
.iter()
.map(|(k, v)| {
(
(*k as usize + old_offset) as u64,
(*v as usize + new_offset) as u64,
)
})
.collect();
commit_patch(table);
};
// On wasm, we need to download the module, compile it, and then run it.
#[cfg(target_arch = "wasm32")]
wasm_bindgen_futures::spawn_local(async move {
use js_sys::{
ArrayBuffer, Object, Reflect,
WebAssembly::{self, Memory, Table},
};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;
use wasm_bindgen::UnwrapThrowExt;
use wasm_bindgen_futures::JsFuture;
let funcs: Table = wasm_bindgen::function_table().unchecked_into();
let memory: Memory = wasm_bindgen::memory().unchecked_into();
let exports: Object = wasm_bindgen::exports().unchecked_into();
let buffer: ArrayBuffer = memory.buffer().unchecked_into();
let path = table.lib.to_str().unwrap();
if !path.ends_with(".wasm") {
return;
}
// Start the fetch of the module
let response = web_sys::window().unwrap_throw().fetch_with_str(&path);
// Wait for the fetch to complete - we need the wasm module size in bytes to reserve in the memory
let response: web_sys::Response = JsFuture::from(response).await.unwrap().unchecked_into();
let dl_bytes: ArrayBuffer = JsFuture::from(response.array_buffer().unwrap())
.await
.unwrap()
.unchecked_into();
// Expand the memory and table size to accommodate the new data and functions
//
// Normally we wouldn't be able to trust that we are allocating *enough* memory
// for BSS segments, but ld emits them in the binary when using import-memory.
//
// Make sure we align the memory base to the page size
const PAGE_SIZE: u32 = 64 * 1024;
let page_count = (buffer.byte_length() as f64 / PAGE_SIZE as f64).ceil() as u32;
let memory_base = (page_count + 1) * PAGE_SIZE;
// We need to grow the memory to accommodate the new module
memory.grow((dl_bytes.byte_length() as f64 / PAGE_SIZE as f64).ceil() as u32 + 1);
// We grow the ifunc table to accommodate the new functions
// In theory we could just put all the ifuncs in the jump map and use that for our count,
// but there's no guarantee from the jump table that it references "itself"
// We might need a sentinel value for each ifunc in the jump map to indicate that it is
let table_base = funcs.grow(table.ifunc_count as u32).unwrap();
// Adjust the jump table to be relative to the new base address
for v in table.map.values_mut() {
*v += table_base as u64;
}
// Build up the import object. We copy everything over from the current exports, but then
// need to add in the memory and table base offsets for the relocations to work.
//
// let imports = {
// env: {
// memory: base.memory,
// __tls_base: base.__tls_base,
// __stack_pointer: base.__stack_pointer,
// __indirect_function_table: base.__indirect_function_table,
// __memory_base: memory_base,
// __table_base: table_base,
// ..base_exports
// },
// };
let env = Object::new();
// Move memory, __tls_base, __stack_pointer, __indirect_function_table, and all exports over
for key in Object::keys(&exports) {
Reflect::set(&env, &key, &Reflect::get(&exports, &key).unwrap()).unwrap();
}
// Set the memory and table in the imports
// Following this pattern: Global.new({ value: "i32", mutable: false }, value)
for (name, value) in [("__table_base", table_base), ("__memory_base", memory_base)] {
let descriptor = Object::new();
Reflect::set(&descriptor, &"value".into(), &"i32".into()).unwrap();
Reflect::set(&descriptor, &"mutable".into(), &false.into()).unwrap();
let value = WebAssembly::Global::new(&descriptor, &value.into()).unwrap();
Reflect::set(&env, &name.into(), &value.into()).unwrap();
}
// Set the memory and table in the imports
let imports = Object::new();
Reflect::set(&imports, &"env".into(), &env).unwrap();
// Download the module, returning { module, instance }
// we unwrap here instead of using `?` since this whole thing is async
let result_object = JsFuture::from(WebAssembly::instantiate_module(
dl_bytes.unchecked_ref(),
&imports,
))
.await
.unwrap();
// We need to run the data relocations and then fire off the constructors
let res: Object = result_object.unchecked_into();
let instance: Object = Reflect::get(&res, &"instance".into())
.unwrap()
.unchecked_into();
let exports: Object = Reflect::get(&instance, &"exports".into())
.unwrap()
.unchecked_into();
_ = Reflect::get(&exports, &"__wasm_apply_data_relocs".into())
.unwrap()
.unchecked_into::<js_sys::Function>()
.call0(&JsValue::undefined());
_ = Reflect::get(&exports, &"__wasm_apply_global_relocs".into())
.unwrap()
.unchecked_into::<js_sys::Function>()
.call0(&JsValue::undefined());
_ = Reflect::get(&exports, &"__wasm_call_ctors".into())
.unwrap()
.unchecked_into::<js_sys::Function>()
.call0(&JsValue::undefined());
unsafe { commit_patch(table) };
});
Ok(())
}
#[derive(Debug, PartialEq, thiserror::Error)]
pub enum PatchError {
/// The patch failed to apply.
///
/// This returns a string instead of the Dlopen error type so we don't need to bring the libloading
/// dependency into the public API.
#[error("Failed to load library: {0}")]
Dlopen(String),
/// The patch failed to apply on Android, most likely due to a permissions issue.
#[error("Failed to load library on Android: {0}")]
AndroidMemfd(String),
}
/// This function returns its own address, providing a stable reference point for hot-patch engine
/// to hook onto. If you were to write an object file for this function, it would amount to:
///
/// ```asm
/// __aslr_reference:
/// mov rax, qword ptr [rip + __aslr_reference@GOTPCREL] // notice the @GOTPCREL relocation
/// ret
/// ```
///
/// The point here being that we have a stable address both at runtime and compile time, making it
/// possible to calculate the ASLR offset from within the process to correct the jump table.
#[doc(hidden)]
#[inline(never)]
#[no_mangle]
pub extern "C" fn __aslr_reference() -> usize {
__aslr_reference as *const () as usize
}
/// On Android, we can't dlopen libraries that aren't placed inside /data/data/<package_name>/lib/
///
/// If the device isn't rooted, then we can't push the library there.
/// This is a workaround to copy the library to a memfd and then dlopen it.
///
/// I haven't tested it on device yet, so if if it doesn't work, then we can simply revert to using
/// "adb root" and then pushing the library to the /data/data folder instead of the tmp folder.
///
/// Android provides us a flag when calling dlopen to use a file descriptor instead of a path, presumably
/// because they want to support this.
/// - https://developer.android.com/ndk/reference/group/libdl
/// - https://developer.android.com/ndk/reference/structandroid/dlextinfo
#[cfg(target_os = "android")]
unsafe fn android_memmap_dlopen(file: &std::path::Path) -> Result<libloading::Library, PatchError> {
use std::ffi::{c_void, CStr, CString};
use std::os::fd::{AsRawFd, BorrowedFd};
use std::ptr;
#[repr(C)]
struct ExtInfo {
flags: u64,
reserved_addr: *const c_void,
reserved_size: libc::size_t,
relro_fd: libc::c_int,
library_fd: libc::c_int,
library_fd_offset: libc::off64_t,
library_namespace: *const c_void,
}
extern "C" {
fn android_dlopen_ext(
filename: *const libc::c_char,
flags: libc::c_int,
ext_info: *const ExtInfo,
) -> *const c_void;
}
use memmap2::MmapAsRawDesc;
use std::os::unix::prelude::{FromRawFd, IntoRawFd};
let contents = std::fs::read(file)
.map_err(|e| PatchError::AndroidMemfd(format!("Failed to read file: {}", e)))?;
let mut mfd = memfd::MemfdOptions::default()
.create("subsecond-patch")
.map_err(|e| PatchError::AndroidMemfd(format!("Failed to create memfd: {}", e)))?;
mfd.as_file()
.set_len(contents.len() as u64)
.map_err(|e| PatchError::AndroidMemfd(format!("Failed to set memfd length: {}", e)))?;
let raw_fd = mfd.into_raw_fd();
let mut map = memmap2::MmapMut::map_mut(raw_fd)
.map_err(|e| PatchError::AndroidMemfd(format!("Failed to map memfd: {}", e)))?;
map.copy_from_slice(&contents);
let map = map
.make_exec()
.map_err(|e| PatchError::AndroidMemfd(format!("Failed to make memfd executable: {}", e)))?;
let filename = c"/subsecond-patch";
let info = ExtInfo {
flags: 0x10, // ANDROID_DLEXT_USE_LIBRARY_FD
reserved_addr: ptr::null(),
reserved_size: 0,
relro_fd: 0,
library_fd: raw_fd,
library_fd_offset: 0,
library_namespace: ptr::null(),
};
let flags = libloading::os::unix::RTLD_LAZY | libloading::os::unix::RTLD_LOCAL;
let handle = libloading::os::unix::with_dlerror(
|| {
let ptr = android_dlopen_ext(filename.as_ptr() as _, flags, &info);
if ptr.is_null() {
return None;
} else {
return Some(ptr);
}
},
|err| err.to_str().unwrap_or_default().to_string(),
)
.map_err(|e| {
PatchError::AndroidMemfd(format!(
"android_dlopen_ext failed: {}",
e.unwrap_or_default()
))
})?;
let lib = unsafe { libloading::os::unix::Library::from_raw(handle as *mut c_void) };
let lib: libloading::Library = lib.into();
Ok(lib)
}
/// A trait that enables types to be hot-patched.
///
/// This trait is only implemented for FnMut types which naturally includes function pointers and
/// closures that can be re-ran. FnOnce closures are currently not supported since the hot-patching
/// system we use implies that the function can be called multiple times.
pub trait HotFunction<Args, Marker> {
/// The return type of the function.
type Return;
/// The real function type. This is meant to be a function pointer.
/// When we call `call_as_ptr`, we will transmute the function to this type and call it.
type Real;
/// Call the HotFunction with the given arguments.
///
/// # Why
///
/// "rust-call" isn't stable, so we wrap the underlying call with our own, giving it a stable vtable entry.
/// This is more important than it seems since this function becomes "real" and can be hot-patched.
fn call_it(&mut self, args: Args) -> Self::Return;
/// Call the HotFunction as if it were a function pointer.
///
/// # Safety
///
/// This is only safe if the underlying type is a function (function pointer or virtual/fat pointer).
/// Using this will use the JumpTable to find the patched function and call it.
unsafe fn call_as_ptr(&mut self, _args: Args) -> Self::Return;
}
macro_rules! impl_hot_function {
(
$(
($marker:ident, $($arg:ident),*)
),*
) => {
$(
/// A marker type for the function.
/// This is hidden with the intention to seal this trait.
#[doc(hidden)]
pub struct $marker;
impl<T, $($arg,)* R> HotFunction<($($arg,)*), $marker> for T
where
T: FnMut($($arg),*) -> R,
{
type Return = R;
type Real = fn($($arg),*) -> R;
fn call_it(&mut self, args: ($($arg,)*)) -> Self::Return {
#[allow(non_snake_case)]
let ( $($arg,)* ) = args;
self($($arg),*)
}
unsafe fn call_as_ptr(&mut self, args: ($($arg,)*)) -> Self::Return {
unsafe {
if let Some(jump_table) = get_jump_table() {
let real = std::mem::transmute_copy::<Self, Self::Real>(&self) as *const ();
// Android implements MTE / pointer tagging and we need to preserve the tag.
// If we leave the tag, then indexing our jump table will fail and patching won't work (or crash!)
// This is only implemented on 64-bit platforms since pointer tagging is not available on 32-bit platforms
// In dev, Dioxus disables MTE to work around this issue, but we still handle it anyways.
#[cfg(all(target_pointer_width = "64", target_os = "android"))] let nibble = real as u64 & 0xFF00_0000_0000_0000;
#[cfg(target_pointer_width = "64")] let real = real as u64 & 0x00FFF_FFF_FFFF_FFFF;
#[cfg(target_pointer_width = "64")] let real = real as u64;
// No nibble on 32-bit platforms, but we still need to assume u64 since the host always writes 64-bit addresses
#[cfg(target_pointer_width = "32")] let real = real as u64;
if let Some(ptr) = jump_table.map.get(&real).cloned() {
// Re-apply the nibble - though this might not be required (we aren't calling malloc for a new pointer)
#[cfg(all(target_pointer_width = "64", target_os = "android"))] let ptr: u64 = ptr | nibble;
#[cfg(target_pointer_width = "64")] let ptr: u64 = ptr;
#[cfg(target_pointer_width = "32")] let ptr: u32 = ptr as u32;
// Macro-rules requires unpacking the tuple before we call it
#[allow(non_snake_case)]
let ( $($arg,)* ) = args;
#[cfg(target_pointer_width = "64")]
type PtrWidth = u64;
#[cfg(target_pointer_width = "32")]
type PtrWidth = u32;
return std::mem::transmute::<PtrWidth, Self::Real>(ptr)($($arg),*);
}
}
self.call_it(args)
}
}
}
)*
};
}
impl_hot_function!(
(Fn0Marker,),
(Fn1Marker, A),
(Fn2Marker, A, B),
(Fn3Marker, A, B, C),
(Fn4Marker, A, B, C, D),
(Fn5Marker, A, B, C, D, E),
(Fn6Marker, A, B, C, D, E, F),
(Fn7Marker, A, B, C, D, E, F, G),
(Fn8Marker, A, B, C, D, E, F, G, H),
(Fn9Marker, A, B, C, D, E, F, G, H, I)
);