Compare commits

..
796 Commits
Author SHA1 Message Date
Ole André Vadla Ravnås 95b9cd3bae barebone: Stub g_io_watch_funcs in the Linux .ko
A freestanding GLib builds neither giounix.c nor giowin32.c, so
g_io_watch_funcs is defined nowhere, yet gobject's gsourceclosure.c
takes its address. The kernel module must resolve every symbol, and
the agent never creates a GIOChannel watch, so provide a weak
placeholder that GLib's own definition overrides once the freestanding
build ships one.
2026-10-08 17:01:18 +02:00
Ole André Vadla Ravnås d8a73dae8b android: Stop truncating app icons
The Base64OutputStream was never closed, so its final group was never
written.
2026-10-08 16:37:42 +02:00
Ole André Vadla Ravnås 8947659b2f subprojects: Prepare for release 2026-10-08 14:48:53 +02:00
Ole André Vadla Ravnås d0895b4026 subprojects: Bump outdated 2026-10-08 14:29:30 +02:00
Ole André Vadla Ravnås 8d0e8ad85e compiler: Bump frida-fs to 7.1.1 2026-10-08 14:04:26 +02:00
Ole André Vadla Ravnås cd0df43cdc barebone: Report the Linux copy's errno
SystemFunction reads the system error through gum_thread_get_system_error,
which linux-injected left to the stub returning 0, so failures came back
with errno 0. Read the C library's thread-local errno instead, resolving
__errno_location on glibc and musl or __errno on Bionic.
2026-10-08 13:08:19 +02:00
Ole André Vadla Ravnås 40d053dd51 barebone: Drop the Linux copy-status logging
The kernel half already detects a copy's exit through the
sched_process_exit tracepoint and detaches the session, so logging
what each copy hit was only noise. Remove it, along with the helpers
left unused.
2026-10-08 12:52:13 +02:00
Ole André Vadla Ravnås 7962abb3a3 barebone: Resolve a Linux copy's own exports
gum_module_find_global_export_by_name fell through to the kernel
symbol table for a linux-injected copy, so userspace names like
opendir never resolved and frida-fs could not list a directory.
Search the copy's mapped modules instead, as the Windows and XNU
copies already do.
2026-10-08 11:26:49 +02:00
Ole André Vadla Ravnås a94cd60e60 barebone: Keep the Linux kernel symbols alive
The upcast to LinuxKernelSymbols inside the ?: freed the System.map
before set_kernel stored it, leaving image.kernel dangling and the
session reading freed memory once the config was parsed. Assign each
image kind directly instead.
2026-10-08 11:03:14 +02:00
Ole André Vadla Ravnås d784b73d12 compiler: Bump frida-fs to 7.1.0 2026-10-08 09:07:23 +02:00
Ole André Vadla Ravnås f0bd948a71 barebone: Warm async I/O before the loop runs
GLib brings its worker context and GTask pool up lazily, on the
first async GIO call. Inside the agent's live loop that wedges it,
so warm them at copy startup while still single-threaded.
2026-10-08 05:13:36 +02:00
Ole André Vadla Ravnås fc2c0101a1 barebone: Give the win9x loop its own wake event
The main loop must be wakeable even when GLib watches no handles
and no timer is due, where it sleeps INFINITE. Wait on a dedicated
event in poll_handles instead of a token slot shared with the send
path, and set it from poke_loop_wakeup.
2026-10-08 04:12:42 +02:00
Ole André Vadla Ravnås f911715ebf barebone: Enable GLib's IO features for the agent
The diet GLib leaves the private vtable's IO entries null until
glib_enable_io_features() runs, so the first GIO user faulted on a
null g_get_worker_context. Streams are that first user.
2026-10-08 02:49:56 +02:00
Ole André Vadla Ravnås 8c206ea6cf barebone: Back streams and ANSI in the copies
Hand Gum the poll, read, write and close of the user copies so
scripts get Win32InputStream, Win32OutputStream and their UNIX
counterparts, and convert ANSI strings through the code page of
the process. The Win9x copy waits on handles like the NT one.
2026-10-08 00:41:42 +02:00
Ole André Vadla Ravnås 677f9c8379 barebone: Share the windows_fn! macro
The NT kernel half and the user copies each had a copy; the Win9x
copy wants it too.
2026-10-08 00:41:42 +02:00
Ole André Vadla Ravnås 474d8aab5a barebone: Expose the last error of Windows copies
Back gum_thread_get_system_error() and its setter with
GetLastError() and SetLastError() when running in the userspace
copy, so SystemFunction and Interceptor report lastError.
2026-10-08 00:41:42 +02:00
Ole André Vadla Ravnås 3302340a48 barebone: Enumerate the imports of Windows modules
Walk the import directory of a module in the process the copy runs
in, the way exports already are, so Module.enumerateImports() stops
coming back empty on Win9x and NT.
2026-10-08 00:41:42 +02:00
Ole André Vadla Ravnås b2161e02aa compiler: Bump frida-fs to 7.0.1
Its stat field reads passed the path to NativePointer#read*(), which
now take an optional offset.
2026-10-07 20:35:36 +02:00
Ole André Vadla Ravnås 6880660481 subprojects: Revert the termux-elf-cleaner bump
Its new code needs C++20, which the host tool build does not use.
2026-10-07 15:46:21 +02:00
Ole André Vadla Ravnås 94dc97155f subprojects: Prepare for release 2026-10-07 14:26:53 +02:00
Ole André Vadla Ravnås 16c98b23d0 subprojects: Bump outdated 2026-10-07 14:23:37 +02:00
Ole André Vadla Ravnås 9f9428208e submodules: Bump releng 2026-10-07 14:23:36 +02:00
Ole André Vadla Ravnås ce8d8889a7 compiler: Fix the pattern bundle tests
The library fixture declared Vec3 beside the geometry import meant to
provide it, and the alias assertion predates shared imported units,
which bind the alias to the shared class instead of a copy.
2026-10-07 14:10:00 +02:00
Ole André Vadla Ravnås 1d571ec980 patterns: Let hosts define preprocessor macros
Add a defines dictionary to PatternCompileOptions, carried by the
module into decode and call, and take the language server's from
settings.patterns.defines on workspace/didChangeConfiguration. A
pattern can then branch on where its data comes from, such as a
file or a mapped image, without knowing its host.
2026-10-07 14:05:05 +02:00
Ole André Vadla Ravnås 904a4a12ed patterns: Report endless template recursion
A template that instantiates itself with a growing argument never
reaches a cached shell, so the resolver recursed until the stack ran
out. Stop after 64 nested instantiations and say so.
2026-10-07 14:05:05 +02:00
Ole André Vadla Ravnås 7ada909cf8 patterns: Hand ref parameters the pattern
A ref parameter typed auto received the field's value, so a function
formatting a bitfield or struct found no members. Pass the pattern as
is done for parameters of a composite type.
2026-10-07 14:05:05 +02:00
Ole André Vadla Ravnås cc56d552ad patterns: Find template locals by name as fallback
A template instantiated from two enclosing templates with a parameter
of the same name shares one shell, so the argument it captured names
the other instantiation's local. Fall back to the local of that name.
2026-10-07 14:05:05 +02:00
Ole André Vadla Ravnås 1f28539920 patterns: Answer array_index() inside an element
The index lived on the frame that owns the array, so a member of the
element type asked its own frame and always got zero. Walk up to the
frame that is iterating.
2026-10-07 14:05:05 +02:00
Ole André Vadla Ravnås 37dc612b12 patterns: Keep enum names through member access
Only a field named directly was labelled with its enumerator; a member
of a struct, a bitfield member, an element or a ref parameter gave the
bare number. Hand builtins that take a pattern the pattern itself, so
formatted_value() formats scalars and bitfields too.
2026-10-07 14:05:05 +02:00
Ole André Vadla Ravnås d26d4c88d4 patterns: Look up fields by name as a fallback
A template instantiated with a field argument is shared by every
enclosing type that spells the argument the same way, so the field it
captured belongs to whichever instantiation came first. Fall back to
the decoded field of that name.
2026-10-07 14:05:05 +02:00
Ole André Vadla Ravnås b69b0ebd61 barebone: Stop scribbling on the Win9x gone ring
on_module_load bumped the words at arena 0xb0 and 0xb4, which are
the head and tail of the departed-thread ring. A target that
loaded a library while attached shifted the ring under the copy,
which then read a stale slot and reported a thread that never left.
2026-10-07 09:55:05 +02:00
Ole André Vadla Ravnås d5ad5e288f barebone: Let the Win9x copy's main thread exit
Return from the main thread and let it run ExitThread on the
injection stack. KERNEL32 made the thread's own stack in the
process that created it, thus point the TIB and the TDB at the
injection stack first. Free the memory only once VMM destroys the
thread.

The thread used to park in the image and on the injection stack,
which detach freed. It crashed the process once a later injection
reused that memory.
2026-10-07 02:16:22 +02:00
Ole André Vadla Ravnås 557824d30b barebone: Free the Win9x copy's start stack
The injected thread leaves it as soon as the trampoline switches to
the injection stack, which is before it reports that it runs.
2026-10-07 02:16:22 +02:00
Ole André Vadla Ravnås 276b45047d barebone: Wake the Win9x copy when detaching
The copy's worker sleeps in an alertable wait until an APC wakes it,
thus it never saw the stop request. Detach timed out after ten
seconds, the host stopped waiting for the agent to leave, and the
next session ran alongside the old agent and its VxD descriptor.
That bluescreened the guest.
2026-10-07 02:16:22 +02:00
Ole André Vadla Ravnås 53eac9e6ae barebone: Let Win9x page in what the agent reads
Hand a fault on an absent page back to Windows when Gum does not
claim it. The agent reads the memory of other processes, and such
a page is paged out, not invalid. Explorer's image headers were
one, thus attaching to it parked the agent while it was switched
into Explorer's memory context, and the guest bluescreened.
2026-10-07 00:51:57 +02:00
Ole André Vadla Ravnås d1f5966ca6 subprojects: Prepare for release 2026-10-05 10:00:26 +02:00
Ole André Vadla Ravnås 9748fa155a subprojects: Bump outdated 2026-10-05 09:56:55 +02:00
Ole André Vadla Ravnås 8eab4bb94b submodules: Bump releng 2026-10-05 09:56:54 +02:00
mbv06 5a2f81d600 android: Scan boot-image-methods.art for setArgV0
Newer ART copies the boot image's methods section into its own memfd
mapping (see Jit::MapBootImageMethods()), so look for the setArgV0()
JNI slot there too.
2026-10-05 09:54:50 +02:00
Ole André Vadla Ravnås 308b99f1e0 subprojects: Prepare for release 2026-10-04 11:35:27 +02:00
Ole André Vadla Ravnås 9b748ea649 subprojects: Bump outdated 2026-10-04 11:34:39 +02:00
Ole André Vadla Ravnås ea9efa1e24 subprojects: Prepare for release 2026-10-03 07:56:08 +02:00
Ole André Vadla Ravnås bb012fa0bb subprojects: Bump outdated 2026-10-03 07:55:12 +02:00
Ole André Vadla Ravnås 4f49ef529e compiler: Add library builds
Let Compiler.build_library() emit a package's modules, declaration
files and source maps, one set per source, with the patterns they
import copied alongside and their declarations beside them, so a
library needs no tsc of its own and its consumers compile the patterns
themselves. Add watch_library() to keep the output fresh as the
sources change.
2026-10-02 22:38:22 +02:00
Ole André Vadla Ravnås f3e525f6b0 subprojects: Prepare for release 2026-10-02 20:52:52 +02:00
Ole André Vadla Ravnås 065c8ed0dc compiler: Absolutize project roots
A relative root crashed builds in the TypeScript VFS, and left pattern
file names relative to the working directory.
2026-10-02 20:52:52 +02:00
Ole André Vadla Ravnås 5f454c7281 subprojects: Bump outdated 2026-10-02 20:26:50 +02:00
Ole André Vadla Ravnås 1963f365a4 compiler: Compile patterns from files
Take an entrypoint and a project root, like Compiler.build(), so imports
and includes resolve the way they do in agent builds, and name the file
of each type and diagnostic relative to the project root. Resolve the
same way in the language server, with open documents as overlays.
2026-10-02 19:16:46 +02:00
Ole André Vadla Ravnås ba1ab6c76f barebone: Fix the agent's warnings
Gate what only some flavors use, drop what none use, and take GPollFD
from the bindings instead of redeclaring it.
2026-10-02 03:11:47 +02:00
Ole André Vadla Ravnås 0b6733f7a9 barebone: Zero Arm vector state in CPU contexts
The kernel's CpuState has no v[], which broke the 32-bit Arm build.
2026-10-02 03:11:47 +02:00
Ole André Vadla Ravnås 9bd22b4a3e compiler: Probe for the MinGW compiler per flavor
MSYS2 ships every flavor's directory, so an empty ucrt64/ shadowed a
MINGW64-only toolchain. Pick the first flavor that has the compiler,
else fall back to PATH.
2026-10-02 03:11:47 +02:00
Ole André Vadla Ravnås 1a069a5d6c subprojects: Prepare for release 2026-10-02 01:36:34 +02:00
Ole André Vadla Ravnås a9c70dd907 subprojects: Bump outdated 2026-10-02 01:32:02 +02:00
Ole André Vadla Ravnås 3c51ca3563 ci: Use UCRT64 for the Windows cgo toolchain 2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 8f03b3a4a9 compiler: Prefer UCRT64 when building with cgo
MSYS2 deprecates MINGW64, and UCRT matches the CRT that MSVC links
against. Fall back to MINGW64 where UCRT64 is missing.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 75e5310101 compiler: Trim redundant work from $parse
Hoist the environment's base, compute each field's offset once, and
check each run of static fields once. Start such runs after dynamic
fields too in packed structs, and let constant attributes stay on them.
Parse structs and fixed arrays without allocating pairs, and derive a
pattern's address on demand.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås a766c09cbe compiler: Take NativePointers as given
Stop wrapping addresses in ptr(), and declare NativePointer rather than
NativePointerValue.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 8c430cff4e compiler: Share imported types and functions
Emit each imported unit once, keyed by its path and incoming macros,
and bind its declarations in importers, also under an alias.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 9e4c5cb215 compiler: Honour the byte order of casts
Swap be u16(x) and its kin when the order differs from the default.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 98cc87e1d5 compiler: Check bounds before reading pattern data
Report a read past the data as truncation rather than faulting, except
in growable sections. Let std::mem's readers read zeros there, as ImHex
does.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 79a17eedfd compiler: Place patterns at 64-bit addresses
GumJS reads a u64 as a UInt64, which was taken for a section pointer.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås b3255dd311 compiler: Format patterns once decoding ends
ImHex formats lazily, so a format may refer to what follows. Format
the untransformed pattern, and let formatted_value() format on demand.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 9f83790b47 compiler: Expose patterns while they are decoded
So members can refer to it by name, as in ImHex.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 4130d6e57c compiler: Pass patterns to auto and struct params
Read a pattern as its value where a number or condition is expected.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 0918e8d70a compiler: Match ImHex's std::time
Exchange times as ImHex's packed u128, and implement the library
functions over them.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 55f88343db compiler: Copy patterns assigned to struct members 2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås bf33ca6184 compiler: Store values wider than 64 bits 2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås bd7d1857f0 compiler: Resolve builtin:: calls to the builtin
So a library function of the same name cannot shadow it.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 7594907131 compiler: Size strings held in locals 2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 653c876d8a compiler: Let the shim read zero bytes at the end 2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 005712427a compiler: Hash patterns through their extent 2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 75217cd95b compiler: Size static types from their layout
Decoding at the cursor ran the type's attributes on unrelated data.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 6aa0955e97 compiler: Apply alias attributes to struct fields
Such as the transform of type::uLEB128.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 2bfff35610 compiler: Read patterns used as numbers
Let patterns convert to their value, and make array lengths numbers.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 94d02474c1 compiler: Pass patterns by reference to attributes
Wrap the argument when the function's parameter is ref.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 9684d98b51 compiler: End struct parsing on a bare return 2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 2a155c6101 compiler: Keep the cursor across placed fields
A nested decode moved it, stretching the enclosing struct.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 8212dc7065 compiler: Pass template arguments to bitfields 2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 73a03e6f97 compiler: Resolve pattern members through pointers
Dereference pointer fields on member access, and resolve members the
compiler cannot type at runtime.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 2748be9323 compiler: Keep all bytes of fixed-size strings
So they compare equal to magics such as "SQLite format 3\x00". Decode
bytes that aren't UTF-8 as \uDCxx, as literals already are.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 26a5c2fd0f compiler: Follow ImHex's operator precedence
Bind bitwise operators tighter than comparisons.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås aba0cbf19a compiler: Read static pattern fields directly
Read fields with a known layout at literal offsets, and skip the pair
destructuring for fixed-width reads.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 3c8b2824c2 compiler: Decode patterns into $Pattern instances
Keep the type, arguments, parent and fields behind getters, rather
than defining them on every decoded value.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås bc1c2cf1e8 compiler: Track the pattern cursor as an offset
Describe field extents with a $Span, which derives the address on
demand.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 47edda06b2 compiler: Access pattern fields at an offset
Avoid allocating a NativePointer per field access.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 72bdfbc0d7 compiler: Order emitted patterns by abstraction
Emit callers before callees and drop unreachable functions. Load each
corpus module under node, as the order now affects evaluation.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås 6c1dd8e31f compiler: Share the pattern runtime across modules
Serve the helpers as a virtual module, so a bundle holds one copy. Give
each host constant a module of its own, so unused ones are dropped.
2026-10-02 01:26:51 +02:00
Ole André Vadla Ravnås ab0df9b070 barebone: Add version to the kernel Module stub 2026-10-02 00:03:43 +02:00
Ole André Vadla Ravnås 0c8081bdb1 barebone: Bump @types/frida-gum to 19.12.0 2026-10-02 00:03:43 +02:00
Ole André Vadla Ravnås 9c1ef0f10e compiler: Bump @types/frida-gum to 19.12.0 2026-10-02 00:01:29 +02:00
Ole André Vadla Ravnås 4859422d65 base: Keep Posix types out of the API
Consumers of frida-base should not need posix.vapi.
2026-10-01 21:03:20 +02:00
Ole André Vadla Ravnås dd0970bc72 ci: Set up MSVC without a Node 20 action 2026-10-01 20:44:53 +02:00
Ole André Vadla Ravnås 43a6e33ffd compiler: Add ImHex pattern language support
Lex, parse, resolve and lay out .hexpat sources in Go, evaluate them
against bytes, and emit JavaScript that does the same in an agent,
with TypeScript declarations. Embed the std library, with its builtins
on both sides.

Let agents import .pat and .hexpat files as ES modules. Add
PatternCompiler, which compiles a source into a PatternModule that
describes its types and decodes memory against them, visualizers
included. Serve .pat and .hexpat documents in the language server.

Share parses and compiled modules between builds, queries and the
language server. Like ImHex, fail on unresolved names only when the
code using them runs.

Run the ImHex-Patterns corpus and the PatternLanguage tests as opt-in
suites: 303 of 312 corpus files compile, the rest needing ImHex itself
or failing there too; all 78 reference cases pass, and all 55 under
node.
2026-10-01 20:44:53 +02:00
Ole André Vadla Ravnås a12a300a99 barebone: Fix build without the Droidy backend
Lift the ADB client out of the Droidy backend as src/adb.vala, with its
types renamed to Adb*, and build it with the base sources. Build the
Android helper client whenever the Barebone backend is enabled.
2026-10-01 20:44:53 +02:00
Ole André Vadla Ravnås a9cfb2c310 payload: Create stdio registries on first use
Gum's Interceptor now looks up the module of each function it hooks,
and gum_init_embedded() hooks the unwinder. That activates the module
registry, which reads /proc/self/auxv through fdopen() before the libc
shim has created its stream registry, crashing every Android agent on
load.
2026-10-01 19:08:53 +02:00
Ole André Vadla Ravnås 563de2b426 barebone: Serve macOS Android-emulator debug regs
Arming the hardware breakpoint sets MDCR_EL2.TDE so HVF routes the
guest's debug exceptions to the host, but TDE also forces TDA, so the
guest's own debug-register accesses trap too. QEMU's HVF path has no
handler for them and aborts the whole VM -- a rare crash on kernels
that touch those registers under injection, such as Android 16's 6.12.

Perform the trapped access against the vcpu and step over it, so the
guest keeps running and QEMU never sees an unhandled system register.
2026-10-01 01:04:16 +02:00
Ole André Vadla Ravnås 950b5c832d barebone: Filter macOS Android-emulator idle hits
run_until_schedule waits for a real task to hit the scheduler
breakpoint, but only one core carried it, and on an idle guest
that core mostly ran the idle task, so the search flaked. Arming
every core instead floods the search: the idle loop enters the
scheduler constantly and each hit costs a GDB round-trip.

Have each core arm the breakpoint on its own thread from the
exception handler, so there are no cross-core register writes,
and drop the idle hits natively. A scheduler hit taken with IRQ
masked is the idle loop or an interrupt-context reschedule;
rewrite it to a benign exit and resume without waking the
debugger, so only a real task's hit reaches Frida.
2026-10-01 00:03:32 +02:00
Ole André Vadla Ravnås 74508ad276 barebone: Resume macOS Android-emulator cores
The emulator's gdbstub resumes only the current core on a
continue, so the other cores stay parked and the guest makes
no progress: run_until_schedule waits forever for a task that
never runs. Resume every vcpu from the emulator's own
cpu_resume so the whole guest advances.
2026-09-30 14:59:14 +02:00
Ole André Vadla Ravnås 0b65db9f08 barebone: Push macOS Android-emulator regs to vcpu
Setting the CPUState dirty flag did not reliably push gdb
register writes into the HVF vcpu, so an injected call ran
from a stale pc and never returned, hanging every
machine.invoke before the agent could start. Write x0-x30,
pc and cpsr straight to the vcpu with hv_vcpu_set_reg.
2026-09-30 14:59:14 +02:00
Ole André Vadla Ravnås 3e3e8226e9 barebone: Rewrite macOS Android-emulator shim
The shim's per-exit hooks ran under the JS lock, serialising
every core and starving the guest's own faults once all cores
were live. Move breakpoint programming and exception handling
into a CModule so the hot path runs lock-free, and hold the
mutable state in a shared buffer since a CModule's globals are
read-only.
2026-09-30 14:59:14 +02:00
Ole André Vadla Ravnås eae2380196 barebone: Skip Corellium probe on Android emulator
The Corellium probe runs "monitor info", which the emulator
answers with its entire HMP help text — pointless work on a
stub that is never Corellium. Skip it like the VZ and
Parallels stubs do, keeping the vCont and PhyMemMode probes
the emulator needs.
2026-09-30 14:59:14 +02:00
Ole André Vadla Ravnås df7eb6c805 barebone: Borrow one core to park on SMP
Stepping past an unwanted scheduler hit corrupts a translation block
under multi-threaded TCG, dropping later hits and hanging the search.
Borrow one core for it and leave the rest stopped.
2026-09-28 19:30:18 +02:00
Ole André Vadla Ravnås 127fbde6c6 barebone: Order commands to match interfaces 2026-09-28 18:05:04 +02:00
Ole André Vadla Ravnås 8420a6faaa barebone: Force-include stdint in agent bindgen 2026-09-28 17:39:21 +02:00
Ole André Vadla Ravnås b9e7573b5d barebone: Hold a named spawn in the agent 2026-09-28 17:39:21 +02:00
Ole André Vadla Ravnås 18db169e04 barebone: Read process details in the agent 2026-09-28 17:39:21 +02:00
Ole André Vadla Ravnås 8ec0c08622 barebone: Enumerate and spawn emulator apps 2026-09-28 17:39:21 +02:00
Ole André Vadla Ravnås 73c9ba0faa gdb: Drop a vestigial register read 2026-09-28 17:24:07 +02:00
Ole André Vadla Ravnås 845b686967 linux-kernel-image: Drop unused helpers 2026-09-28 17:24:07 +02:00
Ole André Vadla Ravnås 27d98acfe7 barebone: Inject the agent into SMP arm64 Linux
Read current from per-cpu memory rather than an allocating stub,
skip the idle task when borrowing one at schedule, and confine
each invoked call to its core so a sleeping call keeps its task.
Start the agent with all cores live for kthread_create. Treat any
state past SYSTEM_BOOTING as running, for kernels without
SYSTEM_SCHEDULING.
2026-09-28 17:22:35 +02:00
Ole André Vadla Ravnås 8bb5783cf1 barebone: Name Linux processes by their cmdline
Read the name from argv[0] rather than the truncated comm,
and skip kernel threads.
2026-09-28 00:38:57 +02:00
Ole André Vadla Ravnås 17fcaec4c8 barebone: Add kallsyms mining regression test
Open a kernel image named by FRIDA_TEST_KERNEL_IMAGE and resolve
a symbol list, reporting parse time and hit count.
2026-09-27 22:10:13 +02:00
Ole André Vadla Ravnås c89f1e8da9 linux-kernel-image: Mine 32-bit and VA48 kallsyms
Add finders for arm64 VA_BITS=48 bases, Rust symbol names past
80 chars, 32-bit absolute tables, and 32-bit relative offsets in
plain and percpu-absolute form.

Run the 32-bit finders only after the 64-bit paths fail, so
64-bit kernels are unaffected.
2026-09-27 22:10:09 +02:00
Ole André Vadla Ravnås 31c6d7d081 barebone: Return no applications on Linux
Linux has no application registry, so answer the enumerate command
with an empty list rather than letting it fall through to "Unknown
command".
2026-09-27 15:41:44 +02:00
Ole André Vadla Ravnås e872f8a99d barebone: Report Linux thread state
Fill in each thread's run state so ThreadDetails carries it. The
copy reads it from /proc/<pid>/task/<tid>/stat; the kernel half
reads task->__state. Both map to a GumThreadState.
2026-09-27 15:41:44 +02:00
Ole André Vadla Ravnås af22a4b53f barebone: Capture Linux thread registers
A copy runs in the target's userspace and cannot read a thread's
saved registers itself, so it asks the kernel half over the arena;
the kernel half reads the pt_regs off the task's kernel stack and
hands them back. find_task_by_vpid resolves any thread by id.
2026-09-27 15:34:43 +02:00
Ole André Vadla Ravnås 394c932f8d barebone: Keep the arm64 copy off huge pages
The copy's code is made executable a page at a time, which faults when
the kernel backs the mapping with a huge page: the leaf spans both
halves and has no page table to walk. A THP-enabled guest folds the
fresh mapping into one, so injection timed out. Ask for small pages
before faulting the copy in.
2026-09-26 22:41:24 +02:00
Ole André Vadla Ravnås b66a485f7c subprojects: Prepare for release 2026-09-25 13:15:40 +02:00
Ole André Vadla Ravnås b39661d192 subprojects: Bump outdated 2026-09-25 13:13:56 +02:00
Ole André Vadla Ravnås cabf50e98d barebone: Bump @types/frida-gum to 19.11.0 2026-09-25 13:13:35 +02:00
Ole André Vadla Ravnås 5748488e23 compiler: Bump @types/frida-gum to 19.11.0 2026-09-25 13:13:35 +02:00
Ole André Vadla Ravnås c1d3207caf subprojects: Bump outdated 2026-09-25 12:18:43 +02:00
Ole André Vadla Ravnås 9830a8db74 linux: Build the bootstrap exit via Arm64Writer
Emit the exit with the writer's put_ldr_reg_u64 and put_svc_imm and
Frida's LinuxSyscall.EXIT, instead of a hand-encoded MOVZ and SVC.
2026-09-25 12:05:29 +02:00
Ole André Vadla Ravnås ddde9cd975 linux: Drop kernel-injector comments
They restated what the code already says.
2026-09-25 10:49:33 +02:00
Ole André Vadla Ravnås 0d19d6719d linux: Reclaim the loader region on agent unload
Kernel-injected agents ride the shared RemoteAgent monitor, so their
unload policy and process status already reach on_agent_stopped. Carry
the loader region in the BootstrapResult and free it through the kernel
primitive instead of a ptrace CleanupSession.

The region is then reclaimed on an immediate unload, and via the cloned
result in a fork child that unloads too, while a resident agent or a
pending exec keeps it mapped.
2026-09-25 10:45:53 +02:00
Ole André Vadla Ravnås 9c3deaaded linux: Silence kernel-injector warnings
The request structs are written here and read only by the module
through the pointer, so valac flagged every field as unused. Reference
them from a static initialiser to settle it, and drop the unused
read_memory()/read_pointer() primitives.
2026-09-25 10:45:37 +02:00
Ole André Vadla Ravnås c6b8176c03 linux: Free the bootstrap stack and TLS
The kernel-assisted injector spawns a bootstrap thread on a dedicated
stack with a synthesised TLS block, both mapped into the target. The
thread exits once it has handed off to the agent's own pthread, but
its stack and TLS were left mapped, leaking 136 KiB per injection.

Reclaim them once the agent has settled, by when the bootstrap thread
is long gone. The loader region stays mapped, since the agent keeps
running from it.
2026-09-25 10:23:26 +02:00
Ole André Vadla Ravnås a95b627bf9 barebone: Exclude Android from emulator glue
Android defines LINUX too, so the guards pulled in
EmulatorInstrumentation, which meson compiles only for the desktop
(host_os == 'linux'). The Android build then broke on the missing
type. Narrow the guards to LINUX && !ANDROID to match.
2026-09-25 09:46:13 +02:00
Ole André Vadla Ravnås 5fae7f0dab fruity: Fix RSD port discovery on macOS 27
macOS 27 only reveals other processes' sockets to platform binaries
whose non-platform ancestors are all Developer ID or Apple signed.
Read the PCB list through /usr/sbin/sysctl, orphaned so launchd adopts
it, to not depend on how our own process was signed and launched.
2026-09-25 09:37:16 +02:00
Ole André Vadla Ravnås a1cf475801 subprojects: Bump outdated 2026-09-24 21:34:38 +02:00
Ole André Vadla Ravnås 3bfc37ebb4 barebone: Leave macOS emulator breakpoints to stub
Adopting breakpoints sets breakpoints_provided_externally, which drops
the Z packets to the stub. That suits Windows, whose shim watches the
debug registers itself, but starves the macOS shim: it learns each
address only from the stub's own cpu_breakpoint_insert and reports
stops through gdb_set_stop_cpu, so nothing ever gets planted.

The macOS inclusion was never meant: adoption was wired for Windows
and later fenced with macOS to fix the build. Restrict the call, and
the adoption machinery it needs, to Windows.
2026-09-24 21:31:08 +02:00
Ole André Vadla Ravnås c715362609 barebone: Base emulator shims on InternalAgent
Make EmulatorInstrumentation an InternalAgent, which brings the RPC
client, attach, and script lifecycle along. The shims expose RPC
exports instead of recv() handlers: allowPipePath on macOS and Linux,
setBreakpoints on Windows, whose hits now arrive as an on_event array.

Reach the host process through hub.resolve_host_session ("local"), as
Simmy does, rather than spinning up a private DeviceManager.
2026-09-24 21:31:08 +02:00
Ole André Vadla Ravnås 71c1d83659 barebone: Support the Android emulator on Linux
On Linux the emulator accelerates with KVM, whose gdbstub commits
register writes, inserts breakpoints, and handles vcpu exits on its
own. The shim thus only whitelists the pipe-over-vsock socket path,
and breakpoints go straight to the stub.
2026-09-24 21:31:08 +02:00
Ole André Vadla Ravnås 637d2d1103 barebone: Implement gum_memory_query_region()
Replace the gum_memory_query_protection() override, which now clashes
with Gum's own wrapper. Report the whole region, since the Interceptor
uses its extent to tell C stacks apart.
2026-09-24 15:08:30 +02:00
Ole André Vadla Ravnås 7157acae00 barebone: Accept thread flags when enumerating
Match the signature gum_barebone_enumerate_threads() gained upstream.
2026-09-24 15:08:30 +02:00
Ole André Vadla Ravnås 2327849c71 barebone: Give WinNtLayout a single kernel module
d30e8fbb narrowed the host to reading only the kernel, but left
WinNtLayout carrying a modules list of one and the host test still
expecting the old whole-list walk. Fold the layout down to a single
kernel module, and fail if it cannot be found rather than returning
an empty layout. The agent enumerates the rest in situ.
2026-09-24 13:17:19 +02:00
Francesco Tamagni f4d7a22aac fruity: Add force_transport=usbmux channel option
So that specific lockdown endpoints can be forcefully reached through
the usbmux transport even when a tunnel is available.
2026-09-24 11:55:26 +02:00
Akshit Singh dd3ed8907c socket: Tolerate peer vanishing before accept
on_websocket_opened() aborted the process through
assert_not_reached() when get_remote_address() failed, which
happens when the peer disconnects between the WebSocket handshake
and this handler running. Drop the half-open connection and carry
on instead.
2026-09-24 11:48:17 +02:00
Jiska Classen fe8934f868 darwin: Drop VM_PROT_COPY when mmapping the blob
frida_darwin_helper_backend_mmap() remapped the agent blob with
copy = TRUE and then reprotected it with VM_PROT_COPY. On iOS 27
VM_PROT_COPY produces a permanent VM entry that mach_vm_deallocate()
no longer removes but merely strips of permissions, while still
reporting success. The agent blob is allocated on every injection, so
frida-server retained a 40 MB blob per injection for the lifetime of a
process, until every attach failed with "unable to mmap ((os/kern) no
space available)".

The blob is remapped from our own heap, whose max_protection permits
writing, so a plain VM_PROT_READ | VM_PROT_WRITE reprotect always
succeeds and VM_PROT_COPY was never needed here.

Tested on an SRD with iOS 27.0 beta 7.
2026-09-24 11:15:20 +02:00
Ole André Vadla Ravnås d27e5c51cc barebone: Restore page protection before freeing
protect_unless_already asks the allocator what it grants, not what the
pages currently hold. The allocator grants read-write, so restoring
read-write on the way out returned early and did nothing -- while the
text pages had been made read-only during injection.

set_memory_ro also updates the direct-map alias, so freeing them that
way hands the page allocator a read-only page, and whoever gets it next
faults clearing it. Over eight runs: fourteen oopses before, none after.
2026-09-23 14:08:13 +02:00
jiska c3d8d94ded darwin: Preserve suspension across policy softening 2026-09-23 00:17:08 +02:00
Ole André Vadla Ravnås f53ffd72c8 gdb: Forget the selected thread when the stub stops
The stub picks its own thread when it reports a stop, so remembering
which one was asked for last is only good until the next stop or
resume. Held across one, the group packets skip the selection they
need and answer for, or write to, whatever the stub chose.

Clear it on both transitions. Under load, where a stop lands between
the selection and its use far more often, this takes the failure rate
back from 47 to 25 percent.
2026-09-23 00:13:00 +02:00
Ole André Vadla Ravnås 2ef1277ccc subprojects: Bump outdated 2026-09-23 00:02:42 +02:00
Ole André Vadla Ravnås 151a4587a6 submodules: Bump releng 2026-09-23 00:02:41 +02:00
Ole André Vadla Ravnås 4141a1d0f4 subprojects: Bump outdated 2026-09-22 23:47:04 +02:00
Ole André Vadla Ravnås cfd234c65a submodules: Bump releng 2026-09-22 23:47:04 +02:00
Ole André Vadla Ravnås a84cf817a7 barebone: Give the 32-bit agents their vsock slots
The pipe hostlink's socket calls were only listed in the 64-bit
scripts, so x86 and arm failed to link. List them there too, and
let the 32-bit shim reach fget_task.
2026-09-22 20:55:03 +02:00
Ole André Vadla Ravnås 4769387ac2 barebone: Fix the 32-bit x86 Linux agent build
The shim supplies ioremap, pci_irq_vector and ktime_get_real_ts64
itself, so declaring them again as patched symbols clashed. Reach the
latter two through accessors that hand back the same shape on every
architecture, and give x86 its own map_io, the shim having no nocache
fallback.

kernel_write is a pointer there and an integer elsewhere, so split the
two callers as well, passing what the other architectures already pass.
2026-09-22 20:55:03 +02:00
Ole André Vadla Ravnås b76ba67971 barebone: Space the shim's pointer declarations
Put a space on either side of the star, as the rest of the file does.
2026-09-22 20:55:03 +02:00
Ole André Vadla Ravnås ad865bf14d barebone: Drop the winnt resume block thunk
Its record went away when faults started going through the handler,
and nothing calls it.
2026-09-22 20:55:03 +02:00
Ole André Vadla Ravnås da0aae3579 barebone: Gate the Linux hostlink pipes
Both modules reach for native and layout, which only exist in an
injected build, so a kernel module failed to compile. Declare them
alongside the rest of that flavour.
2026-09-22 20:55:03 +02:00
Ole André Vadla Ravnås fc692bb122 gdb: Trust the advertised binary write support
The stub tells us up front whether it takes X packets, so keep the
flag with the rest of the connection state and drop the degrade on
failure, which only ever masked a stub lying about what it accepts.
2026-09-22 17:29:51 +02:00
Ole André Vadla Ravnås d44dba6f95 tests: Wait for the Fruity device to appear
The manual tests looked the device up with no timeout, which means
not waiting at all, so they failed before USB discovery had run.
2026-09-22 17:22:07 +02:00
Ole André Vadla Ravnås a6549a6692 gdb: Ask for stop info until it arrives
State starts out stopped, so the loop guarding the connect-time
request never ran and the first stop was never asked for. Loop
until the stop info itself lands.
2026-09-22 17:22:03 +02:00
Ole André Vadla Ravnås 19b4af7c04 gdb: Write binary only when the stub allows it
Apple's debugserver does not advertise binary-upload, and answers X
packets by writing the ",<len>:" header into target memory ahead of
the payload instead of rejecting them, so the hex fallback never
fired and the injected code was corrupted.
2026-09-22 17:21:57 +02:00
Ole André Vadla Ravnås 8a0564bcd2 fruity: Chain LLDB.Thread to base constructor
The id and name properties lost their setters when construct-only
interface properties went away, so passing them to Object() left
both null and packet building crashed on the first thread query.
2026-09-22 17:21:48 +02:00
Ole André Vadla Ravnås 3d43d64c49 subprojects: Bump outdated 2026-09-22 15:57:08 +02:00
Ole André Vadla Ravnås c6fac8cbca submodules: Bump releng 2026-09-22 15:57:07 +02:00
Ole André Vadla Ravnås 62c666492b prospero: Add backend for the PS5
The console is FreeBSD underneath, so it reuses the FreeBSD host
session, but nothing else carries over: it refuses to exec an unsigned
binary, hands out no password database, and numbers a jailed process's
modules from 0x2000.

Spawn a program by exec'ing a stock system app and mapping the real
ELF over it, and spawn an app by asking the shell to launch it while
kqueue watches for the fork. Drive the target through the raw ptrace
syscall, borrowing the debugger authid, and hand the agent kernel
access through a pair of overlapped sockets. Enumerate apps from the
metadata the console stores on disk, icons included.

Link the agent against crt1 so it doubles as a payload, and reach it
over a socketpair rather than a named pipe, which the console does not
support.
2026-09-22 15:51:06 +02:00
Ole André Vadla Ravnås 302d0dcd59 freebsd: Stride process records by their own size
The kernel reports each record's size up front, and it need not match
the struct the headers describe, so dividing the reply by sizeof left
the list empty.
2026-09-22 15:51:06 +02:00
Ole André Vadla Ravnås b81b48c376 debugger: Drop construct-only interface properties
valac routes such a property through the interface setter, which it
defines but never declares, so callers in other units end up with an
implicit declaration. Give the implementations their own storage and
expose only a getter.
2026-09-22 15:51:06 +02:00
Ole André Vadla Ravnås f043b61f02 barebone: Rename local colliding with a macro
Compilers targeting Linux predefine linux, so the generated C did not
build. Name it config instead.
2026-09-22 15:51:06 +02:00
Ole André Vadla Ravnås b7d36ce56a barebone: Guard emulator breakpoint adoption
The field only exists on Windows and macOS, so building anywhere else
failed. Guard the use the way its declaration and close() already are.
2026-09-22 15:51:06 +02:00
Ole André Vadla Ravnås 367e1d7e99 Share the non-blocking waitpid helper
Move ChildProcess.wait_for_next_stop, WaitResult, WaitKind and
PosixStatus into lib/base so other POSIX backends can wait without
blocking the main loop. The ptrace-specific parts of a stop stay in
the Linux backend, as PtraceStatus.
2026-09-22 15:51:06 +02:00
Ole André Vadla Ravnås 1d2bc226e7 Fix pointer spacing in declarations
Write the type, a space, the asterisk, a space, and then the name,
the way the rest of the codebase does. Vala and VAPI sources follow
the same rule as C.
2026-09-22 15:51:06 +02:00
Ole André Vadla Ravnås 71b9ee120d fruity: Refresh RSD snapshot on service miss
The RemoteXPC handshake snapshots the services that exist when the
tunnel opens. Services published later, such as those brought in by
mounting a Developer Disk Image, never show up.

Resolve services through the tunnel, so a miss reopens the RSD
connection and retries against a fresh snapshot.

Kudos to @hsorbo for reporting.
2026-09-22 15:22:25 +02:00
Ole André Vadla Ravnås 1f965e5282 gdb: Read and write the thread that was asked for
Reading or writing the whole register file sent its packet without
saying which thread it meant, so both landed on whichever one the stub
had selected last. A single register says what it means, through the
thread suffix or by selecting first; the group packets did not, and on
a target with more than one processor they answered for, and wrote to,
the wrong one.

Select before the group packets too, remembering the choice so the
landing loop does not pay for a round trip it does not need.
2026-09-22 13:05:00 +02:00
Ole André Vadla Ravnås 115245595a gdb: Give the stub's answers a deadline
A query sends its packet and waits with nothing but a cancellable, so
an answer that never arrives takes the session with it. The waits that
follow a stop were bounded one at a time; the path they all run
through was not.

Carry a deadline on each pending response and sweep them from a single
timer, which starts with the first query outstanding and stops once
none are. A timer per query is far too dear: the landing loop asks
after every arrival, and paying for a timeout source each time takes
the instrumentation phase from 2.6 to 18 seconds.
2026-09-22 12:52:53 +02:00
Ole André Vadla Ravnås 7d4afc0b59 barebone: Repair the vcpu that trapped
QEMU reads registers back from the first vcpu only, so the shim's
hflags repair never reached the others. The gdbstub then reported
them in 32-bit form, and a landing on any other processor was
compared against a truncated stack pointer and rejected. Under load
that spun until the borrowed task migrated back, costing twenty
seconds or the call's own deadline.

Pull the trapped vcpu's registers through QEMU's reader before
reporting it, and repair it there. Its CPUState is taken from the
register the run loop carries it in, learned by matching against
the one vcpu QEMU does read.

Rejections on the other processors go from 240 to none, and no run
is slow any more: the instrumentation phase averages 5 s and peaks
at 6.5 s, against 7.7 to 16.7 s and peaks of 21 to 24 s.
2026-09-21 23:48:52 +02:00
Ole André Vadla Ravnås 15814adcf5 kallsyms: Read the whole range of kernels
Only four of the thirteen emulator kernels parsed, and the ones that
did took up to forty seconds. Both failings came from the same place:
each part of the table was found by scanning the whole image for it in
turn, against a layout only the middle generation of kernels has.

Find the token table first, since it ends the region the names and the
addresses live in, then the percpu prefix, and take the names in one
backward pass that records where every run of symbols begins. Kernels
storing offsets from a relative base, and those keeping percpu symbols
absolute, fall out of that same pass. All thirteen now parse, in 92 to
246 ms.
2026-09-21 18:48:08 +02:00
Ole André Vadla Ravnås 3b6aa6b315 gdb: Bound the wait for the stub's first stop
Connecting asks for stop info and waited for the answer forever, so a
stub that never sends one takes the whole connection with it before
anything else has run.

Ask again, bounded, and give up with a timeout.
2026-09-21 08:39:36 +02:00
Ole André Vadla Ravnås 9c907a9fed barebone: Give a guest call a deadline
The wait for a call to land runs until the return address is reached
on the stack we supplied. The rest of the kernel passes through that
address too, so the loop keeps going, and a call that never returns
keeps it going forever. Every backend waits this way.

A minute is far past any call that is coming back: on a machine that
is not starved, injection finishes inside three seconds.
2026-09-20 09:19:55 +02:00
Ole André Vadla Ravnås 4b717bccbd gdb: Ask again when a stop goes missing
Where breakpoints come from outside the stub, the stop that follows a
hit is reported out of band and can go missing, leaving nothing on the
wire to wait for. Three waits assumed one always arrives: for an
exception, for a stop, and for a step.

The step is the costly one. Stepping over a breakpoint moves a single
processor with vCont;s and holds every other one stopped, so a
completion that never comes freezes the machine, and the guest looks
deadlocked from the inside.

Wait in bounded rounds and ask again, giving up with a timeout instead
of never returning. Stubs reporting their own breakpoints are
unaffected.
2026-09-19 16:49:40 +02:00
Ole André Vadla Ravnås 873d90c4d5 barebone: Report breakpoint hits faithfully
The shim claimed every debug exception before checking whether one of
the planted registers had matched. Ones that match none are the
guest's own, single steps included, and the stub itself steps to get
past a breakpoint.

Hits were also reported at most once every 50 ms per address. The call
mechanism continues past every arrival at the return address until it
finds the one on the stack it supplied, so a second arrival inside the
window went missing.
2026-09-19 16:47:09 +02:00
Ole André Vadla Ravnås ea4f15e528 barebone: Give the hostlink device its IRQ line
Nothing claims the emulator's virtio-serial controller, so the kernel
leaves its interrupt line at zero and the chipset keeps the link off.
Routing asked the kernel for the line and unwrapped the answer, which
panicked the guest.

Pick the line instead: take what config space names, else share one
another link already carries, and write it back so the device and the
handler agree. Message interrupts are silenced on the way in, so that
line is the one the device raises.
2026-09-19 16:47:09 +02:00
Ole André Vadla Ravnås f0f272a47c barebone: Carry the hostlink over the QEMU pipe
The guest kernel's own driver owns the virtio-serial device and holds
its interrupt unshared, so an injected agent gets none. The emulator's
pipe carries adb the same way and hands out a channel per open, so the
agent names the pipe:unix service on it and the emulator joins that
stream to the socket frida-core listens on.

That listener is a UNIX socket on Windows as well, so what stood behind
the guard is now built everywhere.
2026-09-18 09:36:55 +02:00
Ole André Vadla Ravnås b5485d71e2 agent: Ask the kernel for the hostlink line again
The firmware leaves the config byte at zero on this platform, so taking
it named no interrupt at all. Keep the guard that ignores a line shared
with another device until there is a transport to wake.
2026-09-18 04:12:02 +02:00
Ole André Vadla Ravnås 23f0953088 barebone: Hand the agent per-CPU slots verbatim
A per-CPU symbol names an offset from the processor's own base, not an
address in the image, and the agent reads it through gs. Adding the
kernel base gave it one that is neither, so current came out as rubbish
and waking a task on it faulted in interrupt context.
2026-09-18 03:39:34 +02:00
Ole André Vadla Ravnås 676e6d6058 agent: Take the hostlink interrupt on x86
Vector zero is the message-signalled interrupt the guest's own virtio
driver holds, and it does not share, so take the line the firmware
programmed instead. That one is shared, and arrives before there is a
transport to wake, so leave it alone until there is.
2026-09-18 02:21:59 +02:00
Ole André Vadla Ravnås 6d14d92d03 agent: Map I/O where ioremap is a macro
Kernels before 5.6 export ioremap_nocache and nothing named ioremap, so
the slot stayed empty and mapping a device called through it.
2026-09-18 02:06:12 +02:00
Ole André Vadla Ravnås f97166b8ce agent: Take the heap from vmalloc
The slab allocator cannot hand out 32 MiB in one piece, so asking it
for the heap fails and the panic that follows allocates as well.
2026-09-18 02:06:06 +02:00
Ole André Vadla Ravnås 55a4a40aa5 agent: Name the kernel slots x86_64 links against
The vsock hostlink and the socket primitives it calls were given slots
on arm64 only, so the x86_64 agent named symbols nothing defined and
would not link.
2026-09-18 02:05:52 +02:00
Ole André Vadla Ravnås bf1f308443 barebone: Break on the Windows emulator
WHPX reports no breakpoint trap to the stub, so the shim arms the
partition's exception exits and watches through the debug registers.
Arming touches partition state, which deadlocks from the run path, so
it happens on the script's own thread; a processor already inside a run
is cancelled to pick up what was planted since.
2026-09-18 02:05:33 +02:00
Ole André Vadla Ravnås dec5561943 gdb: Let breakpoints come from outside the stub
A stub that plants none of its own can have them provided for it: the
client publishes what it wants watched and is told when one is hit.
2026-09-18 02:05:33 +02:00
Ole André Vadla Ravnås 39fee15e3f barebone: Relax CFI while the agent is in
Android builds the kernel with Clang CFI, and an indirect call into the
agent belongs to no module, so the scheduler calling in panics. Make the
check itself return, not its failure path: the caller does not expect
that one to come back.
2026-09-18 02:05:26 +02:00
Ole André Vadla Ravnås 1f9410251e barebone: Give the agent a stack of its own
A kernel stack is 16 KiB and the agent wants far more than the borrowed
task has left, so it runs on memory taken for the purpose.
2026-09-18 02:05:18 +02:00
Ole André Vadla Ravnås 22323e48b9 barebone: Borrow a task on x86
The scheduler is reached by making it spin where it stands and catching
whoever arrives, since stopping the guest on every arrival costs more
than the arrivals are worth.

Reading current takes the per-CPU slot rather than a probe that has to
be allocated first, the allocator being what needs a task to begin
with. Page protection goes through the kernel's own set_memory_*, which
reaches pages the debugger cannot walk.

SYSTEM_RUNNING is 2 on some kernels and 3 on others, so test for a
system that is past scheduling instead.
2026-09-18 02:05:10 +02:00
Ole André Vadla Ravnås ee827b9e9a barebone: Bound the kernel a sampled pc can be in
The landing sweep took any aligned address below the pc whose banner
matched, so a dormant copy of the image elsewhere in memory won the
race. Require the pc to fall inside the span the symbols cover.

Per-CPU symbols carry their own small values rather than addresses in
the image, so leave those alone when rebasing.
2026-09-18 02:04:28 +02:00
Ole André Vadla Ravnås d7167c5a16 barebone: Disarm PatchGuard on x86-64
Hooking the dispatcher is noticed, and the guest bug-checks within a
minute of the agent leaving. Sweep the pool around our own allocation
for the deferred calls that do the checking, the same way arm64 does,
and clear the context each one carries.

Skip whole regions the page tables say are absent rather than asking
the kernel about every page, which keeps the sweep near a second.
2026-09-18 01:24:34 +02:00
Ole André Vadla Ravnås 0697d4e19d barebone: Report winnt faults through the handler
The fault vectors are read-only on Windows 11, and a table of our own
is reloaded from under us, thus the gates cannot be taken over. Replace
the routine the vectors dispatch to instead, which the gates lead to in
two hops, and take the fault from the trap frame it is given.
2026-09-18 01:24:25 +02:00
Ole André Vadla Ravnås 99e62dddf4 barebone: Walk the winnt page tables as mapped
Windows randomises where it maps the page tables into themselves, thus
the address the walk assumed has been wrong since Windows 10 and every
lookup read memory that is not there. Find the self-map by the entry
that names the table it lives in.

The kernel maps its own text with large pages, which the walk skipped
on the assumption that such a mapping is already writable. It is not,
so apply the change to the entry that maps it.
2026-09-18 01:24:25 +02:00
Ole André Vadla Ravnås 110fc7c96c barebone: Find winnt guest state in the agent
The host would sweep the guest for the loaded module list and the
process list head, spending a round trip per candidate on data the
agent can walk in a few loads.

Walk the loaded module list from the note the host leaves, which the
process copies already do through the same entry layout, and find the
process list head from the system process on first use.
2026-09-18 01:22:44 +02:00
Ole André Vadla Ravnås 0ba1fc9ed0 barebone: Resolve the winnt layout in a second
Fifteen seconds went on three sweeps that a stub pays for one round
trip at a time.

The syscall entry MSR already points into the kernel image, which beats
hunting the stack for a pointer to it, and the image is mapped on large
pages, thus sweeping on that boundary finds it in a handful of probes
rather than a hundred. Carry the export directory out of the sweep
instead of reading and parsing it again for the module list and once
more for the symbols.

That leaves the process list head, which cost nine seconds of probing
and which injection never reads; the agent knows how to find it.
2026-09-18 01:22:44 +02:00
Ole André Vadla Ravnås d490a5f923 barebone: Stop calling the winnt pool executable
Windows 11 hands out non-executable pool, thus claiming it is already
executable skips the protection change the agent needs and the guest
bug-checks on the first instruction. Let the host protect the pages,
which it can now that the console reaches physical memory.
2026-09-18 01:22:44 +02:00
Ole André Vadla Ravnås 5804ad8b22 barebone: Land calls on a cold routine on x86-64
invoke() detects its call returning by watching the address the thread
was hijacked at, which on winnt is a system service the whole kernel
runs through. With a spin standing in for a breakpoint that wedges
every processor, so point x86-64 at the bug-check routine the way arm64
already does.
2026-09-18 01:22:44 +02:00
Ole André Vadla Ravnås 22d5528285 barebone: Release processors parked on a spin
A spin holds whoever arrives first. A caller that rejects an arrival --
because it wants its own call returning on its own stack -- would then
wait on a processor that can never leave, and every other processor
piles up behind it.

Lift the spins before resuming, let the guest run for a moment, and arm
them again, which is what a breakpoint gives a caller for free.
2026-09-18 01:22:44 +02:00
Ole André Vadla Ravnås f732450cad barebone: Address physical memory on VirtualBox
The console takes a physical address with a prefix of its own, thus it
can serve the page-table walking that changing protection needs. A
staged write still goes through virtual memory, because writegstmem
knows no other kind.
2026-09-18 01:22:44 +02:00
Ole André Vadla Ravnås 44248c1ad2 barebone: Cache what the console reports
Halting to read registers costs round trips, thus hold on to what a
stop reported until the guest runs again, and fetch the wider set only
when a caller asks for a register outside it.

Write back only the registers whose value changed, which keeps the
console from setting ones it cannot.
2026-09-18 01:22:44 +02:00
Ole André Vadla Ravnås e7b20adfad barebone: Speed up the VirtualBox console
One command per round trip made the console unusable for bulk work: the
4.7 MB agent took eight minutes to upload and the kernel layout fifteen
seconds to resolve.

Batch reads, and stage a large write through a file that writegstmem
loads in one go, which the console reads in chunks of its own and thus
wants a whole number of them. Read the program counter, stack pointer
and code selector of every processor in one turn rather than every
register of each, and leave out the ones that belong to the processor
rather than the thread, which a caller must not restore onto whichever
processor a call lands on.

Wait for the console to go quiet after halting, because each processor
reports its halt on its own, long after the command that asked for it
was acknowledged.
2026-09-17 21:49:51 +02:00
Ole André Vadla Ravnås 52bfb579ba barebone: Add VirtualBox console stub flavor
VirtualBox only lets one debugger attach, and its GDB stub cannot do
what the winnt flavor needs. Drive its native debugger console instead,
selected as the "virtualbox-console" flavor.
2026-09-17 21:47:55 +02:00
Ole André Vadla Ravnås 99bad7b7d3 barebone: Implement Debugger on VirtualBox
VirtualBox on NEM sets breakpoints that never fire, but it does let
us write to resident kernel code. So stand a breakpoint on a jump
to itself: whichever thread arrives parks there, and the debugger
notices by polling the program counter.
2026-09-17 15:16:26 +02:00
Ole André Vadla Ravnås 7c158e692f barebone: Add a VirtualBox console client
Its GDB stub cannot write memory, and on NEM its breakpoints never
fire. The console can do both, but only one debugger may attach at
a time, so this has to stand on its own.

Framing is the awkward part: each halting CPU announces itself with
a prompt of its own, echo is not implemented, and a state change
discards pending input. So a command is followed by one the console
does not know, and read up to the complaint that names it.
2026-09-17 15:16:26 +02:00
Ole André Vadla Ravnås 182963a8ec barebone: Open a named pipe for serial transport
The serial transport assumed a Unix socket and refused to work on
Windows, where a guest's serial port shows up as a named pipe.
Give lib/base an IOStream over an overlapped handle and reach the
pipe through it.
2026-09-17 15:16:26 +02:00
Ole André Vadla Ravnås d30e8fbbb2 barebone: Read only the kernel module on the host
Walking the whole module list and every module's exports over the
wire cost the better part of a minute, and served nobody: the host
needs the allocator's two symbols, and the rest is for the agent,
which can read it in the guest for free. Pass the list head along
instead.
2026-09-17 15:16:26 +02:00
Ole André Vadla Ravnås 746a297542 barebone: Find the winnt kernel without the PCR
A stub that withholds the GS base leaves the kernel image as the
only way in, which is also arm64's situation. Decide on what the
stub exposes rather than on the architecture, and seed the scan
from the program counter, which in kernel mode already sits inside
an image.
2026-09-17 15:16:26 +02:00
Ole André Vadla Ravnås 05c2df24ac barebone: Add VirtualBox stub flavor
Its GDB stub answers one request at a time, and greets a client
with a stop packet per CPU. Serialise requests and let the burst
settle before the handshake starts.
2026-09-17 15:16:26 +02:00
Ole André Vadla Ravnås 169580b22a barebone: Read a present export directory at once
Reading it page by page cost a round trip per page. Try the whole
range first and fall back to pages only when part of it is out.
2026-09-17 15:16:26 +02:00
Ole André Vadla Ravnås 51153487f9 barebone: Rename $gdb to $debugger
The script runtime reaches a Debugger now, whatever speaks to the
target underneath.
2026-09-17 15:16:26 +02:00
Ole André Vadla Ravnås 218ae7b3ea gdb: Extract Debugger interface from client
Barebone talked to a GDB.Client throughout. Name what it actually
needs instead, so a hypervisor with a debugger of its own can serve
it. GDB.Client, Thread and Breakpoint implement the interface;
TargetArch, the run state and the breakpoint kind move out of
Frida.GDB and into Frida.

continue becomes resume: as an interface method it would name a
field in the generated C, where continue is a keyword.

Raw packets stay with the protocol that has them, reached through
a cast. Physical addressing was a capability behind a packet, so it
becomes one.
2026-09-17 15:16:26 +02:00
Ole André Vadla Ravnås b27d371df6 gdb: Drop FRIDA_GDB_TRACE 2026-09-17 15:16:26 +02:00
Ole André Vadla Ravnås d7eb1a4df1 gdb: Let a stub ignore stops while connecting 2026-09-17 15:16:26 +02:00
Ole André Vadla Ravnås 374bf7c896 gdb: Let a stub opt out of pipelining 2026-09-17 15:16:26 +02:00
Ole André Vadla Ravnås 04d78bb826 gdb: Handle stop notifications one at a time 2026-09-17 15:16:26 +02:00
Ole André Vadla Ravnås e43c01d159 gdb: Bound the feature read by packet size 2026-09-17 15:16:26 +02:00
Ole André Vadla Ravnås 149f005446 gdb: Send supported features with qSupported 2026-09-17 15:16:26 +02:00
Ole André Vadla Ravnås 1fbded1079 api: Publish the kernel image types
The types the image config names were not in the public API at all:
the generator takes it from the sources it is pointed at, and the two
new ones were not among them. A binding generator reading the gir got
as far as the config and then had nothing to resolve:

    AssertionError: unable to resolve type XnuKernelcache

Naming them there brought out a second leak. Vala keeps a virtual
method in the class struct whatever its visibility, so the internal
to_list() carried Gee into the public gir, which nothing outside the
library can resolve either. It answers for both kinds itself now, and
no longer needs to be virtual.
2026-09-17 15:09:27 +02:00
Ole André Vadla Ravnås d3fcc932c5 barebone: Take the kernel image as an object
An image was named by path, and read again by everyone who wanted
something from it: the Linux kallsyms parse sat behind the barebone
gate where nothing else could reach it, and the XNU kernelcache was
read and parsed twice over, once for the relocation and once for the
layout. A caller that wanted to know what a kernel names before
launching anything -- which transport a guest supports, say -- had no
way to ask.

So the images become objects the caller holds:

    LinuxKernelImage.from_blob (blob)
    LinuxSystemMap.from_blob (blob)
    XnuKernelcache.from_blob (blob)

Both Linux kinds answer has_symbol() and try_find_symbol(), and the
config carries whichever the caller has rather than a path, so an
image and a map can no longer both be supplied. Nothing reads the
filesystem to find out what it was handed: the kind is named, not
sniffed.

The kallsyms tables are located up front and the names are decoded
only when asked, so a lookup walks the tables comparing in place
instead of building the whole symbol list to answer one question.

FRIDA_BAREBONE_CONFIG names the kind it has:

    "image": { "type": "xnu-kernelcache",    "file": "..." }
    "image": { "type": "linux-kernel-image", "file": "..." }
    "image": { "type": "linux-system-map",   "file": "..." }
2026-09-17 14:43:48 +02:00
Ole André Vadla Ravnås e68cdcd229 barebone: Guard a field Windows never uses
The pipe-vsock service is only ever set where there is one, so a
Windows build warned that it is never used.
2026-09-17 14:34:27 +02:00
Ole André Vadla Ravnås 69af5a12a8 resource-compiler: Terminate blobs on MSVC too
Only the assembler path ended a blob with a NUL, so on the Microsoft
toolchain the last one in a section ran into whatever followed:

    GLib-CRITICAL: g_variant_new_string(): requires valid UTF-8

Terminate there as well, and skip it for anything that is plainly not
a string: the blob's own magic says whether it is a PE, an ELF or a
Mach-O, which also tells the assembler path what to align, so neither
has to guess from the extension.
2026-09-17 14:34:27 +02:00
Ole André Vadla Ravnås 61b3e3b8f6 barebone: Carry QEMU's channels over TCP
QEMU's Windows pipe chardev writes but never reads, so the QMP
handshake cannot complete over one; the hostlink's chardev would have
failed the same way. Both move to a loopback port.

With server,nowait QEMU no longer holds the machine still waiting to
be connected to, so the handle handover goes away with it.
2026-09-17 14:34:27 +02:00
Ole André Vadla Ravnås 637383261e barebone: Reach QEMU's hostlink on Windows
QEMU has no UNIX socket to listen on there, so the QMP channel could
not be opened at all and the config check rejected every address.
Serve both it and the hostlink's chardev over a named pipe.
2026-09-17 14:34:27 +02:00
Ole André Vadla Ravnås da558f706d barebone: Make the emulator's WHPX gdbstub usable
whpx_get_registers() never recomputes env->hflags, so QEMU believes
the guest is a 32-bit CPU outside long mode: registers come back
truncated to their low half, and every kernel address fails to
translate with E14. Recompute the long-mode bits after each sync.

Aimed rather than pinned: the function carries its own __func__
string, and the fields it writes give away the register file.
2026-09-17 14:34:27 +02:00
Ole André Vadla Ravnås 776a3a0e58 barebone: Sweep the x86 KASLR window
Stepping back from the PC the guest was caught at only reaches the
base when that PC is in the kernel's own text. x86_64 slides the base
across a gigabyte, so sweep the window -- 512 landings -- when
stepping back has not found the banner.
2026-09-17 14:33:23 +02:00
Ole André Vadla Ravnås 442293e122 barebone: Read an absolute-percpu kallsyms table
x86 kernels set CONFIG_KALLSYMS_ABSOLUTE_PERCPU, whose offsets are
signed and therefore do not ascend, so the ascending-run search cannot
find the table. Anchor on the layout instead: relative_base, then
num_syms, then the names. The run search is still tried first.

Recovers all 140881 symbols from an android-30 x86_64 kernel.
2026-09-17 14:33:23 +02:00
Ole André Vadla Ravnås dbe35e5247 barebone: Unpack an LZ4-packed kernel payload
Android builds its x86 kernels with CONFIG_KERNEL_LZ4, so the bzImage
payload sits behind the legacy LZ4 frame and the kallsyms scan found
nothing to read. Inflate it first.
2026-09-17 14:33:23 +02:00
Ole André Vadla Ravnås af0be008d5 gdb: Carry a built-in i386-64bit feature document
The emulator's stub names i386-64bit.xml in its target.xml and then
declines to serve it, as it already does for the aarch64 documents.
Carry our own so an x86_64 guest can be attached.
2026-09-17 14:33:23 +02:00
Ole André Vadla Ravnås 09a425f107 barebone: Instrument the emulator on macOS only
The gdbstub quirks worked around here are macOS ones, so skip the
instrumentation elsewhere until another OS is shown to need it.
2026-09-17 14:33:23 +02:00
Ole André Vadla Ravnås d3196950ff barebone: Fix copy reply wakeup on Linux
Declare kernel_read with its real argument order (buf, count, pos)
and call it that way, so wait_for_a_word reads the copy's says pipe
instead of faulting on a null buffer. The reply doorbell exited at
its first read on every architecture, and replies only reached the
host when the kernel half happened to still be polling, so
create_script timed out at random.

Wake the loop through nudge_the_loop, matching the hostlink doorbell,
so a reply that lands between the work check and the sleep is not
lost.
2026-09-17 12:29:00 +02:00
Ole André Vadla Ravnås e91795569a barebone: Separate with a single blank line
Two or more in a row say nothing the one says.
2026-09-17 01:24:49 +02:00
Ole André Vadla Ravnås f8d14d0b1d barebone: Catch winnt arm64 faults at the handler
The agent gave every CPU a vector table of its own and pointed VBAR at
it. Windows points it back from three places: a hardcoded pair of
instructions when a CPU starts, the architectural state a CPU resumes
from, and the return to user mode. A CPU that had been sent back thus
sent the script's fault to Windows, which bugchecked the machine on an
exception nobody handled, and the host saw only a command that timed
out.

Replace the handler the vector entries call instead. Every table leads
there, so no CPU can be sent back behind our back, and the trap frame it
receives carries the state to read and the state to resume with.
2026-09-17 01:24:49 +02:00
Ole André Vadla Ravnås 6d206eedcc subprojects: Bump outdated 2026-09-16 19:28:37 +02:00
Ole André Vadla Ravnås 2a1ca2d126 barebone: Poll handles in the winnt copy
The barebone GLib reaches a descriptor only through the poll func the
embedder installs, and the copy installed none, so a handle a script
opened was never waited on.

Wait on the handles with NtWaitForMultipleObjects, and keep the event
the kernel half sets among them, so the one loop serves both. Every
kernel gains a poke that breaks the wait, since a token says nothing
to a thread parked in the object manager.
2026-09-16 19:24:22 +02:00
Ole André Vadla Ravnås e640d2b5a2 barebone: Wake every winnt sleeper at once
The kernel half sets one event to tell a copy that something arrived,
and that event was auto-reset, so a single set released one waiter.
The copies have more than one: the loop, and any thread held back by a
full ring. The one left asleep was never told again, and a command sat
unserved for as long as the process lived.

Make it a notification event, which releases all of them, and let the
copy reset it once it wakes.
2026-09-16 19:22:46 +02:00
Ole André Vadla Ravnås 33a17a1e74 barebone: Enumerate in the winnt arm64 test
The test that says it sees modules and threads sent two constants and
asserted they came back, thus it proved only that a script loads in the
copy. Enumerate the modules of the process, an export of ntdll, and the
threads, and assert on what comes back.
2026-09-16 19:22:46 +02:00
Ole André Vadla Ravnås 5abdfe6f5f barebone: Test an in-process REPL in a live guest
Attach to a real guest process, load a script and check that an
evaluate RPC replies, covering the userspace injection path that
the kernel/pid-0 tests miss.
2026-09-16 19:16:19 +02:00
Ole André Vadla Ravnås d9779bd956 barebone: Poll the copy's wakeup in its own loop
Drop the copy's doorbell thread. Its single GMainContext now
polls a wakeup pipe through an installed poll func, so one loop
waits on descriptors without a helper. Resolve that pipe against
the copy's own task rather than the command thread's, since the
two do not share a descriptor table, and fence the ring publish
before waking so the copy never reads a stale head.
2026-09-16 19:16:19 +02:00
Ole André Vadla Ravnås 2098ff6925 barebone: Let the copy request executable memory
The copy runs in a domain that cannot make its own memory
executable, so gum's protect hands the range to the kernel half
over the arena and waits. The kernel half relaxes it below the
LSM and answers back.
2026-09-16 19:16:19 +02:00
Ole André Vadla Ravnås 3efec68374 barebone: Map the Linux copy's code past execmem
A hardened GKI kernel denies executable mappings to the copy's
domain and verifies indirect calls (kCFI). Map the image writable
and turn its code half read-execute by flipping the page tables
below the LSM, reaching the pte callback through a type-id
trampoline. Keep gum W^X so it never asks for RWX. Skip the sched
tracepoint probes until they carry CFI-safe trampolines.
2026-09-16 19:16:19 +02:00
Ole André Vadla Ravnås 1fcdd7a016 barebone: Correct arm64 block splitting
A level-3 descriptor parses as a block, so the walk treated every mapped
page as one to split; only split below level 3. Split the blocks under
the flush stub too, since it can land in one the agent's range did not
cover. Resolve every table's address before splitting any block, so a
table sitting in an already-split block still resolves through the
original mapping.
2026-09-16 18:55:13 +02:00
Ole André Vadla Ravnås ef431465b9 barebone: Wait for userspace before injecting
A task reaches the scheduler with interrupts on long before userspace
exists, and run_until_schedule borrowed the first such task -- during
early boot, when the kernel's allocators are not yet fit to hand out an
agent. execmem then returned huge-block-mapped, overlapping memory, and
the injection corrupted the guest.

Gate the borrowed task on system_state having reached SYSTEM_RUNNING, so
execmem gives page-mapped, non-overlapping memory and injection works.
2026-09-16 18:55:13 +02:00
Ole André Vadla Ravnås 1a497bc1c4 barebone: Split blocks to protect arm64 pages
The protect walk only descended table-to-table down to a level-3 page,
so a kernel that huge-maps the region it hands out -- the execmem ROX
cache backs the agent with a 2 MB block at level 2 -- made it throw
"Walk failed at level 2".

Before protecting, break each block covering the range into a
next-level table that maps the same memory with the same attributes,
so the pages beneath it can be protected one by one. Protection runs
with the MMU off and the guest halted, so the tables are rewritten in
place and the existing flush invalidates the TLB afterwards.
2026-09-16 17:41:26 +02:00
Ole André Vadla Ravnås be3b3d95ae device: Report connection progress
A Device connects on the first call that needs it, and with the
Barebone backend that means waiting for the target, injecting the agent
and waiting for it to come up. Add Device.connecting (status, progress)
and Device.connected, so a UI can show what a call is waiting on, and
how far along it is, whichever method made it. Providers report through
a callback handed to create(), as the socket provider serves every
remote device. Report each Barebone and socket stage, with the agent
upload advancing the fraction as it is written.
2026-09-16 17:41:26 +02:00
Ole André Vadla Ravnås ad69de758d barebone: Give the winnt x86 halves their slots
Both lagged arm64 by five kernel addresses, and the 32-bit half has
not linked since the serial hostlink started writing to a port.
2026-09-16 17:35:13 +02:00
Ole André Vadla Ravnås 7efce2f879 barebone: Cover the winnt arm64 agent with tests
Place a copy in a process and talk to it, ask it about the modules
and threads of the process it landed in, list the kernel's own
threads, and let a timer fire.
2026-09-16 17:35:13 +02:00
Ole André Vadla Ravnås d959ad23a6 barebone: Run a copy in a Windows arm64 process
Four things stood between a placed copy and a running one.

The copy asked the translation control register for the page size,
which only the kernel may do. Tell it the answer instead.

Making written code visible asked for cache maintenance a process may
not do, thus ask the system. The kernel half asked for the whole
instruction cache, which says nothing about the other processors nor
about writes still in the data cache, thus do it line by line.

The guard refuses a call to an address the memory manager never
handed out, and a thread starts by way of such a call. Give the copy
memory the manager knows about, say which addresses in it may be
called, and start the thread on a jump the guard accepts.

Give the ground back only once the thread that carried the copy is
gone.

Listing threads is the asking half's own business: walk the list the
process keeps rather than every list on the machine, ask the manager
before reading an address the walk worked out, and fill in the
registers only for a caller that asked for them.

Leave the session server alone: it refuses code of its own and is
critical, thus a copy placed there stops the machine.
2026-09-16 17:34:12 +02:00
Ole André Vadla Ravnås 287c75d8a7 barebone: Walk the right table for user pages
A lower-half address lives in the table TTBR0 names, not the one
TTBR1 names, so protecting a mapping placed in a process read and
wrote a kernel descriptor instead.

Tell the two never-execute bits apart while there: EL0 is governed
by bit 54 and EL1 by bit 53, thus a page mapped for a process read
back as non-executable even once it was made executable.
2026-09-16 17:34:12 +02:00
Ole André Vadla Ravnås 6b2e45e136 barebone: Ask the system for the thread it makes
Starting the copy read the loader's exports by dereference, in the other
process, where a page is often not resident. A failed read answered with
whatever it left behind, so the service index for one name differed from
one run to the next, and the call went to whichever service the misread
index named. That is where the access-denied answer came from.

Read them through the memory manager as well, and the index is the same
every time. NtCreateThread resolves, but the system refuses it across
processes even from the kernel, so take NtCreateThreadEx, which the
system uses itself: it takes the entry and its argument directly, and
gives the thread a stack and a block of its own.
2026-09-16 17:34:12 +02:00
Ole André Vadla Ravnås 7e9b5b8c1a barebone: Read another process through the manager
Listing processes attached to each one and read its command line through
the fault path, a pointer at a time. The pages of a process that is not
running are rarely resident, so nearly every one of those reads faulted:
a single listing took over two hundred, each answered as a failed read,
and a path came back empty far more often than it came back whole.

Ask MmCopyVirtualMemory for the string instead. It brings the pages in
and reports a status, so the reads no longer go anywhere near the
exception path, and the whole string arrives in one call.
2026-09-16 17:34:02 +02:00
Ole André Vadla Ravnås d4c1981166 barebone: Restyle the emulator gdbstub shim
Match the agent JS in this repo: two-space indent, one statement per
line, named helpers in newspaper order. No behavioural change.
2026-09-15 18:00:45 +02:00
Ole André Vadla Ravnås 0056a0d734 barebone: Read the wall clock on old kernels
ktime_get_real_ts64 arrived in 4.18; on older kernels the slot is
empty, so seeding Math.random through gettimeofday jumped to NULL and
took the agent down on the first script. Fall back to getnstimeofday64,
which has the same shape.
2026-09-15 17:01:18 +02:00
Ole André Vadla Ravnås 95a45760fe barebone: Keep the hostlink socket path short
A UNIX socket path must fit sun_path, and the system temp directory is
already long on some hosts; a full UUID pushed it over.
2026-09-15 17:01:18 +02:00
Ole André Vadla Ravnås 82dce5ddd0 barebone: Report agent faults on arm64 Linux
Record an unrecoverable fault in our own pages in frida_agent_fault and
park the thread, matching the win9x path, so the guest stays up and a
timed-out command can name where we stood. Without a reference the
record was stripped from the agent.
2026-09-15 17:01:18 +02:00
Ole André Vadla Ravnås 778462bc30 barebone: Wake the borrowed task on old kernels
A task caught entering schedule() has set itself to sleep, so a call
made in its context that sleeps -- module_alloc, kmalloc reclaim -- is
dequeued there and never resumed. Force it runnable first on kernels
predating execmem.

current lives in SP_EL0, which the stub does not surface as a register,
so read it by invoking a two-instruction stub that moves it into x0.
2026-09-15 17:01:18 +02:00
Ole André Vadla Ravnås b7d2722734 barebone: Skip the writable-half copy if absent
The pristine copy only feeds process-copies, and its few-hundred-KiB
kmalloc can fail on a fragmented guest. Proceed without it rather than
copy into a NULL destination.
2026-09-15 17:01:18 +02:00
Ole André Vadla Ravnås ec0d002537 barebone: Adapt the agent to pre-4.9 arm64 kernels
Base virt_to_phys on the classic linear map, at the top of the kernel
half, when the VA space predates the 5.4 flip; a queue ring handed to
the device otherwise lands 256 GB off and never advances.

Mark device mappings dirty so their first store does not trap, and map
through __ioremap where generic_ioremap_prot is absent.

Resolve a legacy PCI line through of_irq_parse_and_map_pci where
pci_irq_vector has not arrived, and fall back to __kmalloc where kmalloc
is inline. kernel_read and kernel_write took the offset by value before
4.14, so pass it that way.
2026-09-15 17:01:18 +02:00
Ole André Vadla Ravnås cd3b5b3f6c barebone: Fall back to module_alloc on old kernels
execmem_alloc arrived in 6.x; kernels before it hand out
executable memory through module_alloc/module_memfree.
2026-09-15 17:01:18 +02:00
Ole André Vadla Ravnås d750d38b4d barebone: Mask only IRQ when picking a context
Linux on arm64 keeps FIQ masked throughout, so the old IRQ|FIQ
test rejected every sleepable task on such kernels and injection
never found a context. Check the IRQ bit alone.
2026-09-15 17:01:18 +02:00
Ole André Vadla Ravnås 408ca82484 barebone: Harden kallsyms parsing on old kernels
Reject a token index whose entries are almost all empty, a lookalike
that turns up before the real table, by requiring the reconstructed
table to hold enough non-empty fragments. And anchor the names' start
on the position that decodes exactly as many symbols as the address
table has, so names stay paired with addresses instead of latching onto
an earlier clean-looking run.
2026-09-15 17:01:18 +02:00
Ole André Vadla Ravnås 34dfaf782b barebone: Support absolute kallsyms addresses
An older kernel stores each symbol's full address (kallsyms_addresses)
rather than a signed offset from a base, so fall back to reading the
absolute table when no relative base follows the offsets. Vet the base
as a page-aligned kernel pointer, and seed the names on a long clean
run so it cannot latch onto lookalike token data.
2026-09-15 17:01:18 +02:00
Ole André Vadla Ravnås f646ba93bd gdb: Fall back to built-in feature documents
Some stubs advertise a target.xml that includes the standard register
documents but decline to serve them; the Android emulator's does. Keep
a snapshot of the aarch64 core and fpu features and use it when the
stub cannot, so the register layout stays known.
2026-09-15 17:01:18 +02:00
Ole André Vadla Ravnås 9462ffb30f barebone: Recover kernel symbols from the image
Mine the kernel image's embedded kallsyms when the configured path is a
gzip or raw Image rather than a System.map, so no separate symbol file
is needed. Locate the token table, offsets and names, and pair each
name with relative_base + offset -- the address it was linked for.
2026-09-15 17:01:18 +02:00
Ole André Vadla Ravnås 3f4d81a7ba barebone: Instrument the Android emulator gdbstub
The emulator's gdbstub cannot commit register writes, crashes on
breakpoint insertion, and aborts on an unknown vcpu exit. Add an
ANDROID_EMULATOR stub flavor that requires the hosting process at
connection.pid; attach to it and load an embedded script that fixes all
three in place before driving the stub. The script verifies the
instruction at each patch site and refuses to patch a build it does not
recognise. Whitelist the pipe-over-vsock socket path there too.
2026-09-15 17:01:18 +02:00
Ole André Vadla Ravnås 9e210316ae barebone: Detect stub capabilities from registers
Reach a call's return through one run_until_pc helper shared by invoke
and execute, replacing the breakpoint dance each open-coded.

Detect the arm64 MMU system registers from the stub's advertised
register set rather than a config flag, choosing host page-table walking
versus the kernel's set_memory_* helpers. Read guest physical memory
from the local process at pid for a stub that needs it.
2026-09-15 17:01:18 +02:00
Ole André Vadla Ravnås 597e493ae8 barebone: Add a pipe-over-vsock Linux hostlink
On a full Android guest the agent cannot own a virtio device,
so reach the host over the guest kernel's own vsock stack:
connect to the emulator's connector port and hand it a
"pipe:unix:<path>" request, which it bridges to a UNIX socket
frida-core listens on. Wake the loop from a kCFI-thunked
sk_data_ready hook.
2026-09-15 17:01:18 +02:00
Ole André Vadla Ravnås 5040551e7d barebone: Make the Linux agent CFI/SCS/UBSAN-safe
Android kernels enforce kCFI, a shadow call stack and UBSAN
on the injected agent. Reserve x18 so we stop clobbering the
SCS pointer; hand the kernel kCFI thunks for the kthread and
IRQ callbacks (type id copied from a matching kernel fn, via
a range-independent indirect branch); skip cloak_task, whose
detach_pid arity varies and trips UBSAN.
2026-09-15 17:01:18 +02:00
Ole André Vadla Ravnås e78b371d32 barebone: Protect pages via kernel set_memory_*
When the stub lacks the MMU system registers, change page
protection by calling the kernel's set_memory_ro/rw/x/nx and
take the page size as 4096, rather than walking the tables.
Gate on connection.mmu_registers_available; allocate agent
code from EXECMEM_MODULE_TEXT on that path.
2026-09-15 17:01:18 +02:00
Ole André Vadla Ravnås 81d5b29bd9 barebone: Find the arm64 kernel by its banner
The arm64 kernel find hunts the Image header magic, but a loader that
zeroes the header leaves nothing to match, and walking back from the
hijacked pc misses a kernel the pc sits above in a module. Anchor on
linux_banner instead, as the x86 path already does: confirm it at the
linked address first for a kernel that was not slid, then sweep.
2026-09-15 16:40:24 +02:00
Ole André Vadla Ravnås afd70d834e barebone: Disarm PatchGuard on arm64 winnt
PatchGuard stops the machine with bugcheck 0x109 about a minute and a
half after anything patches kernel .text, which is every hook the
Interceptor installs. It reaches its checks by queueing its own KDPCs
whose DeferredContext is a non-canonical pointer: the host routines it
borrows test for exactly that and branch into PG. Write a canonical
context and the routine takes its own path instead.

Find them by shape, since none of the host routines are exported: walk
the pool window the kernel allocates from, asking MmIsAddressValid
before each page, and look for a routine in ntoskrnl's .text beside a
non-canonical context. Insist on the object being a KDPC as well. A
queued work item carries the same pair at the same spacing, and writing
to one is itself reported -- that alone bugchecks in the usual minute
and a half, naming the routine it borrowed.

Do this before anything patches .text, so there is nothing to find in
the meantime.

Also give each processor back the vector table it had. They do not all
boot on the same one, so restoring a single value left some of them on
a table that was never theirs.
2026-09-15 15:19:28 +02:00
Ole André Vadla Ravnås de22bb5ecb barebone: Tell an absent module from an empty one
An export directory whose pages are all absent read back as zeroes,
which parses exactly like a module that exports nothing. The kernel
image was then rejected for lacking PsLoadedModuleList, and the only
symptom was "Unable to find the kernel image".

Say so instead, and let the two callers that are merely looking decide
to move on: image discovery tries the next candidate, and symbol
enumeration skips the module.
2026-09-15 15:19:28 +02:00
Ole André Vadla Ravnås c1c7a406dd barebone: Take arm64 faults from a shadow table
PatchGuard hashes the exception vectors, so patching them stops the
machine with bugcheck 0x109. It only reads their contents, though, so
leave those alone and run from a table of our own.

Forward every entry to the kernel's rather than copying it: the entries
are full of pc-relative branches, which a copy this far away would send
into nothing. The veneer needs a register and only x18 is ever free.
Coming from EL1 the kernel overwrites it before reading it; coming from
EL0 it holds the user's TEB and the entry's second instruction saves it,
so there the two saving instructions are copied and the veneer resumes
past them.

Take the first slot of the table array in CFGRO, not the one VBAR_EL1
names: a processor boots on whichever its mitigations pick, while
KiPreflightReturnToUserMode reloads from the first. Writing the slot
only converts the processor that reloads next, so install the table on
all of them through KeIpiGenericCall, and give each one back the table
it had.

The handler moves to its own stack before building a frame, since a
fault can arrive on an interrupt or DPC stack with nothing left, and
restores x18 after aligning the stack, or every kernel call it makes
reads the wrong processor block.
2026-09-15 15:19:28 +02:00
Ole André Vadla Ravnås 2eb5f9a3a7 barebone: Let callers take turns handing over work
Work that must run on the thread's own stack goes through a single
slot, so two callers at once would lose one another's, and each would
then wait out its slices for an answer that is not coming.
2026-09-15 15:19:23 +02:00
Ole André Vadla Ravnås 7a4f4bdd83 barebone: Bound the wait for the stack helper
Work that has to run on the thread's own stack is handed to a helper
thread, which is started the first time anyone asks. The requester then
waited for it without a deadline, so a helper that is slow to appear --
the kernel still finding pages for its stack -- or one that never
appears at all left the agent stopped there for good.

The loop already re-reads the slot, so wait in slices and look again,
the way the GLib primitives do.
2026-09-15 15:19:23 +02:00
Ole André Vadla Ravnås 8e9fc804d9 barebone: Carry on when the port refuses a setting
A port bridged to a socket has no use for a baud rate, and the driver
is free to turn the request down. Ask for the settings, wait for each
answer so none is still in flight when the first frame goes out, and
keep the port either way.
2026-09-15 15:19:18 +02:00
Ole André Vadla Ravnås b204633ef4 barebone: Give each serial direction its own event
One event cannot carry two requests: a notification event stays raised,
so the next wait returns on a signal that was not its own, and an
auto-reset one is taken by whichever waiter arrives first. Either way a
read and a write on the same port answer for each other, which is why a
reply came back only now and then.

Give the reader and the writer an event each, made once and cleared
before every request, so a synchronous completion cannot leave one
raised for the next. Wait for the port's own settings the same way,
rather than leaving them in flight while the first frame goes out.

Ask the port to hold a read until a byte arrives: with the interval and
total multiplier both at their maximum, a read completes on the first
byte received, so nothing wakes up in between.
2026-09-15 15:19:18 +02:00
Ole André Vadla Ravnås 5e71497c64 barebone: Overlap serial reads and writes
The port was opened for synchronous I/O, which makes the kernel let
one request at a time near the file object. The reader spends its life
parked in a read, so nothing could ever be written back: whether the
greeting escaped at all came down to whether it was issued before the
reader got going.

Open the port for overlapped I/O instead and give each direction an
event to wait on, so a reply can go out while a read is outstanding.
2026-09-15 15:19:13 +02:00
Ole André Vadla Ravnås 434c8c02dd barebone: Wait to be greeted over a serial link
A serial port discards whatever is written to it while the guest side
is closed, and the agent opens its end well after we have connected
ours, so the first command went nowhere. The other transports connect
to us and give us that ordering for free.

Have the agent announce itself once its transport is up, and hold the
serial link's setup until the greeting arrives.
2026-09-15 15:19:07 +02:00
Ole André Vadla Ravnås 035aa392c4 barebone: Let the loop notice a serial frame
The blob flavour does not drain its transport from the main loop; it
waits until the kernel half says it has work. That question was only
ever put to the virtio, character device and vsock links, so a frame
the serial reader had queued sat there and the host waited for a reply
that was never composed. Have the serial link answer it too.
2026-09-15 15:19:07 +02:00
Ole André Vadla Ravnås ac5c84009e barebone: Order the serial line control correctly
SERIAL_LINE_CONTROL asks for the stop bits first, then the parity,
then the word length, so the port was told to use eight stop bits and
a word length of zero. The driver refused the request and left the
line at whatever it had.
2026-09-15 15:19:07 +02:00
Ole André Vadla Ravnås 8b808ad576 barebone: Give an event the room the kernel needs
The events behind the wait primitives were allocated 16 bytes, but a
KEVENT holds a word of type and size, the signal state, and a list
head for its waiters. KeInitializeEvent points that list head at
itself, so on a 64-bit kernel the last eight bytes landed outside the
block and overwrote the neighbouring pool chunk's header, which the
kernel later reported as a corrupt variable-size subsegment.
2026-09-15 15:19:07 +02:00
Ole André Vadla Ravnås 55ce119f63 barebone: Let the kernel grow the worker's stack
A system thread gets 32 KiB and the script runtime needs more, so the
body ran on a stack of our own. The kernel checks the stack pointer
against the bounds it recorded for the thread, though, and fast-fails
outside them, which is what stopped the machine in RtlpGetStackLimits.

KeExpandKernelStackAndCalloutEx does the same thing with the kernel's
knowledge, so ask for the room and run the body in the callout. The
previous commit reached this by rewriting the bounds in the thread
object, which needed offsets that move between releases.
2026-09-15 15:19:07 +02:00
Ole André Vadla Ravnås c4323db441 barebone: Declare the stack the worker runs on
A system thread gets 32 KiB and the script runtime needs more, so the
body has always run on a stack of our own. The kernel, though, checks
the stack pointer against the bounds it recorded for the thread and
fast-fails when it falls outside them, which is what stopped the
machine in RtlpGetStackLimits.

Rewrite those two bounds for as long as we are on our stack, and put
the thread's own back afterwards. Find them as the page-aligned
pointers nearest either side of the stack the kernel handed us, rather
than at offsets that move between releases.
2026-09-15 15:19:07 +02:00
Ole André Vadla Ravnås e7da82bc65 barebone: Turn foreign faults away in the vector
Windows takes every kernel-mode synchronous exception through the entry
we hook, so nearly all of what arrives belongs to the kernel. Deciding
that in the handler meant each of those faults first switched stacks
and ran our code with interrupts masked, on whichever processor and at
whatever IRQL it happened to be.

Compare the return address against our own image in the entry itself,
which takes a handful of instructions and the two registers already
saved there, and branch to the handler we replaced when it does not
match.
2026-09-15 15:19:07 +02:00
Ole André Vadla Ravnås a7dcfac8e0 barebone: Publish the fault entry atomically
The entry was overwritten a word at a time while every processor kept
taking its faults through it, so one that faulted mid-copy ran a half
written trampoline. Build the trampoline in the free bytes the entry
has to spare, make it visible, and only then replace the one
instruction at the top, which is a single store.
2026-09-15 15:19:07 +02:00
Ole André Vadla Ravnås 3b8f649fde barebone: Give the fault thunk its own stack
The thunk saved the context and then ran the Exceptor on the stack of
the thread that faulted. A kernel thread has little room to spare, and
the handler needs as much as the script asks for, so the pointer left
the thread's stack behind and the kernel stopped with a stack switch
panic. Keep the save where it is, bounded, and move to a stack of our
own, one per processor, for the handler itself.
2026-09-15 15:19:07 +02:00
Ole André Vadla Ravnås a6800681ae barebone: Claim only faults from our own code
Windows runs its kernel threads at EL1t, so the synchronous entry we
hook is where every thread in the system takes its faults, most of
them the kernel's own business: a demand-paging fault it is about to
service. Handing those to the Exceptor first ran the agent on a
foreign thread and swallowed work the kernel was waiting for.

Restore the range check, take the arm64 bounds from the linker rather
than the config so the gate holds from the first instruction, and let
everything else go straight to the handler we replaced.
2026-09-15 15:19:07 +02:00
Ole André Vadla Ravnås d72b3dbe82 barebone: Reserve every page an allocation spans
A kernel allocator hands out a block with less than page alignment, so
the allocation starts on the page boundary above it. Ask for enough
that every page the result spans is ours: the caller changes the
protection of those pages and writes all of them, and the tail of the
last one belonged to whoever came next.
2026-09-15 15:19:07 +02:00
Ole André Vadla Ravnås a9c7694e21 barebone: Clear what an image leaves uninitialised
An ELF keeps its zero-initialised data out of the file, so writing the
file over an allocation leaves that part holding whatever the previous
owner put there. A pool hands out memory as it finds it, and both the
Rust and the C halves read statics expecting zero.
2026-09-15 15:19:07 +02:00
Ole André Vadla Ravnås bd0c4fc4cd barebone: Stop asking the monitor for MMU regs
Parallels serves no system register, so the arm64 machine read them
through a monitor command instead. That command aborts the VM whenever
another processor happens to be somewhere the monitor cannot read it
from, which it does often enough to end a run at random and to make
every measurement around it untrustworthy.

Say NOT_SUPPORTED there instead, and answer the page size from the
granule the kernel uses, which is the only thing the injection path
wanted the registers for. Route the remaining readers through the
cache as well, so a target that does serve them is asked once.
2026-09-15 15:19:07 +02:00
Ole André Vadla Ravnås 42069c36a9 barebone: Leave a migrated core its own x18
A call may reschedule onto another core, and both invoke() and execute()
match the landing thread rather than the one they started on. Each then
wrote back every register it had saved, x18 among them. That register
holds the KPCR on Windows, so a core that took the call over was given
the processor data of the core that began it, and every later access
through it reached the wrong processor.

Restore the platform register only where the call stayed put, through
one helper both share.
2026-09-15 15:18:57 +02:00
Ole André Vadla Ravnås 50f9f3fa40 barebone: Fault on the interrupted thread's stack
Taking an exception selects SP_EL1, which Windows does not keep as a
stack a handler may spend. The replacement vector parked registers
below it and the thunk then took a frame there and called on, which
scribbled over the kernel stack region and showed up much later as a
FAST_FAIL_CORRUPT_LIST_ENTRY on an unrelated thread.

Select SP_EL0 on the way in, which is the kernel stack the interrupted
thread was already running on, and select SP_EL1 again before chaining,
so the kernel's own handler is entered as the hardware would.
2026-09-15 14:42:14 +02:00
Ole André Vadla Ravnås 9cf631c522 barebone: Test the arm64 WinNT agent
Map out a live arm64 guest, and run a script in one. Both read where
to find the guest from the environment, as the other live-guest tests
do.
2026-09-15 14:42:13 +02:00
Ole André Vadla Ravnås 907ba95f38 barebone: Add a serial hostlink transport
A Parallels guest has no virtio and no vsock, but it does have a PL011
that the host sees as a Unix socket. Carry the hostlink over it.

Find the port by enumerating SERIALCOMM, configure it, and read on a
thread of its own, so the loop wakes on a frame rather than polling.

Connect from the host before the guest resumes: the emulated port
drops what it writes while no peer is attached.
2026-09-15 14:42:13 +02:00
Ole André Vadla Ravnås 0f2dc534e6 barebone: Align what the Rust allocator hands out
The global allocator asked the kernel for the size and ignored the
alignment, so anything wanting more than the pool happens to give came
back misaligned, and dealloc freed a pointer that was never the block.

Go through the C shims, which already over-allocate and keep a header,
and hold the floor at two pointers.
2026-09-15 14:40:58 +02:00
Ole André Vadla Ravnås 3176508870 barebone: Port the WinNT agent to arm64
Teach the agent the other calling convention, the wider CONTEXT, and
the shape of a Zw stub, which is a movz of the service index and a
branch to the dispatcher.

There is no self-map on arm64, so walk the tables from TTBR1_EL1 by way
of MmGetVirtualForPhysical. The permission bits read the other way
around: a descriptor says read-only and execute-never, where an x86
entry says writable and no-execute.

Take faults from the vector table, which the kernel offers no interface
to replace. Patch the entry for SP_EL0, the stack a Windows kernel
thread runs on; the SP_ELx entry is never taken.

The kernel maps its own image with blocks, which no page table breaks
up, so reprotect the descriptor that maps the address at whatever level
it sits, and tell the caller how far that reached. Skipping such a
mapping left the vector page read-only and the write bugchecked.

Reserve x18. It holds the KPCR, and the compiler is otherwise free to
use it as a scratch register.
2026-09-15 14:40:58 +02:00
Ole André Vadla Ravnås c4c4b70280 barebone: Map out an arm64 WinNT kernel
The version block that the x86 flavor reads is not there on arm64, so
find the anchors from the running kernel instead: x18 holds the KPCR,
the image lies below it at a 64 KiB boundary, and it exports the module
list. Walk from PsInitialSystemProcess to reach the process list.

Set the landing zone for a call to KeBugCheckEx, and size a breakpoint
by the target's architecture rather than assuming one byte. Without
either, a run on more than one processor comes apart.

Read the MMU registers through the monitor on a Parallels guest, whose
stub serves no system register.
2026-09-15 14:40:58 +02:00
Ole André Vadla Ravnås 25a10dae9f barebone: Add a Parallels GDB stub client
Parallels Desktop serves a GDB remote from prl_vm_app once vm.debug is
set. It names its architecture by feature rather than in target.xml,
answers qAttached with nothing, and serves memory only after a stop
reply, so the generic client cannot drive it.

Halt from enable_extensions rather than while preparing the connection:
the stop reply carries register values, and nothing can parse those
until the register set is installed.

The stub honours guest page protection, and a failed write aborts the
VM, so promote a soft breakpoint to a hardware one wherever a target
reports protected code.
2026-09-15 14:40:57 +02:00
Ole André Vadla Ravnås 01ba812d82 barebone: Let an allocator declare protection
A pool that already hands out executable memory needs no page-table
walk to make the agent runnable. Let the allocator say what it grants,
and skip the reprotect when it covers what the caller wants.

Declare the Windows pool executable, as it is.
2026-09-15 14:40:55 +02:00
Ole André Vadla Ravnås a1211f3b24 barebone: Stop gating faults on our own range
The Exceptor lets a user handle any fault, not only one taken in the
agent's own code. Drop the range checks that hid every other fault
from it, and the idle-slab helper that only those checks needed.
2026-09-15 14:40:54 +02:00
Ole André Vadla Ravnås 6c96a18b0b barebone: Find the headers when built on Windows
Canonicalizing an include path there hands back the extended-length
form, and the separators inside one are never normalized -- so the
compiler found <limits.h> and then lost <sys/cdefs.h>, and no
bindings could be generated at all.

Hand on the path as it is written instead.
2026-09-15 14:02:35 +02:00
Ole André Vadla Ravnås c28a321e97 barebone: Keep a queue within the device's maximum
Every queue was set up with room for 64, which virtio-serial's control
queues do not have -- they hold 32, where the data queues hold 128.
The check that would have caught it was a debug_assert, and the agent
ships released, so it wrote 64 over a queue half that size.

QEMU used to let that pass; since the fix for CVE-2026-50626 it
refuses the size and marks the device broken, and a broken device
never announces the port the host is waiting for.

Ask what the device allows and stay within it, which is what the spec
asked for all along.
2026-09-15 14:02:35 +02:00
Ole André Vadla Ravnås 74462098a5 barebone: Open the hostlink without passing fds
Handing QEMU a socket over SCM_RIGHTS is the one thing that cannot be
made to work on Windows: AF_UNIX is there, but ancillary data is not,
so getfd had no answer and the whole hostlink was shut out.

Let QEMU serve the socket and connect to it instead. The chardev was
already a socket backend; only the address changes from a file
descriptor to a path. A socket that stays put for the life of the
chardev can be looked at while it is in use, which is worth more than
it sounds when a guest will not come up. One way in on every
platform, and the last of the guards in these two files goes with it.
2026-09-15 14:02:35 +02:00
Ole André Vadla Ravnås 36048b9747 barebone: Reach QMP over a UNIX socket on Windows
Windows has had AF_UNIX for years and GLib builds the socket address
for it there, headers and symbols alike -- only the Vala bindings
were missing, as the backend asks for gio-windows-2.0 alone.

Ask for gio-unix-2.0 as well, and let the address be made the way it
is everywhere else. Vala emits an include per symbol, so nothing
Windows lacks comes along with it.
2026-09-15 14:02:35 +02:00
Ole André Vadla Ravnås 3f1315f81d barebone: Report agent faults to the host
Park the thread on any fault our own code cannot handle, not
only on an invalid opcode, and record the vector, the program
counter and the address in frida_agent_fault. The host reads
that record when a command times out and names the function the
agent stood in.

Our pages are fixed and locked, thus a fault that reaches this
point is ours to answer for, and Windows answers it by taking
the guest down. Parking keeps the guest up and leaves the fault
where the host can read it.

The record belongs to every kernel the agent runs on; Win9x is
the only one that writes it so far.
2026-09-12 01:25:49 +02:00
Ole André Vadla Ravnås 1ee4e76dbf barebone: Give Gum a stack it may scribble on
Hand Gum the stack the thunk saved, not the stack the faulting
code ran on.

Gum's x86 recovery keeps no stack pointer of its own: it takes
the one in the context, backs off, and writes its scope and a
return address there. The faulting stack pointer sits above the
frame that VMM and the thunk are still standing on, thus those
writes landed in the middle of them, and a caught read wedged
the agent instead of returning.
2026-09-12 01:25:38 +02:00
Ole André Vadla Ravnås ab66f9caf4 barebone: Take only our own faults on Win9x
Read the faulting registers from the frame at VMM's EBP instead
of the thunk's own pushad block, and ask Gum only for the faults
of our ring zero code. Every other fault goes to Windows.

Gum answers for the thread that faulted, thus a fault the agent's
thread took elsewhere -- inside VMM, or in an interrupt that
preempted it -- was claimed by an open exception scope and resumed
on registers that were never the faulting ones. Moving the mouse
supplies such faults at a steady rate, which bluescreened the
guest while the agent was attached.
2026-09-11 16:54:56 +02:00
Ole André Vadla Ravnås 0602e5dca4 subprojects: Prepare for release 2026-09-09 20:37:12 +02:00
Ole André Vadla Ravnås cbcb39b724 barebone: Stop copying export names
The name a module hands out lives as long as the callback needs
it, so pass it straight through instead of leaving a copy behind
for every export.
2026-09-09 20:31:40 +02:00
Ole André Vadla Ravnås 30e00071be subprojects: Bump outdated 2026-09-09 20:16:23 +02:00
Ole André Vadla Ravnås 9712f1021b barebone: Expose BTF to scripts
Register a Btf namespace with the script API registry, so scripts
can ask the kernel's own type information about a structure, an
enum or a function instead of hard-coding what it says.

    const m = Btf.getStruct("module");
    m.size;                     // 1280
    m.getOffsetOf("name");      // 24
    m.fields.name;              // { offset: 24, size: 56,
                                //   type: "char[56]" }
    Btf.getConstant("MODULE_STATE_LIVE");   // 0
    Btf.getFunction("__symbol_get");        // void * (const char *)

Index the types while walking them, so a lookup resumes where the
last one stopped, and remember only what was found.
2026-09-09 20:09:39 +02:00
Ole André Vadla Ravnås 8ba21583ca barebone: Let an x86 run span leaf tables again
Scanning the upper half from the top stopped a run at every root
entry, so nothing longer than one leaf table could be mapped.
Start one root entry lower on each attempt instead, and let the
run carry on into the entries above it.
2026-09-09 20:09:39 +02:00
Ole André Vadla Ravnås d363476d24 barebone: Leave the module version unset
Report no version rather than an empty string when there is
none to give, so scripts see null.
2026-09-09 15:20:33 +02:00
Ole André Vadla Ravnås 87a3e60980 barebone: Keep the copy out of the kernel
Symbol enumeration walked the kernel's module list from the copy,
whose BTF pointer is a kernel address, so the process died on
SIGSEGV as soon as a script asked an image for its symbols.

Answer from the copy's own images instead, through GumElfModule.
2026-09-09 15:11:32 +02:00
Ole André Vadla Ravnås e340e0a36a barebone: Read the copy's images through Gum
Drop the agent's own ELF walker and hand the mapped image to
GumElfModule.

Read the file the mapping came from rather than the image in
memory, so the parse sees what the loader left out.
2026-09-09 15:00:00 +02:00
Ole André Vadla Ravnås c3d661f10f barebone: Expose Linux module symbols
Enumerate the symbols a loaded module keeps in its kallsyms
table, and the symbols it exports, so scripts can resolve names
inside a driver instead of only the kernel.

Read both through BTF where the kernel carries it, and probe for
them otherwise, following only pointers the module's own memory
holds and reading through Gum, which recovers from a fault.
2026-09-09 14:35:31 +02:00
Ole André Vadla Ravnås 783ce27c15 barebone: Size the Linux kernel from _end
Symbols above _end name fixed mappings the kernel keeps
elsewhere, such as the vector page a 32-bit ARM kernel puts near
the top of memory. Taking the highest symbol instead grew the
kernel to 2 GiB there.
2026-09-09 14:35:31 +02:00
Ole André Vadla Ravnås 9e8e3f07f4 barebone: Expose kernel modules on Linux
Enumerate the loaded modules and watch for load and unload
events, so the module registry stays in sync and scripts can
observe drivers coming and going.

Share the publishing logic between the injected agent and the
kernel module. When injected, reach the module list through BTF,
falling back to probing the list itself on kernels built without
it, and go through the regparm thunks on i386.
2026-09-09 14:35:31 +02:00
Ole André Vadla Ravnås 5b780434b8 subprojects: Bump outdated 2026-09-09 00:51:54 +02:00
Ole André Vadla Ravnås 1443659088 submodules: Bump releng 2026-09-09 00:51:53 +02:00
Ole André Vadla Ravnås 86c50f1c2d subprojects: Bump outdated 2026-09-09 00:46:47 +02:00
Ole André Vadla Ravnås 6560175dc3 barebone: Report RWX support on Linux
The agent owns its own pages now, so the allocator can take them
writable and executable instead of flipping each slab twice per patch.
2026-09-09 00:37:35 +02:00
Ole André Vadla Ravnås eb489b0d7c barebone: Flush caches without a syscall on arm
__clear_cache() went through SVC, which is what a process does. Ours
is the kernel; a handful of CP15 writes is all it takes.
2026-09-09 00:37:35 +02:00
Ole André Vadla Ravnås f49536ce1c barebone: Do Linux memory work in the guest
The pages the agent allocates are its own, and set_memory_rw() makes
them writable where the host used to map an alias. Kernel text is the
one place the kernel refuses, and it offers its own patch primitives.
2026-09-09 00:37:35 +02:00
Ole André Vadla Ravnås 507c513ecb barebone: Start the kernel scan at the half on arm
Three quarters in assumes a 3G/1G split. Kernel space starts at or
above the half on either, so start there.
2026-09-09 00:37:35 +02:00
Ole André Vadla Ravnås 33aaecea7d subprojects: Bump outdated 2026-09-08 22:33:52 +02:00
Ole André Vadla Ravnås e6b3859b94 submodules: Bump releng 2026-09-08 22:33:51 +02:00
Ole André Vadla Ravnås a4738780e7 barebone: Translate and alias in Arm's kernel half
Arm had no address translation, so remapping was refused outright, and
the run it allocated came from whichever half the scan reached first.
Walk the table the kernel uses, and prefer the kernel's own range.
2026-09-08 18:25:09 +02:00
Ole André Vadla Ravnås 3921730cae barebone: Widen the pages a 32-bit agent asks about
The remap request carries 64-bit addresses, and the array of pointers
went out as-is: on a 32-bit target the host read two of them per
element and translated whatever came out. Widen them first.
2026-09-08 18:25:09 +02:00
Ole André Vadla Ravnås cebc16fdff barebone: Map x86 aliases where the kernel lives
The scan started at the bottom of the address space and took the first
free run, which is user space and belongs to whichever process happens
to be current. A CModule kept its data there and faulted on the first
reference; the module space at the top is both valid and within reach
of a 32-bit displacement.
2026-09-08 18:25:09 +02:00
Ole André Vadla Ravnås 2a5eb32b39 barebone: Measure the stack the module runs on too
The kernel module half never said how much stack it had, leaving the
runtime with the backend's fixed floor rather than what is left below
the caller. Same kernel thread, same 16 KiB, same answer as the
injected half already gives.
2026-09-08 17:18:35 +02:00
Ole André Vadla Ravnås 47c3b0818c barebone: Serve memory RPCs on any architecture
Remapping and patching were written for Arm64 and refused elsewhere,
so on x86 the agent got a null alias back and wrote a trampoline
through it, taking the guest down. Translation is a machine's own
business, and a kernel the host can write to virtually needs no walk.

The three memory commands also touch the guest, so halt it for them:
the stub answers nothing while the guest runs.
2026-09-08 17:18:35 +02:00
Ole André Vadla Ravnås dd1d1b5a1b barebone: Share the halt around physical access
Every architecture switches the stub into physical addressing the same
way, and every one of them needs the guest halted for it. One pair of
helpers, and Arm's and x86's paths get the fix Arm64 had.
2026-09-08 17:18:34 +02:00
Ole André Vadla Ravnås 5b23bcdb38 barebone: Know a shadow page by its own record
Shadow pages were recognized by address, on the assumption that the
kernel heap lives above 0xfffff000_00000000. Linux hands out kmalloc
memory from the linear map far below that, so every patch to kernel
text was quietly dropped instead of being committed: hooks reported
success and never landed.
2026-09-08 16:48:55 +02:00
Ole André Vadla Ravnås 9d1fe85ab2 barebone: Halt the guest to reach physical memory
QEMU answers Qqemu.PhyMemMode only while the target is stopped, and
after injection nobody stops it, so the first Memory.protect() from a
script waited for a reply the stub was never going to send, taking the
session with it.
2026-09-08 16:48:55 +02:00
Ole André Vadla Ravnås 6c538f2262 tests: Cover Barebone's arm64 Linux guest
The live-guest tests skipped arm64, and none of them made a script
answer: rpc is a message in and a message out, which is what a REPL
does and what the one-way send() above never reached.
2026-09-08 15:21:49 +02:00
Ole André Vadla Ravnås 5f88059819 barebone: Report the real kernel stack size
The kernel half claimed the megabyte that a copy in a process runs on,
so the runtime's own limit sat far past the end of the 16 KiB a kernel
thread has, and a script a few frames deep panicked the guest instead
of throwing. Measure what is left below the entry into JavaScript.
2026-09-08 15:21:49 +02:00
Ole André Vadla Ravnås b067b3ea24 compiler: Emit ES modules for bundling
Node16 made tsgo emit CommonJS, which esbuild rewrapped in interop
and a default export. Consumers that eval a bundle as a plain script
then choke on the export. Emit ESM with bundler resolution instead,
rewriting relative import extensions so .ts imports still resolve.
2026-09-08 14:17:06 +02:00
Ole André Vadla Ravnås 300e8aeedf barebone: Bracket the kmod's init array
kbuild owns the final link and no script defines the constructor
brackets, so the prelinked object left them undefined. Give the
prelink a script that does.
2026-09-08 12:09:02 +02:00
Ole André Vadla Ravnås 93022f9e82 barebone: Fix the Linux kernel module build
The module half reached for the injected half's heap, symbol table
and arena forwarding, and had no process id of its own. Gate each
on the flavour that has it, and give the module one.
2026-09-08 11:37:47 +02:00
Ole André Vadla Ravnås bb0550a900 tests: Build LSP file URIs properly
Concatenating "file://" with a Windows path put backslashes in
the JSON, which the language server rejects.
2026-09-07 17:06:32 +02:00
Ole André Vadla Ravnås d53eade5b1 compiler: Initialize the backend only once
Both Compiler and LanguageServer initialize it from their static
construct, and starting the Go runtime twice aborts the process
with "randinit twice".
2026-09-07 16:50:10 +02:00
Ole André Vadla Ravnås 3447b29a34 compiler: Drop the duplicate Text field
BackendEvent declared it twice, which some Go toolchains reject.
2026-09-07 16:50:10 +02:00
Ole André Vadla Ravnås 9219ec7526 ci: Disable V8 on windows-arm64
To avoid issues with our prebuilt V8 being built with two different MSVC
versions (Intel vs arm64), whose STLs differ.
2026-09-07 16:47:09 +02:00
Ole André Vadla Ravnås fa16655e54 tests: Fix the x64 RELATIVE relocation test
Give it an addend, as that is where such a relocation carries
its value.
2026-09-07 16:19:45 +02:00
Ole André Vadla Ravnås e6e2b1d1aa Fix builds without the Barebone backend
Guard remove_barebone_device() the way add_barebone_device()
already is, and keep resolve_mmio_base() out of the Windows
build, where the QOM property getters it needs are absent.
2026-09-07 16:11:37 +02:00
Ole André Vadla Ravnås 39583a2761 barebone: Cast the range size XNU reports
The shared range reporter takes a u64, which is what every other
kernel's enumerator hands it; XNU's uses a usize.
2026-09-06 22:15:04 +02:00
Ole André Vadla Ravnås 9198961cb5 compiler: Share parsed files with LanguageServer
Put the build's program and the LSP session on one ParseCache, so a
file is parsed once per content hash however many of them are open.
The compiler refs each program file and derefs on replace or dispose.
2026-09-06 20:37:28 +02:00
Ole André Vadla Ravnås a987284afa compiler: Add LanguageServer
Run the TypeScript language server in-process, speaking LSP through
post() and the message signal, with the typings and options a build
sees. macOS links CoreServices for its file watcher.
2026-09-06 20:37:28 +02:00
Ole André Vadla Ravnås 3000eeffd8 compiler: Stat the embedded typings
Program reuse between builds stats every source file, and a typing
without one forced a fresh program.
2026-09-06 20:37:28 +02:00
Ole André Vadla Ravnås 5a7e94c75a compiler: Move to TypeScript 7's Go compiler
Track the tsc module in microsoft/TypeScript through our fork, which
also hosts what typescript-go used to. Ask for every @types package,
as 7 only includes them on request.
2026-09-06 20:37:28 +02:00
Ole André Vadla Ravnås 7e3d7a1d78 barebone: Match the Linux kmod detach_pid to 6.18
The 6.18 detach_pid collects freed pids into a caller array that
free_pids releases; take that shape when reparenting a spawned
thread, the way the injected agent already does.
2026-09-06 19:20:21 +02:00
Ole André Vadla Ravnås 2977e3dbac barebone: Hide the Linux copy's own footprint
Filter the copy's own threads and mapped ranges out of what it lists,
and record the relay FDs it holds so those can be hidden too.
2026-09-06 19:20:21 +02:00
Ole André Vadla Ravnås 865e7b512d barebone: Hide the Linux kernel half's own threads
Unlink each spawned thread from the task list and the pid hashes so
neither the guest nor our own enumeration reports it. On i386 route
the tasklist write lock and pid detach through the regparm shim, the
way its read lock already goes.
2026-09-06 19:20:21 +02:00
Ole André Vadla Ravnås b2353b0281 barebone: Enumerate the Linux kernel half natively
Listing ranges and threads went through the copy's /proc paths, whose
syscalls fault a kernel thread. Walk the kernel's own modules and task
list instead.
2026-09-06 15:52:20 +02:00
Ole André Vadla Ravnås 29545abf8b api: Drop async constructors from the gir
Vala emits them as sync-looking entries whose C symbol does not
exist, so the bindings cannot bind them anyway.
2026-09-06 11:52:49 +02:00
Ole André Vadla Ravnås 522af7bc22 barebone: Model the agent config as a sum type
Split it into injected and resident cases, and the transports to
match, so no invalid combination can be expressed. Carry the image
as bytes and name the hostlink fabric.
2026-09-06 11:52:49 +02:00
Ole André Vadla Ravnås 60af24b496 barebone: Resolve a virtio-mmio hostlink's base
Map it from the QOM bus topology so the agent need not find it
itself.
2026-09-06 11:52:49 +02:00
Ole André Vadla Ravnås 4e1b650ee0 barebone: Keep Linux's blob changes to itself
The Linux kernel-half work reached into the shared blob worker two
ways the other blobs cannot follow.

Install the fault reporter before the constructors again on the
non-Linux blobs, so winnt and win9x still catch a fault during init;
Linux alone waits until Gum and its own range are up, as its
recovery needs both.

Look up the virtio-mmio IRQ only on Linux; the call reached a
primitive the other kernels do not carry, so their blobs no longer
built.
2026-09-05 18:47:07 +02:00
Ole André Vadla Ravnås 907a997112 barebone: Recover Linux kernel guarded faults
Let a script on the kernel half guard a read the way one in a copy
does: an invalid access inside PID 0 unwinds to the catch, not a
dead kernel.

i386 and x86-64 halt the oops from a die notifier; 32-bit arm and
arm64 die past it, so detour fixup_exception through the kernel's
own text patcher. The i386 die frame carries no stack pointer and
restores only eip, so resume through a stub and stage Gum's longjmp
on a private scratch.
2026-09-05 18:18:42 +02:00
Ole André Vadla Ravnås 1cf143e07e barebone: Map a virtio-mmio hostlink's interrupt
A virtio-mmio device the guest kernel never drives has no interrupt
line mapped, so hooking one needs it made first. Decode the slot's
device-tree node -- named by its address -- to make the line and
learn its number, the way the PCI half asks the kernel for its own.
2026-09-05 18:18:30 +02:00
Ole André Vadla Ravnås 527d644526 barebone: Read arm's SCTLR under arm64 name too
QEMU's 32-bit arm stub names the banked system registers after
their arm64 aliases, so the control register comes back as
SCTLR_EL1 and the old sctlr lookup finds nothing. Fall back to the
aliased name when the plain one is absent.
2026-09-05 18:18:30 +02:00
Ole André Vadla Ravnås cda85394d4 barebone: Supply arm's unaligned access helpers
The compiler calls __aeabi_uread4, __aeabi_uread8 and
__aeabi_uwrite8 for the unaligned loads and stores a packed field
wants, and compiler_builtins does not carry them the way libgcc
would, so the 32-bit arm blob would not link. Copy the bytes
across by hand.
2026-09-05 18:18:30 +02:00
Ole André Vadla Ravnås 1c4c490106 barebone: Find a Linux copy's registers on x86-64
The copy's first user register file sat at the top of its kernel
stack, found by rounding the running stack pointer up. x86-64 keeps
that top sixteen bytes lower, in a per-cpu word, so every register
written landed past the frame the return reads. The copy took its
first interrupt with a stale segment and per-cpu base and brought
the kernel down.

Read cpu_current_top_of_stack and seat the frame under it.
2026-09-05 18:18:30 +02:00
Ole André Vadla Ravnås b7f6e48e76 barebone: Ask Linux for the virtio-pci IRQ
The x86 half read the interrupt line from PCI config space, which
holds nothing when the guest boots straight into the kernel with
no firmware to route the pins. The interrupt never arrived, so the
control queue went unserviced, the port never opened, and the host
waited forever.

Take the line from the kernel, the way the arm halves already do;
the config-space read stays for the Windows guests, whose firmware
fills it in.
2026-09-05 18:18:30 +02:00
Ole André Vadla Ravnås 64bc34ab4e barebone: Put the Linux agent in module data
execmem_alloc's module text is mapped read-only in 2 MB pages on
6.12, and relaxing the agent's data to writable hits a large page
the backend cannot split. vmalloc gives 4 KB pages but sits more
than two gigabytes from the kernel's text, out of reach of the
rel32 calls a tracepoint patches in to reach a probe.

Module data is mapped in the module range in 4 KB pages: near
enough for the direct calls, fine enough to protect a page at a
time.
2026-09-05 18:18:30 +02:00
Ole André Vadla Ravnås 7d1b616228 barebone: Resolve the Linux current task by name
The per-cpu current is exported as current_task on these kernels,
not folded into the pcpu_hot struct older ones carried. Point the
.kernel_addrs slot at it; left unresolved the slot stayed zero, so
x86-64 read the per-cpu base by luck and i386 faulted on it.
2026-09-05 18:18:30 +02:00
Ole André Vadla Ravnås c4f1898a87 barebone: Find a relocated Linux kernel on x86
x86 slides the whole image by a random aligned amount, so its
symbols no longer sit where System.map linked them. Bound the
search by the pc caught in the kernel and confirm each aligned
landing by the banner every build carries, the way arm64 already
walks back to its image header.

Drop the assumption that x86 runs unrelocated; only 32-bit arm
still does.
2026-09-05 18:18:30 +02:00
Ole André Vadla Ravnås 32b0c99ed7 barebone: Recover a Linux copy's guarded faults
Hand each fault to Gum before reporting it, so one a script guards
with the Exceptor recovers and the copy carries on, as the Windows
copies already do. A signal restorer lets the handler resume on the
mended context.

A fault Gum declines goes to the target's own handler where it has
one, and is reported as a copy's death otherwise.

Carry the register file to and from Gum on x86, x86-64, arm and
arm64, and give 32-bit arm the signal-frame offsets that the arm64
ones had stood in for.
2026-09-05 18:18:30 +02:00
Ole André Vadla Ravnås 6573e9c064 barebone: Locate a Linux copy's crash in its image
Log the fault pc and lr as an offset from the copy's image base
so a screenshot of the death line symbolizes on its own, and keep
the absolute address for anything mapped outside the image.
2026-09-05 18:18:30 +02:00
Ole André Vadla Ravnås 6f9630e281 barebone: Read the identifier selector once
Pull the whole selector out with g_variant_get_strv when the
handler starts, rather than fetching and freeing a child per
candidate on every membership test.
2026-09-04 22:08:34 +02:00
Ole André Vadla Ravnås ef60e23191 barebone: List the kernel, apply selectors
Report the running kernel as the first process entry, named after
its image and attachable at pid 0.

Push the process and application selectors down to the agent so it
skips what the caller did not ask for, rather than enumerating
everything and filtering afterwards.
2026-09-04 21:47:02 +02:00
Ole André Vadla Ravnås ddc30dc96e barebone: Report Linux and XNU's stack to GumJS
GumJS caps its stack at a floor sized for a bare kernel thread unless
the platform reports its own. The Windows agents do; Linux and XNU
did not, so their runtimes stayed at that floor, far below the stack
they run on, and deep evaluation overflowed. Report it there too.
2026-09-04 21:00:58 +02:00
Ole André Vadla Ravnås 67caf023ab barebone: Take an empty version as none at all
An unset variable falls back, but an empty one did not, and it
reached the kext as a bundle version no loader would take.
2026-09-04 17:57:34 +02:00
Ole André Vadla Ravnås 5eea0c010d barebone: Assemble the note for the right target
The compiler the agents are built with is handed no target of its
own, so the note came out for the host and the link refused it
everywhere but x86_64. Take the triple from the target spec.
2026-09-04 17:40:21 +02:00
Ole André Vadla Ravnås c6f4c66fc1 barebone: Stamp the agents with the version
The blobs get an ELF note that the usual tools read, and the kext
its bundle keys. A kext version has to be plain numbers, so only
the short string carries the whole of it.

Take the version from the environment, since Cargo and the kext
Makefile are driven directly rather than through Meson.
2026-09-04 17:25:20 +02:00
Ole André Vadla Ravnås aa5142034b barebone: Give Win9x injection a stack of its own
KERNEL32 commits a new thread's startup stack in whichever process
runs the creator, and injection runs it on an arbitrary thread. When
that is not the target, the thread's own stack is left merely
reserved there, so its startup faulted -- taking the guest down on
attach.

Send the thread past that startup to the trampoline on a shared
stack every process holds committed alike, wearing the user
selectors the startup would have restored. Park the main thread in
the arena rather than exit through a startup it never entered.
2026-09-04 17:11:12 +02:00
Ole André Vadla Ravnås 3a9e34e1f7 barebone: Name a timed-out command by its nick
The full enum constant made for a long, noisy message. Use the value's
nick, the way the other enums here do.
2026-09-04 12:37:51 +02:00
Ole André Vadla Ravnås 05556a11ca barebone: Give devices a stable, caller-set id
A caller managing several targets, or persisting what it attached to,
needs to name a device and get the same one back. Take an id in the
options and give each device its own provider, rather than wrapping a
shared one under a fresh serial each time; adding an id that is
already present is an error.
2026-09-04 11:49:40 +02:00
Ole André Vadla Ravnås df33bd86f8 subprojects: Bump outdated 2026-09-03 22:39:55 +02:00
Ole André Vadla Ravnås b02acdabaf barebone: Right-size the injected XNU stack
The thread the agent injects had an 8 MiB stack, the main thread's
size. It is a secondary thread, so match Darwin's default of 512 KiB.
2026-09-03 22:37:24 +02:00
Ole André Vadla Ravnås f890354853 barebone: Give Gum the JS worker's stack size
GumJS runs on the worker the agent spawns, which runs on a 64 KiB
stack of its own on the Windows guests. Answer Gum's stack-size query
with it while a thread is on that worker, so the limit fits the stack
that is there, and fall back to the floor otherwise -- an Interceptor
handler runs on the target thread's stack, not ours.
2026-09-03 22:30:25 +02:00
Ole André Vadla Ravnås 4480b51a84 tests: Handle what open and close may throw
Close the manager in a finally that swallows the error, the way the
other barebone tests do, and give the LPAE test the outer catch its
siblings have.
2026-09-03 19:46:27 +02:00
Ole André Vadla Ravnås 85fbbb67d5 barebone: Fold the loop wakeup into one call
The two calls look redundant. They are not. kernel::wake wakes every
waiter, but does not set the flag GLib polls. g_main_context_wakeup
sets that flag, but wakes nobody unless the loop sits in g_poll.

Fold both into nudge_the_loop, and set the flag before the wake. The
cookie is the address of a static, so drop the null check.
2026-09-03 15:07:17 +02:00
Ole André Vadla Ravnås 1d26330510 barebone: Wake the loop from every link
The work source asks for an infinite timeout and polls no file. Once
it reports nothing pending, the loop sleeps until the context itself
is woken. The virtio link does this now. The socket upcall and the two
Windows wake paths only set their flag and woke the token, so the loop
stayed asleep with work pending.
2026-09-03 14:27:38 +02:00
Ole André Vadla Ravnås 31a248fa78 barebone: Wake the loop when the link has a turn
data_tx_push stops when the transmit ring runs out of descriptors. It
leaves the rest of the frame queued, and waits for the completion
interrupt to bring a turn. The interrupt set the flag and woke the
kernel wait. But the loop sleeps in g_main_context_iteration, and that
sleeper asks GLib whether its own token is set. The wake was spent,
the answer was still no, and the frames stayed queued.

Wake the context the loop sleeps in, as the kext half does.
2026-09-03 14:27:38 +02:00
Ole André Vadla Ravnås fe6c94b5b5 barebone: Free whole descriptor chains
free_chain walked to the tail and published the tail as the new free
head. Every descriptor between head and tail left the list, but
free_cnt still counted them. The count outlived the list, so alloc
could return 0xFFFF and write past the descriptor table.

Read and write the shared rings with volatile accesses. Order the used
index against the element it describes.
2026-09-03 14:27:22 +02:00
Ole André Vadla Ravnås 59d3a03d80 tests: Cover injection in the live Linux guests
Put a copy in a process of the running guest, and talk to it both
ways, on arm, x86 and x86_64. Coverage stopped at the kernel half.
That is where every architecture but arm64 let a task into userland
with no registers.
2026-09-03 14:27:22 +02:00
Ole André Vadla Ravnås 6ee0f9b341 tests: Wait for the guest to release the link
The unbind and the ready marker went out together. The host injected
while the driver was still leaving, and the reset that follows took
the device from the agent. This failed on the Intel guests. On arm it
moved with load and with anything that shifted the timing.

Make the command report when it is done. Hold the guest until then.
2026-09-03 14:27:22 +02:00
Ole André Vadla Ravnås fb1537828c barebone: Support injection on every Linux arch
Placing a copy in a process was written for arm64. It asked for arm64
registers by name. Every other architecture wrote none, and let the
task into userland unset.

Each architecture now says where a task keeps its start registers.
Find that from the stack the entry stands on, not from a field only
BTF names. Neither 32-bit guest carries BTF.

Each one needs more than that. IRET needs the selectors the kernel
zeroed. A 32-bit stack is half the assumed size. arm leaves eight
bytes above the registers. Start flags are not zero outside arm64:
x86 wants interrupts enabled, arm wants the mode and the thumb bit.
An i386 entry takes its argument on the stack, so leave a frame where
the copy looks. Reading the arena back no longer walks the borrowed
mm, which needed two more BTF offsets.

On arm, copy the pages with user access open. The kernel guards it
with a domain, and the resolved routine leaves the switch to its
caller.
2026-09-03 14:27:22 +02:00
Ole André Vadla Ravnås 32d30045d3 barebone: Disable MSI-X when claiming a device
A virtio reset leaves the PCI capability alone. A guest driver that
put the device on MSI-X keeps it there, so the device never raises the
line the agent listens on. The handshake still completed, because the
host drives it. Then nothing arrived.

Disable MSI and MSI-X in configuration space on the way in. This puts
the device back on INTx.
2026-09-03 14:27:22 +02:00
Ole André Vadla Ravnås 8b1a799baa barebone: Add the Linux x86 and x86_64 agents
The slots came from arm64. An x86 kernel differs. current sits in the
per-cpu block. Translation and I/O mapping are inline, or named
differently. The copy into a process has another name again. One slot
was read instead of the value the host put in it.

A 32-bit x86 kernel is built with -mregparm=3. Rust names no such
convention, so the calls go through a shim built for the target. The
shim also holds the thunks the kernel calls back through: a thread
entry, an interrupt handler, the tracepoint probes, and the entry a
task runs before userland. printk and panic are exempt. They are
variadic.

No symbol names PAGE_OFFSET, so read it off the kernel base. The
32-bit user side spells out its syscalls. Check the mmap result as the
kernel means it. A user address on i386 is above 0x80000000, and is
negative as a signed word.
2026-09-03 14:27:22 +02:00
Ole André Vadla Ravnås 18956864c8 barebone: Let the guest run when attaching fails
Opening the GDB connection halts the guest, and a failure on the way
to a session -- a Win9x guest still in DOS mode, say, with nothing
paged in to enumerate -- left it halted, with the connection still
held. Resume and close on the way out, the way a session's close
does.
2026-09-03 14:05:36 +02:00
Ole André Vadla Ravnås 059b6cfe53 barebone: Name the Rust ABI the arm64 agent uses
Rustc now wants a softfloat target to say so twice: once for the
cfg and once for its own ABI. Without the second it refuses to load
the target at all.
2026-09-02 22:40:53 +02:00
Ole André Vadla Ravnås c0b90ef744 subprojects: Bump outdated 2026-09-02 18:42:16 +02:00
Ole André Vadla Ravnås f9394cf092 submodules: Bump releng 2026-09-02 18:42:15 +02:00
Ole André Vadla Ravnås a9b5787ebf barebone: Keep code pages in memory and in place
The kernel map hands out pageable memory, so a trampoline could be
reclaimed and a kernel thread branching into it took a translation
fault. Wire every page the agent runs from or keeps its books in.

Returning a page that has run wedges the agent, so keep those for
its lifetime and hand back only what Gum never saw.
2026-09-02 15:47:52 +02:00
Ole André Vadla Ravnås 1f6273a0b9 barebone: Honor the address Gum asks to be near
Dropping the spec left Gum believing a far trampoline was in reach,
and its ADRP was silently refused. Allocate above the wanted address,
and give up when nothing lands close enough.

Keep bookkeeping on pages that stay writable.
2026-09-02 15:47:52 +02:00
Ole André Vadla Ravnås bd08388460 barebone: Give code pages a name to write through
Kernel pages can be made to run or to be written, never both, and a
protection is only ever taken away. Hand out pages that run from
birth, and remap each run through the kernel map to write to it.

Pages the agent does not own keep the device mapping, in its own
book, since that refuses unaligned stores.
2026-09-02 15:16:52 +02:00
Ole André Vadla Ravnås e833563480 barebone: Sign what the kernel half calls back
The C half signs its function pointers now, so a Rust one handed
across is answered with a fault. Sign each where it crosses, the
way a proc callout already was.

Let the compiler say what to sign with: the discriminators come
from the real types, in a file that has their headers, rather than
from a guess written out by hand.
2026-09-02 15:16:52 +02:00
Ole André Vadla Ravnås 405b9be350 tests: Run the ARM agent in a live guest
The quiescent guest parks on a panic, which leaves no scheduler to run
an agent on and no processes to find. This one keeps the ramdisk the
distribution ships beside the kernel, and carries the GDB stub, QMP and
the hostlink together.

The board has no host bridge its kernel can drive, so the hostlink goes
over virtio-mmio. The slot and the interrupt come from the guest, the
transport is asked for outright, and the driver holding the device lets
go of it once it has been asked.
2026-09-02 12:07:50 +02:00
Ole André Vadla Ravnås 986a9d0f9e tests: Cover the ARM machine
The walk over short descriptors, the runs it hands out and the
protections it changes, against a stub whose tables are known, and
against a guest whose kernel QEMU can be asked about independently.
2026-09-02 12:07:50 +02:00
Ole André Vadla Ravnås feec63051d barebone: Let the hostlink be told where it is
The transport was chosen by whether a bus had been named, which left the
MMIO one to guests whose QEMU publishes hostlink-mmio. An embedder that
knows the address can now say so, the way it can already say where
configuration space is, and a bus is wanted either way for the port.
2026-09-02 12:07:50 +02:00
Ole André Vadla Ravnås be4de9b9ad barebone: Answer rather than abort off arm64
Two of the calls the agent can make were written as casts, so a machine
that is not arm64 took the process down instead of being told the call
is not implemented for it.
2026-09-02 12:07:50 +02:00
Ole André Vadla Ravnås 29f442c257 barebone: Write the config through the machine
Its neighbours protect and call through the machine; this one went
straight to the stub, and so missed whatever the machine knows about
reaching an address.
2026-09-02 12:07:50 +02:00
Ole André Vadla Ravnås d0b591c13b barebone: Support ARM in the agent
Reads current from TPIDRURO, maps I/O through ioremap(), takes its page
attributes from pgprot_kernel, translates addresses with __pv_offset,
and hands memory to a process through arm_copy_to_user().

Sizes an unsigned long as the kernel does while it is here: declaring
one as u64 costs nothing where a word is 64 bits wide and everything
where it is not, a 64-bit argument taking an even-odd register pair on
EABI. Where the kernel half of the address space begins is a build-time
choice as well, so it is taken from an address known to be in it, and
the tasklist lock is taken with whichever pair the kernel exports.
2026-09-02 12:07:50 +02:00
Ole André Vadla Ravnås b6ab15fec5 barebone: Find a 32-bit ARM kernel where it linked
The walk back to an image header only ever found an arm64 kernel, that
magic being the one it looks for. Nothing relocates a 32-bit ARM kernel,
so its System.map already says where it is.
2026-09-02 12:07:50 +02:00
Ole André Vadla Ravnås ee3698bc39 barebone: Land ARM calls where nothing else runs
Only the arm64 machine was told to return through panic, so a call on
ARM returned through whatever the guest happened to be executing --
schedule, on a kernel with a userspace to run. The next thread through
it was taken for the call coming back, and its r0 for the answer.
2026-09-02 12:07:50 +02:00
Ole André Vadla Ravnås 3b083e26e6 barebone: Give the ARM machine its kernel table
Which the machine has no way of finding on its own, the address being
one the kernel names.
2026-09-02 12:07:50 +02:00
Ole André Vadla Ravnås 790c468681 barebone: Implement the ARM machine
Walks the short-descriptor tables, maps and protects what they
describe, calls into the guest and relocates what is loaded into it.

A run has to be contiguous in virtual address space but not in the
tables describing it, so anything longer than a leaf table holds is
gathered a segment at a time. TTBCR.N splits that space, and an entry
read from the wrong half of it describes nothing.
2026-09-02 12:07:50 +02:00
Ole André Vadla Ravnås 64a7f5727e barebone: Build the agent for 32-bit ARM
The target it is built for, and the name the build gives that target
when it asks which architecture it is compiling for.
2026-09-02 12:07:50 +02:00
Ole André Vadla Ravnås 3f95474842 subprojects: Bump outdated 2026-09-02 00:30:25 +02:00
Ole André Vadla Ravnås 53f0308e52 submodules: Bump releng 2026-09-02 00:30:24 +02:00
Ole André Vadla Ravnås 4fe86fd7c8 subprojects: Bump outdated 2026-09-01 21:38:34 +02:00
Ole André Vadla Ravnås 03a6db3e56 submodules: Bump releng 2026-09-01 21:38:33 +02:00
Ole André Vadla Ravnås 674db7644e subprojects: Bump outdated 2026-09-01 15:15:49 +02:00
Ole André Vadla Ravnås 917a3bbf24 submodules: Bump releng 2026-09-01 15:15:48 +02:00
Ole André Vadla Ravnås 0b49424ed4 subprojects: Bump outdated 2026-09-01 14:05:00 +02:00
Ole André Vadla Ravnås 494521d93b submodules: Bump releng 2026-09-01 14:04:59 +02:00
xoffio c2146c42ab barebone: Allow wildcards at scan pattern edges
Instead of rejecting scan patterns starting or ending with a wildcard,
only reject those consisting entirely of wildcards, where the guest-side
scanner has no exact or masked token to use as a needle.
2026-09-01 14:02:45 +02:00
Bruno Abreu 18435c9470 compiler: Update Go toolchain to 1.24.13
Address CVEs reported in Go 1.24.3 standard library by bumping the
required Go toolchain version to 1.24.13.
2026-09-01 13:49:29 +02:00
Ole André Vadla Ravnås 4d12cc2225 barebone: Run a snippet in the live guest
Read the script from FRIDA_PROBE_JS and print what it sends, so
the kernel can be asked something without building a test for it.

Skip when the variable is unset.
2026-09-01 12:50:56 +02:00
Ole André Vadla Ravnås a842b69328 barebone: Wake the kext agent, do not poll it
The transport had no way to say it wanted a turn, so the loop
pumped it on every pass. Let the char device answer that, the
way the other transports do, and drive it from the source that
already serves the kernel half.

Wake the main context rather than the loop thread: a write that
lands before the thread blocks would otherwise leave nothing
behind for it to find.
2026-09-01 12:49:03 +02:00
Ole André Vadla Ravnås dd87557f3c barebone: Run code the kernel map hands out
Take code memory from mach_vm_allocate and tell the pages they
may run before anything has touched them: pmap_protect only takes
permissions away, so a page that starts writable never becomes
one that runs.

Write through a second mapping of the same pages, since a page
may not both take a write and run.
2026-09-01 12:29:29 +02:00
Ole André Vadla Ravnås f709d99f84 barebone: Add a kernel patcher for XNU
Find the guards from panic strings and instruction encodings, so
a kernel update does not move them out from under us.

Undo three refusals: pages the pmap has locked down, executable
mappings in the kernel's own pmap, and the table template that
forbids running what lies under it.

Write nothing unless every pass finds something.
2026-09-01 12:29:29 +02:00
Ole André Vadla Ravnås d114e7106e barebone: Hand out page-aligned code memory
The code allocator carves pages out of what it is given, so kalloc's
alignment leaves the slices short.
2026-08-30 12:57:32 +02:00
Ole André Vadla Ravnås 27d4d728e2 barebone: Patch from the kext, not the host
A kext is the kernel, so it protects, remaps and patches on its own.
Asking the host blocks until it answers, which it cannot do while it
waits for the script to load.
2026-08-30 09:34:28 +02:00
Ole André Vadla Ravnås a1959507e0 barebone: Write kernel text through an alias
Kernel text is mapped read-only and its pages cannot be remapped
writable. Map the same physical page a second time instead, and
store whole words: the alias is device memory.
2026-08-30 09:34:28 +02:00
Ole André Vadla Ravnås bfe6235970 barebone: Keep a resident agent alive on detach
Stop tears the agent down where the host injected it. The kext stays
loaded, so the loop must survive the host going away.
2026-08-29 18:31:16 +02:00
Ole André Vadla Ravnås f52fc9abc3 barebone: Sign the proc_iterate callback
The kernel branches to the callout with blraa, so an unsigned
pointer fails authentication and panics.
2026-08-29 15:56:11 +02:00
Ole André Vadla Ravnås 0b1716ee59 barebone: Wake the kext agent on a write
A character device has no interrupt to rouse the main loop, so the
write handler wakes it once the frame is queued.
2026-08-29 15:56:11 +02:00
Ole André Vadla Ravnås e8b3008d53 barebone: Poll the transport in resident agents
The kext implies xnu-core, so the poll that drives a resident
agent's transport was compiled out and the loop only ever slept.
2026-08-29 15:56:04 +02:00
Ole André Vadla Ravnås 1cc5353eeb barebone: Strip the constructors before calling
A kernel collection signs what its init sections hold, and branching
to one as it stands goes nowhere the machine can fetch from.
2026-08-29 14:28:45 +02:00
Ole André Vadla Ravnås 4e96620e52 barebone: Say where the kext was loaded
The agent is handed the kernel's own addresses, and what those are
depends on where the kernel landed. Nothing in userland says. The
kext knows where it is itself, and the two moved together.
2026-08-29 14:28:35 +02:00
Ole André Vadla Ravnås df93339430 barebone: Mark the kext as boot-required
A boot collection takes only what declares itself needed at boot.
An auxiliary one does not ask, which is why this went unnoticed.
2026-08-29 14:28:25 +02:00
Ole André Vadla Ravnås 7c27ea59db barebone: Keep the interrupt route out of a kext
Only the virtio transport installs an interrupt handler, and only a
blob carries that. What it reaches for is IOKit, which no KPI
exports, so a kext holding it cannot be linked.
2026-08-27 19:55:35 +02:00
Ole André Vadla Ravnås 9fbada363d barebone: Give the kext its kmod_info
A kernel collection binds a kext through _kmod_info, and one built
without it is turned away before anything else is looked at.
2026-08-27 19:54:56 +02:00
Ole André Vadla Ravnås 79f5f464c0 subprojects: Bump outdated 2026-08-27 18:25:53 +02:00
Ole André Vadla Ravnås 5fb1d6274f submodules: Bump releng 2026-08-27 18:25:53 +02:00
Ole André Vadla Ravnås b861f2a1f5 barebone: Build the XNU agent as a kext
Split the flavor from its packaging: xnu-core is what the agent
does, xnu wraps it as a blob, xnu-kext puts it in a kext. Add a
start and stop for the kernel's loader, and macos/ holding the glue,
the bundle and how to build it.

What a kernel exports to a kext is not what this agent calls, so the
addresses arrive through /dev/frida before the agent is started, and
that device then carries the frames.
2026-08-27 18:10:00 +02:00
Ole André Vadla Ravnås 463a285a7a barebone: Reuse the image left in a process
A page here is sixteen kilobytes and the image ended its runnable
half on four, so one page was granted both running and writing, and
the monitor keeps such a page for good.

End that half where a page ends, and put a copy going into a process
into the image it left. Three rounds in and out cost 98 KB, not
twelve megabytes.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 60a504b36b barebone: Stop reading past a thing's page
Where a port keeps what it names is found by reading through the
port, and a pointer that looks like the kernel's is not a pointer to
anything mapped. Reading past the page took the machine down, which
a copy listing its own threads did every time.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås ce5b43a054 barebone: Test what a copy sees in its process
What is mapped there, two processes kept apart, a hook in its own
process, and what it leaves behind: everything but the image, which
this kernel keeps once the process has been let run it.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås c44e8467c9 barebone: Give back what a copy takes
A copy takes memory of the process it lands in, and wrote it down
only to be left out of what the process is told is mapped. Give it
back when the copy leaves: four megabytes a time.

Hold what is taken before the shared pages exist until there is
somewhere to say it.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 6630e27b25 barebone: Let a config serve more than one device
Bringing a device up rewrote the caller's allocator configuration
with runtime addresses, so a second device translated an address
already translated and refused it as outside the kernelcache.

Keep what was worked out to ourselves.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 59bd5b3e58 barebone: Cover XNU like the other flavors
Four the Windows flavors have and this one did not: the processes it
lists, getting into one, what it says is loaded there, and what that
process can find out about us -- the threads it is told it has against
the ones it may act on, and the list the system keeps in its own
memory.

What a script reports as its platform is asserted too.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 6a9920fb05 barebone: Say less of what a task is holding
What the kernel says a task has mapped counted ours, so it disagreed
with the walk of it by what we had put there -- sixteen megabytes of
disagreement, which is more telling than either number alone. Take
ours out of both, and out of the count of ranges it took to say so.

Which process a port names is learned the way the rest is: a word is
taken as the task only when it holds a task we put a copy into. What
the kernel offers for that hangs when asked in the middle of its own
answer.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås ba895fbd9d barebone: Answer the other way a task asks
A process may ask how many threads it has and what is mapped in it
without asking either of the calls already answered for, and what came
back disagreed with them: two answers to the same question that
disagree is the whole of what something looking for us wants. Take our
threads out of the count, and ask past a range of ours the same way as
there.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 1d3a4d59d1 barebone: Say a port of ours names no thread
A process holds a name for the port of every thread, ours among them,
and everything it could do with such a name begins by turning the port
into the thread it names. Told that ours names nothing, the rest fails
as it would for a thread that has ended. Where a port keeps the thread
it names is learned on its own, a word being taken as that only when
it holds a thread already known to be ours.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 554a6727d3 barebone: Take our threads off the system's list
The system keeps every thread it made on a list in its own memory,
which a process walks without asking the kernel anything, so what the
other half hides does not cover it. Read where that list is and what
guards it out of the routine that walks it, and have each thread of
ours leave the list while it runs and rejoin before it ends, ending
being when the system takes it off itself.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 168139ad9e barebone: Keep what we map out of a process's map
A process may ask what is mapped in it, a range at a time, each answer
saying where to ask next. What is ours is not answered for: the asking
is done again from the far end of it, so what comes back is the next
range the process owns. Nothing is rewritten, and a range nobody
mentions is a range nobody looks twice at.

Both ways of asking are answered this way, and a copy asking about the
process it is in is answered whole, as with the threads.

Walked from inside a process we are in, against the same walk done by
us: five hundred and twenty-one ranges against five hundred and
twenty-five, and the four it does not have are ours.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 9749dbf479 barebone: Note which threads in a process are ours
Each thread the copy runs on writes down what the kernel calls it, and
the other half takes note, so that what a process is told about itself
can leave them out.

Asking for thread numbers is answered in memory the caller named, read
out of the call's arguments. Asking for the threads themselves is
answered with a port apiece, and ours are known by the ports the copy
holds. Both are shortened, and so is the count the runtime asks for.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås bcc7dd08c6 barebone: Do not count our threads as theirs
A process is held once again, where its library first speaks to the
kernel, and given its copy there: before a line of its own code has
run, before its libraries have finished starting, and with every image
it asked for already in place.

What stood in the way was the runtime laying claim to something the
kernel grants only a process with one thread. It counted ours, was
refused, and ended the process saying so. The threads a process did
not make are not its to count, so they are not counted while it asks.

That takes the second place a process was held at back out again,
along with working out whether it has a runtime at all: a process
without one asks nothing and is held and given its copy the same way.

Where a task counts its threads is not something the kernel says, so
it is found from a change whose meaning is known: a thread is made,
and the two numbers that were the same and are now the same again, one
higher, are how many threads there are and how many are running.

The loader now comes last among the modules rather than first, since
what comes first is the program itself and everything expects that.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 511bc5804e barebone: Match an application to what is running
The host looked for the last backslash in a path to find the name at
the end of it, which finds nothing in a path that leans the other way:
no application on XNU was ever matched to the process running it.

Asking a copy something also woke it only by accident. It sleeps until
there is something to do, and being given something to do is exactly
that, so say so.

The waits around starting a program are bounded by the clock rather
than by a count of turns, since together they have to leave the host
an answer before it stops waiting for one.

Both come from covering XNU with two of the tests the other flavours
already have: hooking a program's entry from its LC_MAIN before it has
run a line and catching that line once let go, and listing what is
installed and starting one of them by name.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 5a67bdd08b barebone: Give a new process a copy it survives
A process we put a copy into hardly ever survived being let go, and
the reason turned out to be written down by the process itself: its
crash report says the runtime ended it on purpose, from _objc_init.
It counts what it finds on the way up and will not come up beside a
thread it did not make.

So a process is now held twice. The first time is where it says how it
will make threads, which every process says and says early: it is held
there so that nothing gets away, but nothing may be put into it yet.
When the host asks to go in, it is let go as far as the first time it
asks the kernel for somewhere to run work -- by which time the runtime
has started -- and held again there, where a copy is safe to place.

Measured by instrumenting every other held spawn, so a process either
way is the control: 13 of 14 we were in are still there, against 12 of
14 we were not. They keep working, too, and see more of themselves as
they go: 145 modules while held, 247 once running.

Waiting on a copy is also bounded now. A copy in a process that has
died answers nothing, and the half that waited on it was the half that
serves every other process.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås d7d5097df0 barebone: Hold a new process at its first word
A process was held the moment it was made, which is too early to be
useful: nothing is loaded yet, so there is nothing to be asked about,
and a copy put there can reach none of what the system keeps beside a
thread -- asking it anything never comes back.

Hold it instead where its library first speaks to the kernel, saying
how it will make threads. That is still before a line of the process's
own code, and by then the loader has put everything in place: a script
in a held process now sees the 141 modules of a daemon, or the 1540 of
an application, where it saw none.

The word is answered before anything is kept back, so the library is
registered either way, and the thread that said it waits in the kernel
until the host says to let it go.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås fc0a5f191a tests: Say what becomes of a process we were in
Instrument every other held spawn and leave the rest alone, then count
what is still there afterwards. A process either way is the control,
so the two numbers can be read against each other in the run that
produced them: 2 of 7 we were in, 15 of 16 we were not.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 7b06d618ad barebone: Ask the system nothing on a bare thread
A copy in a process that has not run serves from the thread the kernel
made it, and a thread the system does not know cannot send the kernel
a message: asking where the loader keeps its list never came back, so
enumerating modules in a held process hung until the host gave up.

Nothing is loaded in such a process anyway, the loader not having run
either, so say so and say it at once.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 0fd8093d55 barebone: Start applications on XNU
An application is not a program to be run but a name the system knows,
and what it takes to bring one up -- which container, which services,
an unlocked device -- is what the system does when asked properly. So
a name with nothing of a path about it is asked of the process that
shows applications, and anything else of the one that starts programs.

What the system starts it starts in its own time and says nothing of
what came of it, so the copy says what a process of it will be called
and the other half, holding every new process anyway, waits for one by
that name to appear.

A process that becomes another one is held once as each, so letting it
go means saying every word owed to it, not the first.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 5a6efd128a tests: Measure XNU response time and throughput
Both in the kernel and through a copy in a process, the way the other
flavours measure their pace. Round trips are reported in microseconds
now: on a guest that answers in well under a millisecond, milliseconds
round to nothing.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 12e181e58b barebone: Take one write at a time on the wire
A stream takes one write at a time, and there is more than one of us:
posting a script's messages is a command going out while a reply is,
and GIO refuses both. Posting sixty-four frames landed one and lost
sixty-three, each saying so where nobody was listening.

Nor may the stream go while it is still being read from, since what
polls it is then polling something that is no longer there.

And what the agent was given is taken back through the debugger, so
where the debugger has gone -- as it has on a kernel that panics on any
synchronous exception while one is attached -- nothing can be, and
waiting for it to happen waits forever.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 608d738ea9 barebone: Deliver every frame on XNU
Measuring what goes through found three ways it did not.

A frame is written to the ring a piece at a time, and the writer gave
up when the ring filled. What that loses is not the frame but the next
one: the reader is still waiting for the rest of the abandoned one when
the next begins. Wait for room instead, the way the Linux half does,
now that there is something to end the wait.

The socket takes what it has room for and says how much that was, and
what it said was thrown away. Offer the rest again.

The copy being taken back is the one most in need of waking, and it had
just been struck off the list the waking looks in.

What was taken over in the kernel's own code is given back when the
host says to stop, rather than on the way out: giving it back means
changing that code, which is asked of the host, and by then there is no
loop left to hear the answer on. A second agent otherwise hangs its
bell where the first one's is still standing.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 04e0eb67b3 barebone: Let the halves wake each other on XNU
Neither half could reach the other's way of waiting, so both ticked:
the copy's loop woke every millisecond to look at its rings, and the
kernel half's loop never slept at all.

Give each the one thing it lacked. The kernel ends the copy's wait by
asking the kernel, which is what a process asks anyway, on a word in
the pages they share. The copy says there is something to look at
through the call that ends a wait, naming a word nothing waits on and
saying with it what nothing else in the system says -- so one
comparison tells a copy from every other caller.

A number the kernel no longer offers would have been the plainer bell,
but a process may only make the calls it is allowed to make, and that
is not one of them.

Reading the transport now happens before the wait is armed rather than
inside it: a thread may have one wait armed at a time, and the reading
can wait itself.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 5364fc4841 barebone: Read Mach-O with gum on XNU
A list entry that is not an image says how many parts it has as
readily as one that is, and following that number through memory that
is mapped either way ends nowhere: enumerating modules never returned
in SpringBoard, and a script loaded there never ran a line.

Check what an image begins with, and let gum's Darwin module reader
walk it rather than following the parts by hand.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 27cc0a3de7 barebone: List what is installed on XNU
What reads a bundle is a framework, and a framework lives in a
process, so the copy is asked. The one that starts everything else
answers none of this: the libraries that would answer talk to the
daemons it is still starting, and the asking waits for what the
answering waits for. An ordinary process is asked instead.

Where the system shows applications itself, its name for one is
preferred over what the bundle says, the way the Darwin backend
prefers it.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 6aa6143c49 barebone: Answer detach on XNU
The handler was there; the command was not routed to it, and the host
swallows what detaching says, so nothing showed for it.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 57f1659b7d barebone: Start programs on XNU
Nothing in the kernel starts a program: what starts one is a library,
and a library lives in a process. Ask the copy in the process that
starts everything else, and hold what comes of it the way any other
new process is held.

The asking is left to the loop, since putting the copy there asks the
host something, which the host cannot answer while it waits for an
answer of its own.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås f241bceb0b barebone: Get into a process that has not run
Holding a process by its task held every thread of it, including
the one the agent puts there to be its copy, so getting in waited
for itself. What is held now is the process's own threads: they are
stopped, and the task is then told it may go, so it wakes to find
its threads stopped and runs nothing of its own while the agent has
the run of it.

Which threads those are is not something the kernel says. It is
worked out from a case where the answer is known: the agent's own
thread, in the task it belongs to, is somewhere in that task's list,
and the only thing in a task that points into a thread is the list
that holds it. That gives both where the list is kept and where the
link sits, and the same answer holds for every task.

A process held before it has run has not started its own library
either, so nothing there can be asked for a thread, and asking hangs
where the asking is done. The half in the kernel knows which
processes it is holding, so it says so, and the copy serves from the
thread it was given instead.

Twenty-three of twenty-five processes held through a boot were got
into and given a script, which ran and said something back.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 67235d4fe0 barebone: Let the manual test watch spawns
Asking for gating by name attaches to the ones whose name matches
before letting them go, which is what holding them was for. That
part does not work yet: a process is held by a flag on its task,
and every thread entering user space in that task waits on it,
including the one the agent puts there. Attaching to a held process
therefore waits for itself.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 5fbb3d551a barebone: Let XNU gate spawns without a helper
Gating on Windows needs something in userland to hold what is
spawned, and the host went looking for explorer.exe. The XNU agent
holds them in the kernel, where they are made, so it needs nothing
of the sort -- and resuming one is the agent's to do as well.

The manual test grows a way to ask for it, which is how this was
watched: fifty spawns held and let go through twenty seconds of a
running phone, each named as the kernel names it. A process that
goes on to become something else is seen twice, once as xpcproxy
and once as what it became.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås b029af0ca5 barebone: Answer for a held XNU process
What is held was being worked out afterwards: the loop asked the
kernel what was running, twenty milliseconds at a time, and matched
what it found against the tasks being kept back. A process held that
long is a process the system is waiting on, and holding everything
that starts while the system is still starting is how launchd gives
up.

Which process a task is, and what it is called, are two questions
the kernel answers from the task without looking anything up. Both
are asked where the word to go was going to be said, into room set
aside beforehand, so nothing is asked for and nothing is allocated
on the thread that is making the process. The host hears about it on
the loop's next turn.

get_bsdtask_info is not named in this kernel, but the same function
in one that names it compiles to the same instructions once the
addresses are blanked. Reading proc_best_name that way answers with
the address already in use, which is what says the reading is right.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 4727278403 barebone: Hold XNU processes before they have run
A process is made and its threads wait until the kernel says they
may go. That word is the last thing said to it, after everything it
needs is in place and before one of its own instructions has run,
and it is said with the task in hand. While the host is gating, the
word is kept back and the task noted instead, and it is said
afterwards, exactly as it would have been, when the host has seen
enough. Holding what appeared and hoping to be quick about it is
gone.

The word is task_clear_return_wait, which the task table does not
name: it is the one thing fork calls with both waits at once that
the spawn path calls too, and its own code agrees -- it tests the
first wait's bit and takes the event from the task, as the sources
read.

Two things had to give way first.

Changing what memory may be had the host stop the machine and run a
stub in the guest to make the machine agree. That cannot finish for
a request the guest itself made, and on kernels that keep no
debugger attached it cannot finish at all. The host now leaves that
to whoever asked, and the half in the kernel does it where it
stands, which is where it was always going to be cheapest.

And a wait that was not taken took the kernel down. Whoever waits is
looking again anyway.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 95d5cea9f6 barebone: Hold XNU processes as they appear
The host may ask for new processes to be held so that it can see
them before they have got anywhere, and say when each may go on.
XNU had none of that.

The kernel is not asked to say when something starts; it is asked
often enough what is running that anything new is noticed while it
is still finding its feet. A process here is spawned with the
program it is to run already named, so what is seen the first time
is what it is, and the first time is as early as looking can be.
What is new is stopped where it stands, and the loop says so once
it is back where sending a frame belongs.

task_suspend and task_resume are what the task table names at 3407
and 3408. The same rule reading that table answers thread_create
with the address already in use, which is what says it is being
read right.

Spawning a process is not done yet; this is holding what others
spawn.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 1201fb3093 barebone: Detach from one XNU process
The host may be done with one process while it is still attached to
others, and until now XNU could only be done with all of them at
once. Taking one copy out is what taking them all out is made of,
so that is what it is made of now.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås e04f171bc2 barebone: Take back what the XNU copies were given
Nothing was ever given back: each copy kept its image, its arena and
its stack in the process, and the kernel half kept its own view of
two of them, for as long as the process lived.

Now a copy is asked to stop and given a moment to say it has, the
thread the kernel made is stopped where it waits, and everything
either half was holding is handed back. A process that has had a
copy taken out of it takes another and runs it.

thread_terminate is the routine the thread_act table names first,
and the one call its stub makes that no other stub makes. The same
rule answers thread_resume with the address already in use, which
is what says the rule is being read right.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 8f22aa50b2 barebone: Instrument system code from the copy
The copy reports what the process has loaded, rather than what the
kernel is made of: the kernel says where the loader keeps its list,
and the list says where each image begins and what file it came
from. What an image offers is read out of its own tree of names,
and a name asked for without an image is the loader's to answer,
which it does through dlsym.

Hooking the system's code then wanted one more thing. Pages shared
with every other process are not a process's own to write, and
asking to write them plainly is refused; asking for a copy of its
own is granted. The trampoline is taken as near its mark as the
kernel will place it, so the jump to it reaches.

A script in the copy can now look up open, hook it, call it, and be
told the path it was called with.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 618a8576c5 barebone: Stand the XNU copy on libSystem
A thread the kernel made is not a thread the system knows: it has
none of what the system keeps beside a thread, and the first thing
that reaches for it falls over, which is what killed every attempt
to ask the kernel anything by message.

So the copy finds the library every process shares -- the kernel
says where the shared code begins, and what begins there says which
images are in it and where -- and reads out of that library what it
offers and where each thing is. With that it asks the system to make
a thread of its own out of the bare one it was given, and serves the
host from there. The bare thread then waits on a message that never
comes, which is somewhere it may be stopped from without harm.

Threads the copy wants afterwards it asks for the ordinary way, and
what it asks about them -- which there are, what each is doing, and
what each should do instead -- it asks the system rather than the
half in the kernel, which no longer has to make threads for it.

The addresses handed over are signed, this being a machine that
signs the ones it jumps to.

Asking which threads there are is refused on this system, in every
process tried, so what comes back is nothing at all.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 568a30f925 barebone: Let the XNU copy instrument its process
Interceptor works there now: it hooks a function, the hook is
reached, and the process carries on.

Two things a process may not do for itself stood in the way. It may
take the execute away from its own memory but not give it, since
nothing signed what it wrote, so it asks the half that is not asked
that question -- the same way it asks for a thread. And Darwin will
not let it read how the caches are kept, though it may act on them,
so the shape comes from the half that may ask, as the page size
already did.

The copy also stops trying to hand the host a second view of a page
for patching, which it has no way to ask for and no need of: it
changes what its own memory may be instead, as the Linux copy does.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås c2ff29f97b barebone: Show the copy what its process maps
A process may ask the kernel about its own regions without sending
it a message, one region at a time, each the one holding the
address asked about or the next one after it. Walking that from
zero is the whole map, which is what the copy reports rather than
only the memory it took itself.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås c0be69928c barebone: Run a script inside an XNU process
The copy answers the host now: it is given a script, compiles it,
runs it, and what the script sends comes back through the kernel
half. A script in a process reports that process and the thread it
is on.

Three things stood between it and that. Asking what time it is went
to the kernel's calendar, which a process may not call, so it goes
through the group the copy runs on like the rest. The kernel half
slept on its loop with nothing able to wake it -- what fills its
rings is a process, and a process cannot wake a thread of the
kernel's -- so it looks instead, as it already does with the
hostlink. And a message from a copy has to say which process it
speaks for, or the host has nowhere to put it.

Enumerating the ranges of the process is still the copy reporting
only what it took, since asking the kernel for the rest is a
message a process is denied.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås edd5c988f0 barebone: Run the agent's runtime in the XNU copy
The copy brings itself up the way the kernel half does: its own
primitives, its own memory, the runtime the script engine needs.
It then serves the host on the thread it was started on, and the
frames the host addresses to that process reach it there and its
answers go back.

Three things the kernel half may do and a process may not turned
up on the way. Asking the machine how it is paging reads a
register of the kernel's, so the copy is told instead. Throwing
away the whole instruction cache is likewise the kernel's, and the
line-by-line way both halves may ask for was already written for
Linux. Memory that will be run has to be written first, so what
the copy hands back is writable and turns when it is asked to.

A process is also denied the message that starts a thread, so the
copy asks the half that started its first one.

The heap the Linux copy carves its memory out of is the same heap
this one needs, so it moves up beside the agent rather than living
under one flavor.

Creating a script in the copy does not finish yet.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 2db7bba1f3 barebone: Give the XNU copy threads of its own
What a process may ask about its threads is a message to a port
rather than a call, so write out as much of MIG as the copy needs:
a header saying where it goes and what is being asked, and a reply
on a port made for the purpose.

The copy enumerates the threads in the process it landed in, reads
what each is doing, writes a thread's registers back while holding
it still, and asks for threads of its own. That completes the group
it runs on.

Each was checked in the guest against what the process really has:
every thread answers with a stack and an instruction inside the
process, a change taken and let go leaves it running, and a thread
asked for turns up in the count.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås f2fdbfbe7a barebone: Build the XNU agent where it is placed
The copy runs at an address of the process's own, and a group of
primitives is function pointers held in data, so the copy read the
addresses the kernel runs at and went there.

Build the XNU agent the way the other flavors are built: position
independent, against the pic sdk, with the relocations kept and the
image split into the half that is only ever read and the half each
copy needs its own of. Placing a copy now relocates that half to
where the copy sits, and the copy stands on its own group.

Asked which process it is in, and asked to take a page, the copy
answers through that group.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås ba043cea15 barebone: Gather what the XNU halves differ on
The image is the same in the kernel and in the copy, and most of it
does not care which it is. What differs is who answers when it asks
for memory, for a thread or for the time, so put those in a group
the way the other flavors do, with one for each side.

The copy cannot swap its group in yet. A group is function pointers
held in data, and the XNU agent is linked without relocations, so
data in the copy still names the addresses the kernel runs at. The
calls written out beside the group work regardless of where the
copy sits, which is what it uses meanwhile.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 56429e31dc barebone: Let the XNU copy ask the kernel itself
The copy is the agent's image and nothing else, with no library
loaded for it and no symbol resolved on its behalf. It needs none:
a process asks the kernel through the same instruction whatever it
went through to get there, so the calls are written out.

Asked which process it is in, the copy answers the one the kernel
half put it in.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 61ddffb0c3 barebone: Carry frames between the XNU halves
The pages the two halves share hold a ring running each way, and
each half reaches them at an address of its own, so a frame is
written on one side and read on the other with nothing asked of
anybody.

The copy has no allocator yet -- what it would reach for lives in
the kernel -- so its side reads into a buffer it already has.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås b42f1e73bc barebone: Give the XNU copy its own half
The copy is the same image at an address of the process's own, so
the memory is taken from the process and the kernel takes a view
of the same pages to write the image through.

What the halves leave for each other lives in those pages too,
which is why neither has to ask the other to read or write them.
The copy writes there to say it reached its own code, and the
kernel half waits for that word before answering.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 7d23de749e barebone: Put the agent into an XNU process
Reach the process through its task and the map it keeps, take
memory from that map, lay the code out in it, make it runnable,
and give it a thread that is told where to begin.

None of what this needs is exported to kernel extensions. They are
Mach routines, so their addresses come from the tables MIG leaves
behind, and the host writes them into the slots.

The manual test can now attach to a process rather than only the
kernel, which is how this was watched: the code that lands there
runs.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås e476f59baf barebone: Let the flush stub be run
Making pages executable ends in a stub that flushes the
translations, and the stub itself was never granted what it asks
for on behalf of everything else.

Kernels that hand out memory for data alone -- which is all a
modern XNU exports -- therefore fault on the very first flush, and
the agent never starts. Grant it directly rather than through
protect_pages, which is what asks for the flush.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 028a0b1fd2 barebone: Let a session stay attached to the guest
Apple's VZ stub takes the whole virtual machine down when told to
let go, which ends the session the moment the agent is in.

The research kernel needs the opposite -- it panics on a
synchronous exception while a debugger is attached -- so say which
until there is a way to tell the two apart.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås e6fe301a38 barebone: Say what an XNU kernel has mapped
Gum had no ranges and no protections on XNU, so a script asking
what is mapped got nothing back.

The loader scatters the images a kernel is made of, and the agent
cannot read their headers from where it runs -- one that faults
takes the kernel with it. The host reads them while it is working
out where everything landed, and passes on what it found.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås 5f9ec7495a barebone: Let the manual test say what to run
It asked for a symbol only iOS has, so on any other kernel the
session came up and the script could not.

Name a script through the environment, and say what the target has
running.
2026-08-27 18:09:36 +02:00
Ole André Vadla Ravnås fc926151c1 barebone: Skip XNU processes that cannot be there
Numbers are handed out in order, so a long enough run of unused
ones means the end of what the kernel is holding. Asking after all
of them takes long enough for the host to give up waiting.

Drop what read the kernel's own images for their segments: those
addresses are not the agent's to read, and one that faults takes
the kernel with it.
2026-08-23 12:59:11 +02:00
Ole André Vadla Ravnås cdb35a0496 barebone: Find where a scattered XNU kernel landed
Three things stood between a macOS guest and its own layout.

Apple's stub takes the whole virtual machine down when asked to
read what the guest keeps beyond its reach, so read through the
bridge where there is one, and never ask about a device.

A collection header sits at a page of its own somewhere inside a
range rather than at the range's beginning, so look at every page
of one.

The header belongs to none of the sets inside it, and neither does
anything else the loader left where it was; let the collection
stand for whatever the sets did not claim.
2026-08-23 12:59:11 +02:00
Ole André Vadla Ravnås 1d67d48aa4 barebone: Say what a stub turned down
A rejected command arrived as the bare word "09", which says
neither what was asked nor of what.
2026-08-23 11:58:23 +02:00
Ole André Vadla Ravnås 48482c1c33 barebone: Let go of an XNU agent asked to stop
The host asks every agent to stop and waits for it to say it has
left, and XNU answered that it did not know the command.

Take it, and give XNU the part of the teardown it has: the scripts
go, the transport shuts down, and the flag the host waits for is
set.
2026-08-23 01:39:00 +02:00
Ole André Vadla Ravnås 70a835028d barebone: Say what the XNU kernel is made of
Gum had no ranges and no protections on XNU, so a script asking
what is mapped got nothing back.

Read the segments of every image the host named, and move them by
the distance between where each was linked and where it landed.
2026-08-23 01:07:17 +02:00
Ole André Vadla Ravnås 78a02a2680 barebone: Read XNU processes on either kernel
macOS exports what reads a process and iOS does not; iOS walks its
own list and macOS leaves that to whoever asks by number.

Take whichever the kernel offers, on both sides, and only look for
where a process keeps its number and its name when the kernel will
not say.

Fall back to thread_wakeup_prim and ml_static_vtop where the plain
names are absent.
2026-08-23 00:55:42 +02:00
Ole André Vadla Ravnås 6261536b3e barebone: Find where XNU keeps a process's name
XNU ships no type information and inlines the small accessors
away, so neither half can ask where a process keeps its number
and its name.

The host asks the kernel for its processes, looks at what they
hold, and picks the two places out: one of them is launchd, and
launchd says so. What it finds is passed on, along with anything
else a kernel leaves nowhere to read.
2026-08-22 22:49:07 +02:00
Ole André Vadla Ravnås 8416b2f935 barebone: Enumerate processes on XNU
The flavor could see the kernel and nothing running on it. The
kernel walks its own list and hands out what is in it, so the
agent asks rather than knowing how a proc is laid out.
2026-08-22 22:39:01 +02:00
Ole André Vadla Ravnås da56982691 barebone: Carry data with a script message
The post and the message that answers it left the binary payload
behind: a TODO on the host half, and a NULL where Gum takes the bytes.
Thus an rpc export could be called, but nothing could be sent with the
call, and the data a script sends never left the guest.

Both carry an array of bytes now. A test calls an export with four
chunks of 4 MiB, one at a time, and says what that costs: 4.1 MiB/s on
Win9x, 4.0 on WinNT and 2.6 on its x64.
2026-08-22 17:55:36 +02:00
Ole André Vadla Ravnås 4b5718e58e barebone: Say which system an agent runs on
Each flavor answers gum_barebone_query_platform with the name of the
system it is on: windows for the two Windows ones, darwin for XNU, and
linux for the kernel module and the half the host injects. Both halves
of a flavor answer the same, thus a script in a copy and one in the
kernel agree.

The tests ask each Windows flavor twice, once in the kernel half and
once in a copy.
2026-08-22 13:46:40 +02:00
Ole André Vadla Ravnås 43d341c243 barebone: Move the WinNT loader library word
The word holds a pointer, thus it takes eight bytes, and the four after
it are where a copy says it has left. One wrote over the other, and
only the order of the writes kept that from mattering.

It sits at a word of its own now, aligned as a pointer wants.
2026-08-22 12:44:53 +02:00
Ole André Vadla Ravnås c90ce9fadd barebone: Watch what the Linux loader maps
A script was told what the process had mapped when it asked and
never again. The copy now replaces the empty function the loader
calls whenever it has changed something -- what a debugger waits
on -- and asks the kernel again from there.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås 9c756d4192 barebone: Gate spawns on Linux
The host could hold what it spawned itself, but not what the
guest started on its own. Every exec is held while gating is on,
and the loop tells the host which process it was, since the exec
path is not where a frame may be sent from.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås 604c399e5b barebone: Spawn and resume a program on Linux
Spawning was the Windows flavors' alone, and it went through a
process of the target's that could do it: the kernel here starts
programs itself, so the host asks the kernel half directly.

What is started is stopped the moment it has execed, thus the
host is there before the program runs anything of its own, and
resuming lets it go.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås fbfdda8742 barebone: Let the Linux agent be told to stop
The command was the Windows flavors' alone, so the kernel half
stayed where it was with its copies still placed. It now sees
every copy out, lets go of the tracepoints and the interrupt,
and leaves nothing of itself behind.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås 6d039faaed barebone: Watch Linux threads come and go
A script asked to be told and heard only what was there when it
asked. The kernel half now watches the tracepoints the scheduler
already has and leaves what it sees in the arena, and the copy
hands it to Gum's registry.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås d82ad877c2 barebone: Take the Linux copy back out
A session that ends left the copy running, its pipes open in the
file table the kernel half shares, and its pages in the target.
The copy now closes what it opened and goes when it is asked,
and the kernel half unmaps the rest and forgets it; attaching
again after that works.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås 68425814f7 barebone: Stand the Linux copy on a thread pointer
A task the kernel half makes has none, and code of the process
keeps things below it: musl's errno lives there, so the first
call into the C library the copy was placed beside killed it,
hooks included. Each thread of the copy now has a page of its
own to stand on.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås 259d718ca4 barebone: Wake the Linux halves through a pipe
Waiting on a futex from a kernel thread spins in the retry loop
that guards the private hash, and on a kernel built without
preemption that spin holds off the grace period it waits for:
the guest locked up under a target that forks.

The copy opens a pipe each way as it comes up and leaves the
descriptors in the arena; the two halves share a file table, so
the kernel half takes the ends it needs and reads and writes them
as the kernel does. What the copy has to say about a fault, a
panic or a message of its own now reaches the kernel half too,
since the file it would have written to is nobody's.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås 6a5b87f6b1 barebone: Let the Linux copy patch what it runs
Invalidating the whole instruction cache is the kernel's to do,
and the copy runs where that instruction faults. Both halves can
ask line by line instead.

A process may also make its own pages executable, so the copy
writes its patches itself rather than asking the host to remap
what it already owns.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås 1019cd4d90 barebone: List the threads of the Linux copy
The copy is a task beside the process, thus what runs there is
the process's own task list, not its.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås 90ceb856b4 barebone: Answer for what the Linux copy sees
Gum asked the flavor for modules, ranges and protection and got
the kernel's, which are nobody's business inside a process. The
copy answers from /proc/self/maps, as the images the process has
mapped, what is mapped where, and what may be done with it.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås 2c265b649b barebone: Say which process the Linux copy is in
The copy is a task beside the one it was placed in, thus its own
id says nothing about what a script is instrumenting.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås f370ab5986 barebone: Carry frames between the halves on Linux
A command for a process the host attached to goes to the copy living
there and its answer comes back the same way, each side waking the
other on the futex it sleeps on. Which session a frame belongs to is
the process the copy was placed in, which here is not the process it
is: the copy has a task of its own.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås b4168c9a1a barebone: Give the Linux copy memory of its own
The C library asks for memory of its own while it is taking the lock
that guards its allocator, so an allocator that hands out what the C
library gives never finishes the first call: the copy died on its
first allocation, and a blown stack leaves no room for the handler
that would have said so.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås e1d5570993 barebone: Give the Linux copy its own primitives
The half that runs in a process cannot do what the kernel half does:
it allocates through the C library and mmap, changes protection with
mprotect, sleeps and wakes on a futex, and makes threads with clone.
Which of the two is answering is now a table the copy switches on
its way up, as the Windows flavours already do.

A fault there is nobody else's to report, since the kernel kills the
task and says nothing, so what it was is written where the kernel
half can read it.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås 970744a9d5 barebone: Give the kernel half a view of the arena
The pages the copy was handed are pinned and mapped again for the
kernel half, so what the two leave for each other is reached by an
address of its own: neither borrowing the address space nor copying
through it. The address the copy sees is kept for what is keyed by
it, which is the futex the two sleep on.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås 316625f1e4 barebone: Take memory attributes from the kernel
Which index carries device memory and which carries ordinary memory
differs between kernel versions, so the register the kernel filled
in says which is which. The index this was written with turned out
to be tagged normal memory, which a machine less forgiving than an
emulator would not have let pass.

Bring the linker scripts back in step with the slots the agent
declares while here.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås d477446c1e barebone: Find fields the kernel lays out itself
A structure whose fields the kernel is free to reorder keeps them in
a member with no name of its own, so what is asked for is looked for
through those as well.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås 7ed3d4d9a0 barebone: Let the two Linux halves talk
Each side sleeps on a word the other bumps, so neither looks at a
clock: the copy waits on a futex the kernel half wakes while it
borrows the address space, and a thread of the kernel half's own
sleeps on the futex the copy wakes.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås 4129ebdf99 barebone: Give the Linux copy a task of its own
The copy is given a task that shares the address space it was mapped
into, and enters userland at the address the kernel is told to give
it. Nothing of the target is borrowed, so a target whose threads are
all asleep is reached as readily as a busy one.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås 95134fd30c barebone: Place a copy in a Linux process
Borrow the target's memory long enough to map a copy of the image
into it: the half that is only read as it stands, and the half that
is written to as it was before this agent touched it, with every
address in it moved to where the copy runs.
2026-08-22 12:35:48 +02:00
Ole André Vadla Ravnås 6cf068f833 barebone: Read the Linux layout from the kernel
A kernel built with BTF says where every field of every structure
sits, which the task walk was guessing at. Take it from there when
it is present, and keep the guesswork for kernels without it.
2026-08-22 12:35:29 +02:00
Ole André Vadla Ravnås 207f8321dd barebone: Drive the Linux transport by interrupt
Ask the kernel for the line it gave the device and take that, so the
queues are read when a frame lands rather than looked at over and
over. The loop waits on a kernel wait queue the interrupt wakes.
2026-08-22 12:35:29 +02:00
Ole André Vadla Ravnås b8c4735767 barebone: Enumerate processes on Linux
Nothing states where a task keeps its name, number or list: the
offsets move with the version and the configuration. Find them from
the idle task, whose name is known, and take the task list to be
the longest circular one whose every node is a task.

Walk it under the lock that keeps it still, taking a reference to
each task's executable there, and resolve the paths once the lock
is dropped.
2026-08-22 12:35:29 +02:00
Ole André Vadla Ravnås a771bb7b48 barebone: Let the flavor name a process
Which name a process is listed under is the flavor's to decide,
not the handler's.
2026-08-22 12:35:29 +02:00
Ole André Vadla Ravnås 7c571d5ac3 barebone: Find virtio-pci through ECAM on arm64
Configuration space is reached through I/O ports on x86 and through
a window in memory elsewhere, so the host passes where that window
is and the bus maps it once for every function on it.

No line the kernel's interrupt API accepts comes with the device
there, thus the waiter is left to poll for what arrives.
2026-08-22 12:35:29 +02:00
Ole André Vadla Ravnås 2da849ff35 barebone: Give DMA pages memory a device can reach
A device is told where to look in physical terms, and only what the
kernel maps linearly can be told. Executable memory is not, so the
virtqueues pointed the device at nothing on Linux.
2026-08-22 12:35:29 +02:00
Ole André Vadla Ravnås 2b4e66b4f0 barebone: Let the Linux agent write its own code
Executable memory comes back read-only, so the agent faults on the
first instruction it lays down. Ask the kernel to make those pages
writable again, which on arm64 leaves them executable.
2026-08-22 12:35:29 +02:00
Ole André Vadla Ravnås 66bd8e3ff1 barebone: Reach the Linux kernel as arm64 has it
The host resolves a slot by the symbol name with its first character
dropped, so a kernel function whose own name begins with an
underscore needs one more. Call printk and panic with the message as
the format, since a variadic call through a slot marshals nothing.

Take the clock and the mapping helper this kernel exports, and work
out a physical address the way the kernel's macro does: the linear
map's base, from what the translation control register was set up
with, and where memory starts.
2026-08-22 12:35:29 +02:00
Ole André Vadla Ravnås 10cc617e1d barebone: Hijack Linux where a sleep is woken
A thread reaching the scheduler from a path that masked interrupts
cannot be borrowed: the agent goes on to call kernel functions that
sleep, and a sleep taken with interrupts masked is one nothing ever
wakes. Wait for one that got there with them on.
2026-08-22 12:35:29 +02:00
Ole André Vadla Ravnås dfffea35c2 barebone: Give the injected image usable pages
Executable memory is handed out read-only, so the agent could not
write to its own data, nor to the config the host leaves for it.
Protect what the image says is writable, taken from its segments
rather than from wherever text happens to end, and take the Linux
image from execmem so its text is executable at all.
2026-08-22 12:35:29 +02:00
Ole André Vadla Ravnås 793d592362 barebone: Make arm64 page protection take effect
A descriptor's permission bits mean what the kernel decided they
mean: Apple remaps them through SPRR, and a kernel using permission
indirection reads an index out of them, leaving AP to say nothing.
Grant writable pages the way executable ones already were, by
sampling a page the kernel made that way -- the running thread's
stack -- and replaying its encoding.

Editing a descriptor also leaves whatever the TLB cached in force,
so run the maintenance the change needs from a stub of our own.
2026-08-22 12:35:29 +02:00
Ole André Vadla Ravnås 6f194f12df barebone: Read what a QEMU stub says about arm64
Three things stood between the Barebone backend and a Linux guest under
QEMU. TTBR1 carries an ASID above the table's address and CnP in the bit
below, so mask both off before walking. A stub that keeps some of its
registers out of the group packet -- SVE, on a QEMU that has them -- is
written to one register at a time. And only the VZ stub has a link
register of its own; QEMU knows only x30.

Infer the allocator for a Linux kernel from the entry point it exports,
whichever of the two names this version uses.
2026-08-22 12:35:29 +02:00
Ole André Vadla Ravnås 06be91a508 barebone: Inject into a running Linux kernel
Take LINUX as a kernel kind, and find what the agent needs the way the
kernel itself leaves it lying about: the symbols come from the
System.map named as the image, at the addresses it was linked for, and
where it actually landed is read back out of the guest -- walking down
from where it is executing until the header every arm64 image carries
turns up. The distance between the two moves every symbol.

Bring it to a standstill in the scheduler, which every kernel thread
passes through on a stack of its own.
2026-08-22 12:35:29 +02:00
Ole André Vadla Ravnås 4b8b9afa23 barebone: Let the Linux agent be injected too
The agent reaches Linux from the inside as a kernel module, or from the
outside the way it reaches XNU and Windows. Split the backend in two so
everything above the primitives is shared: the facade picks one, the
module one calls into the shim kbuild compiles, and the injected one
takes its addresses from .kernel_addrs.

Rename the XNU half of Gum's platform backend, which was never about
XNU: it is what an injected agent needs on a kernel that keeps its page
tables and its executable regions to itself, so Linux wants it as well.
Give it the arch it lacked and the paths each kernel names its modules
by.
2026-08-22 12:35:29 +02:00
Ole André Vadla Ravnås b6e6a29801 barebone: Stalk a thread on WinNT as well
The test wrote a body of 32 bit code, thus the thread of the x64 guest
ran nonsense and faulted before the Stalker ever saw it, and the one
before it asked whether the pc had left the page of the body, which
says nothing about a thread that sleeps.

It writes the body each machine takes now, and WinNT answers the same
way Win9x does: the blocks arrive and the thread lives.
2026-08-22 12:32:01 +02:00
Ole André Vadla Ravnås 4c33f94595 barebone: Say why the Stalker skips KERNEL32
The line that keeps it out looks arbitrary without the reason.
2026-08-22 12:22:05 +02:00
Ole André Vadla Ravnås a590b53798 barebone: Stalk a thread that calls the system
The test followed a thread that spins in code of its own, thus it never
showed whether the events arrive. It follows one that sleeps now, and
counts the blocks the sink hands over: 376 of them in five seconds.

A stalked thread that calls a service of Win9x faults, since the call
goes through a gate that the Stalker cannot follow, thus the test keeps
it out of KERNEL32.
2026-08-22 12:13:18 +02:00
Ole André Vadla Ravnås 9962d0ad3d barebone: Keep a copy's thread storage in slots
Every GPrivate went through one lock and one map keyed by thread, which
the Interceptor reads on each call it stands in front of, and the code
the Stalker writes reads on each block it leaves.

A copy takes a slot of the system for each GPrivate now and keeps the
number in the key itself. The kernel halves keep the map, since they
have no slots to take.
2026-08-22 02:39:14 +02:00
Ole André Vadla Ravnås fe3496fd48 barebone: Tell a Windows copy where it runs
A copy took the range of the image it runs in from the half that placed
it there, and that half never said. Thus the range stayed empty, and
what the Stalker keeps out of covered nothing: it instrumented the
runtime it runs on, and a thread it followed took the copy with it.

The half that maps the image writes the base and the size into the
arena now, and the copy reads them before Gum starts. A test follows a
thread of the target on Win9x and sees it run the code the Stalker
wrote.
2026-08-22 02:34:44 +02:00
Ole André Vadla Ravnås 3acacc41a0 barebone: Send the rest of a big WinNT frame
The kernel half keeps what does not fit in the ring, and its loop
writes the rest as the copy makes room. Both calls that do so were
left to Win9x alone, thus a frame bigger than the ring waited for
good on NT and a script of 42 kB never arrived.
2026-08-21 22:42:41 +02:00
Ole André Vadla Ravnås daa9e2a553 barebone: Drop unreachable code in the Win9x copy
The tail of patch() sits after the return, thus none of it runs, and
the slot it gives back is given back above.
2026-08-21 22:24:31 +02:00
Ole André Vadla Ravnås ef1be27f04 barebone: Recover from a fault in a Windows copy
A copy heard nothing about the faults of its own process: the fault
hooks of the kernel half are the VMM's and the machine's, which see
ring 0 only. Thus a script that read an address that is not there took
the whole target down, which on Win9x is a blue screen, rather than
throwing where it was read.

Each copy feeds its own Gum now: Win9x through an unhandled exception
filter, chained to the one it replaced, and NT through a vectored
handler that runs ahead of the rest.
2026-08-21 22:24:31 +02:00
Ole André Vadla Ravnås 5d56093b92 barebone: Keep the WinNT loop free while it sends
A frame bigger than the ring made the kernel half wait for room in
the loop that drains what the copy sends, so two full rings left
each half waiting for the other.

What does not fit is kept, as on Win9x, and the loop writes the
rest as the copy makes room.
2026-08-21 22:12:18 +02:00
Ole André Vadla Ravnås e76a336ee7 barebone: Keep the Win9x loop free while it sends
A frame bigger than the ring made the kernel half wait for room,
and it waited inside the loop that answers the patches the copy
asks for on its way up, before it serves the ring at all. The two
then waited for each other, and a script of 42 kB never arrived.

What does not fit is kept, and the loop writes the rest as the
copy makes room.
2026-08-21 20:11:19 +02:00
Ole André Vadla Ravnås ee471ef122 barebone: Wait for the guest to open the hostlink
A port the guest has not opened is one the emulator throws writes
away on, so the first command was lost and the reply waited for
forever. QEMU says when the guest opens it.
2026-08-21 20:10:43 +02:00
Ole André Vadla Ravnås f6b7dff649 barebone: Keep the transport's state off the stack
A kernel thread on Linux has 16 KiB, and the queues' page tables
alone are copied past it as the state is moved out of start().
2026-08-21 20:10:43 +02:00
Ole André Vadla Ravnås 8fe9438b9c barebone: Find and change Windows threads by id
Gum asks the agent for both through hooks of its own, and no Windows
flavor answered either. All four halves answer now.

A lookup walks the list of threads and takes the registers of the one
it wants, rather than of every thread. Changing them goes through
VWIN32's Get and Set_Thread_Context on Win9x, with the handle the
kernel keeps, since that system has no OpenThread and thus no way to
suspend a thread it did not make; NT sets the context from ring 0, and
suspends, sets and resumes in a copy.

The tests follow a thread of the target with Stalker, which is what
uses the change, and look a thread up by id in both halves.
2026-08-21 19:22:47 +02:00
Ole André Vadla Ravnås da40616292 barebone: Hook the real Win9x thread entry
A new thread first runs a stub of KERNEL32 that restores its registers
and then sleeps until the one that made it lets it go. The observer
hooked that stub and announced the thread from there, which kept the
thread asleep for good: the release goes missing while the callback
runs, thus the body never ran and ExitThread was never reached.

Take the address the stub returns to from the frame of the suspended
thread that finds it, and hook that instead. It is the routine that
calls the body and then ExitThread, thus the routine and the parameter
move one slot up the frame.
2026-08-21 18:12:05 +02:00
Ole André Vadla Ravnås cddacfd341 barebone: Take the Windows ring lock per frame
A writer took the lock for each piece it put in the ring, so a second
writer could put a frame of its own between the pieces of a large one.
The reader joins pieces into one buffer, and thus mixed the two.

A writer now holds the lock until the whole frame is in, the wait for
room included.
2026-08-21 16:58:42 +02:00
Ole André Vadla Ravnås 63f5a7c13e barebone: Carry frames in a ring on Windows
A script in a copy answered in 137 ms on Win9x and 187 ms on
WinNT, and answers in 1 ms now. The wire was never the cost:
each half slept in slices of 50 ms, and the kernel half of
Win9x served what a copy asked from a timer of 20 ms.

The arena holds a ring per direction: two indices, a lock
for the side with several writing threads, a word for a
writer that wants room, and a record that says how long its
piece is and whether more of the frame follows. Several
frames may be on the way at once. The single slot with its
sequence and its acknowledgement is gone, and so are the
queue, the pump and the reassembly around it.

GLib does the waiting. Both halves block in an iteration of
their context, and a source of ours answers whether the
transport, a ring, a request to write shared code or a
change of gating wants a turn. Every one of those now wakes
the other side: WinNT sets an event, Win9x calls VWIN32 to
reach a thread of ring 3 and signals a semaphore of VMM the
other way, and the answer to a request goes to the thread
that asked rather than to the loop.

VMM says which threads end, which the code that ends one
does not always reach, thus a copy hears of its own that way
and the device stays registered for as long as the agent is.
2026-08-21 16:18:16 +02:00
Ole André Vadla Ravnås d6c61fcc7d barebone: Name the words of a Win9x arena
The word that says whether a spawn is held was written by
its offset alone, thus the next word taken landed on it and
the gating of spawns stopped working. It has a name now, as
do the words that follow. The arena is also cleared whole
rather than only its handshake, since what a copy reads
above that was whatever the memory held before.
2026-08-21 16:18:16 +02:00
Ole André Vadla Ravnås 39e012d836 barebone: Borrow the virtio state per step
Taking a frame off the ring of the device held a borrow of
that state across the callback, and what the callback leads
to sends, which writes the same state. The compiler is free
to keep the fields of the one and of the other in registers
and to write back what it kept. Each step takes the borrow
it needs and gives it back now.
2026-08-21 16:18:16 +02:00
Ole André Vadla Ravnås 65477911ce barebone: Run what the linker gathered for GLib
GLib and GObject each leave a routine behind for a loader to
run before anything uses them, and no loader ever touches
this image. The section they land in has a name now, and
both halves walk it before Gum starts.
2026-08-21 16:18:16 +02:00
Ole André Vadla Ravnås 68b36e6066 barebone: Keep EBP across a Win9x service call
The macro every service call goes through kept the table of
services in EBP and never put the caller's value back. A
thunk that makes a frame of its own hid that; one that does
not, which is what enumerating processes calls, handed the
wreck on. Whether it mattered depended on whether the
compiler kept anything in EBP, thus an unrelated change to
the code flipped it, and the loop of the kernel half then
answered nothing at all.
2026-08-21 16:18:16 +02:00
Ole André Vadla Ravnås 77d1edd259 barebone: Measure the pace of a script on Windows
A session with a script answers one message at a time, and
the tests said nothing about how long that takes. The new
test asks twenty times over, in the kernel and again in a
copy in explorer, and reports the quickest, the median, the
slowest and the spread between them.

The kernel answers in about 1 ms on every flavor. A copy
takes 137 ms on Win9x and 187 ms on WinNT, since its loop
sleeps in slices of 50 ms instead of waiting to be woken.
The test holds the median to a quarter of a second.
2026-08-21 16:18:16 +02:00
Ole André Vadla Ravnås 4af4a8cd2f barebone: Let a WinNT copy wake the kernel half
A frame that finds the channel full waits for the next turn
of the kernel loop, and nothing woke that loop once the copy
took the frame before it, thus the frame stayed where it was
and the command it carried never arrived.

A copy sets the event when it acknowledges a frame now, the
way the other half already does when it takes one. Win9x
still leans on its poll for this.
2026-08-20 23:56:33 +02:00
Ole André Vadla Ravnås a0c051cad8 barebone: Let only the loop wait on one event
A copy in a process can set one event alone, and it sets it
once, thus a second thread waiting on that event took the
wake meant for the loop, and a reply sat in the channel
until the host gave up.

The thread that owns the loop keeps that event now. Another
thread that asks the host something waits on a token of its
own, and the arrival of its reply wakes it there.
2026-08-20 23:49:13 +02:00
Ole André Vadla Ravnås ed3870d6d7 barebone: Name the gating of spawns once
The two ways in carried the body between them, thus a call
crossed the interface where an error is any error at all,
and the compiler said so. Both ways lead to one private
method now.
2026-08-20 14:48:32 +02:00
Ole André Vadla Ravnås b5cc4504b4 barebone: Report where a new Win9x thread begins
What a thread is to run, and the value to hand it, wait on
its own stack above the place the startup stub returns to,
where the starter of KERNEL32 reads them. A script hears
both as the thread arrives, as it does on the other flavor.

A process of this system is also named the way its author
named the program on WinNT now, since that half can read a
version resource, thus the tests find the shell by its file
rather than by its name.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 9b63436fef barebone: Enumerate applications on WinNT
The host could list the programs of a machine, and start
one by name, on Win9x alone. WinNT answers the same way
now: the kernel half reads what the system registers under
App Paths, in the folders the registry itself names, and a
copy walks the menu of the shell for everyone and for the
user who is logged in.

The walk of the menu was written for the other flavor and
is the same work here, thus it moves to a module of its
own, which each half drives with the calls of its process.
The half that reads on the stack of the kernel also lends
that stack to the work it is already doing, since naming an
image happens inside such a walk.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 98902b8c59 barebone: Let go of a wake slot when the wait ends
A token names one wait of one thread, and the next wait has
a token of its own. The table of thirty two slots never let
go of one, and waking a token nobody waited on took a slot
as well, thus the table filled after a few dozen waits and
every token after that shared the first slot: a wake meant
for one thread released another, and the thread that waited
without a limit never woke.

A slot belongs to a token only while somebody waits on it
now, and a wake looks for an owner instead of making one.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 09782eb7a7 barebone: Show where a WinNT thread waits
A copy named the threads of its process and said nothing
more about them, thus a script could not read where one of
them waits. It opens each thread and asks the system, as
the other flavor does. A thread cannot ask about itself,
and says so by leaving the registers out.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås bd8d31658c barebone: Forget the queue of a copy that left
The memory of a copy goes back to the system when it
leaves, and the pump kept a queue named after its arena,
thus every turn read and wrote a page that was nobody's.
The agent then answered nothing more, and letting go of a
session cost the host fifty seconds of waiting. Both
flavors had it.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 2275790507 barebone: Read x64 registers with the x64 flags
The copy asked for a thread's registers with the flags of
the other width, thus the kernel refused every reading and
a program was never seen to reach the instruction that
holds it. Spawning then ran until the host gave up. It now
answers in under half a second.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås b63d04d7da barebone: Report where a new WinNT thread begins
The starter of this system is entered with the routine and
its parameter, thus a script hears both when the thread
arrives and can stand in front of the routine before it
runs.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 59bddab584 barebone: Watch a Win9x copy's own threads
A thread of this system begins in a stub of KERNEL32 that
puts back the registers the kernel gave it and returns to
the start of the program. The copy asks where that stub is
by making a thread that returns at once, and stands in
front of the place where the registers are back, thus a
script hears of a thread on that thread itself.

The slot that holds what a hook stood in front of moves to
the two flavors, since the stub of 9x needs to reach it.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås cda842b89e barebone: Watch a WinNT copy's own threads
A copy answered with the threads of ring 0 and heard of none
that arrived. It walks a ToolHelp32 snapshot now and stands
in front of the thread starter of kernel32 and ExitThread,
thus a script hears of a thread on that thread itself before
it runs any code of the program.

The starter is not exported on x86, thus it is found through
the thunk that leads to it.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 2227140bbc barebone: List a Win9x copy's own threads
A copy answered with the threads of ring 0, since the
router had no entry of its own and called itself. Ring 3 of
this system reaches both the services and the memory the
kernel keeps, thus the copy walks the same list the kernel
half walks and keeps what belongs to it, registers and all.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 3b81e137d6 barebone: Watch threads on a Windows guest
Process.enumerateThreads answered from the flavor already, but nothing
watched, thus a script could not hear of a thread as it arrives.

Win9x hears it from the system: the agent joins the list of devices,
and the message that a thread is ready is delivered on that thread,
before it runs what it was made for. Thus the observer runs there too,
and the thread waits for it. Measured on Win95: the id the observer is
called with is the id of the thread it is called on.

WinNT hears it from PsSetCreateThreadNotifyRoutine, which speaks on the
thread that asked for the new one, thus the observer runs there. The
new thread cannot be held from that call: it is not in the table of
thread ids yet, and looking it up answers STATUS_INVALID_PARAMETER.

A copy in a process leaves the watching to the kernel half.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås aa52adb41a barebone: Identify a Win9x program by its maker
A program was known by its file name, which says nothing of what it is.
It is now named the way the other backends name one: the maker and the
program, in the order a domain is written, from what the image says of
itself. CompanyName and InternalName give it, ProductName and
OriginalFilename stand in where those are missing, and a 16 bit image
is read from its description, which tends to begin with the maker.
A program that says nothing of itself keeps its file name.

The shell's menu is the other half of the list. Only ring 3 can walk a
directory, thus a copy reads the menu, follows each shortcut to the
program it opens, and reads that image as well. The two lists do not
meet on this guest: 21 programs are registered and 30 more are in the
menu, thus both are answered with.

Two things the version resource asks for: a string says how long its
value is in the six bytes before its key, and a value of nothing leaves
the next key where the value would be. A description that names no
maker, "Utility for easily selecting special characters", is no name
for a program, thus such an image keeps its file name too.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 50c220c2f0 barebone: List the programs of a Win9x guest
The system keeps the programs a user can ask for under App Paths, thus
read that list from ring 0 with the registry services of VMM. A program
is known by the name it is registered under, by what its author called
it, and by the file it runs, which goes in the metadata as it does for
a process. One that runs is given the process it runs as, matched by
file name: the registry keeps a path in the short form, and a process
says where it was started from.

spawn takes either now. A name with no separator in it is looked up in
that list, and anything else is a path as before.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 7f9e77b44b barebone: Let one asker at a time reach ring 0
The arena carries one request at a time, and any thread of a process
can ask: a script that hooks shared code, and the maker of processes
that the agent stands in front of once the gate is open. Two askers
would read each other's answers, and a program held on the wrong
answer either keeps the jump to itself or is let go unheld.

Each request now takes the slot for as long as the answer takes.

The new test starts two programs from two threads at the same moment
and holds both: letting one go leaves the other where it was, at the
first instruction of its own image. It passes without this change as
well, since the maker of processes serves one caller at a time, thus
that pair never meets in the slot. A script that hooks while a program
is held is the pair that does.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås a0aac6d179 barebone: Gate spawns on a Win9x guest
A copy of the agent stands in front of the maker of processes while the
host asks for it, and asks for the program to be held whatever the
caller wanted. The program is then held at its first instruction the
way a program the host spawns is, and the host hears of it as it
happens.

Every copy hears the gate through its own arena, thus a copy placed
later stands in front of the maker as well, and the shell gets a copy
when the gate opens, since that is where a user starts a program.

The host keeps what is held, answers enumerate_pending_spawn from it,
and lets resume find the copy that holds each program. A program that
nobody takes is let go by SpawnGatingWatchdog, as on the other backends.

Measured on Win95: a program started from a script in the shell, with
no flag of its own, is held, told to the host, listed, and resumed. The
program waits at the first instruction of its own image: its thread
reads eip 0x401000 and stays there.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 63e605746b barebone: Spawn a 16 bit program on Win9x
Spawning one never answered. The half that holds a process asks the
kernel where its first instruction is, and the kernel takes the lowest
module of the process and reads a PE header from it. A 16 bit program
has a process record that names KERNEL32 and nothing else, thus the
answer was KERNEL32's entry point: eb fe went into the library every
process shares, and the wait for a thread to arrive there never ended.

The kernel says there is nothing to hold for such a program now, and
the half that spawns takes it as it is. This system starts a 16 bit
program whatever the caller asks for, thus spawn answers with it
running, where a 32 bit one is held.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås b97c03e398 barebone: Name a Win9x process from its image
A program was called by its file name, which for a 16 bit one is worse
still: its process record names KERNEL32 and nothing else. The image
says what the program is: FileDescription in the version resource of a
32 bit image, and the first entry of the non-resident name table of a
16 bit one. The file name is left for the programs that carry neither.

The path is already in the metadata, thus what looks for a program by
its file goes there. The helper for spawning is found that way now.

Measured on Win95: Microsoft Solitaire, Windows Explorer, System Tray
Applet, WIN32 Network Interface Service Process, and WELCOME.EXE by
its file name.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 9cb99e12aa barebone: Name a 16 bit program on Win9x
A 16 bit program has a process record that names KERNEL32 and nothing
else, thus every one of them was called KERNEL32.DLL. Such a record
says so in its flags, and the line the program was started with holds
the path. Take the path from there, and keep the module list for the
rest.

Measured on Win95: SOL.EXE is an NE image, and reads as SOL.EXE now.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 4ac4d1ad4c barebone: Carry a frame of any length to a copy
The channel between the halves holds 16 KB, and a longer frame was
dropped where it was written, thus the host waited for an answer that
never came. A real agent is longer: 24 KB hangs, and so does 47 KB.

Win9x runs every process on one page directory, thus what the kernel
half allocates a copy can already read: a frame that does not fit the
buffer is left in memory of its own, and the channel says where. WinNT
cannot lend memory that cheaply, thus a long frame goes in pieces, and
each side tells the other as it goes.

Neither side waits on the other now. A frame is queued and the loop
puts it across as the channel frees, where publishing used to turn on
the spot in the loop that answers the host.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 979316b47c barebone: Give each Win9x wake token its own event
A token names one wait of one thread, and the tokens were hashed into
64 slots. Two of them sharing a semaphore means a wake meant for one
thread lets another through, which reads its own reason, finds nothing
and waits again, while the thread the wake was for sleeps on. The host
then waits for an answer that never comes. Which tokens meet depends on
where they are allocated, thus a guest answered one agent and not the
next.

Each token owns its slot now, on both sides: the kernel half, which
waits on a semaphore, and the copy in a process, which waits on an
event. A wake is given to every thread waiting on the token, since both
let one thread through each time. The line is answered at interrupt
time, where this system makes nothing, thus a wake that arrives before
there is a semaphore is written down for the waiter to read.

The same fault was found and fixed on WinNT in August. Measured on
Win95: with every token in one slot, two tests fail every time before
this change and pass after it; 20 agents in reused guests, where about
one in eight used to hang.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås f1b105b53b barebone: Let go of a guard when its process does
A guard belongs to one process. The one a process leaves behind keeps
its place in the chain and leads straight on, but the next process took
a guard of its own even so, and a page holds 120 of them. An unclaimed
guard is taken over now, and the guards of a copy that has left are
forgotten with it: this system gives the number of a process that is
gone to another one later.

The code also read as a jump to the chain while nobody claimed it, and
Gum follows a jump it finds where a function starts. It is written back
as soon as the last process lets go. The guards stay: a thread of
another process can be in one of them.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås b8fc2947d9 barebone: Put the shared Win9x code back
A hook in a system library is the same memory in every process, thus
the agent puts a chain of guards in front of it, one for each process
that asked. Those guards lead into the image of the agent, and they
stayed there after the image was given back.

The code a hook stands in front of is written the way it was found
before the image goes. The pages that carry the guards stay: a thread
of another process can be in one of them, and from there it reaches the
code that was moved and then the rest of the library, whichever way the
entry reads.

Measured on Win95: the bytes at each of the three places KERNEL32 was
hooked read the same after a session as on a guest that never had an
agent.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 6ee742201e barebone: Take the IRQ line before the device runs
An agent that met a guest where another one had been never came back
from VPICD_Virtualize_IRQ, thus it never reached its own loop and the
host saw every command time out.

The device was told to go before the line was asked for. A device that
is going draws the line, and VPICD does not hand over a line that is
already drawn. The first agent leaves the line quiet, but the host
plugs the port again for the next one, and the device answers that
before the agent asks for the line.

The line is taken first now, and the device is stopped before the line
goes back. Measured on Win95: four agents in one guest, each one giving
the image back, where the second used to hang.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås b9c2c2a37c barebone: End a WinNT agent thread by returning
The agent runs its body on a stack of its own, since the one the kernel
gives a system thread is a few pages and cannot grow. That switch never
came back: it ended where the body returns, thus the stack stayed and
the thread had to be terminated by name.

It puts the stack pointer back and returns now, which lets the stack go
and lets the thread end the way any thread does. PsTerminateSystemThread
is gone from both images.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 6cee8a230d barebone: Give a Win9x guest its memory back
Three things kept a guest from taking the image back, and disassembling
the services showed them.

Unhook_VMM_Fault knows a handler of its own by a head of twelve bytes,
eb 0a ff 25 and the address after it, and reads the way back out of the
handler's first word. The system enters the handler at that word, thus
it is a jump over itself while the handler is in the table, and names
the next handler for as long as the unhooking takes, interrupts off.

A ring 0 thread ends by returning; the thunk spun there, from the days
when the agent never left. VPICD_Force_Default_Behavior does give the
line back for the handle VPICD_Virtualize_IRQ gave.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås a56c6fa706 barebone: Yield to the host only from ring 0
The yield point is code of the kernel, thus the machine is meant to be
in ring 0 when it is reached. Nothing said so: any stop at that address
was taken, and a stop in another ring would have the host call into the
kernel from a place that cannot. The selector is read now and the
machine goes on until it is ring 0. Measured: 0x08 on NT and 0x28 on
Win9x, both of them ring 0.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås e1fd72ddfb barebone: Give the memory back in ring 0
The allocator of a guest is code of its kernel, thus call it where that
kernel runs: the machine is taken to the same yield point the injection
uses, and the memory goes back from there. Calling it wherever the
machine happened to stand still is what a device did before.

Win9x reboots a little after its heap is handed a block of ours back,
from the yield point as well, thus the image stays there for now.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 10cbf957c1 barebone: Detach from a WinNT process
Detaching from a process was answered by Win9x alone, thus on NT the
copy stayed and everything made for it with it. The copy now takes
itself out of the loader and says when it is off its own code; the half
that placed it then unmaps what the process saw, frees the mdls, the
image and the arena, and closes the event, so that a process that is
attached to again is given a copy the way it was the first time.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås b92d8eee1a barebone: Unhook the agent from the fault handlers
The agent puts itself in front of three faults and stayed there: on NT
the gates of the table still named its code, and on Win9x so did the
chain, thus the first fault after the memory went back took the machine
with it. Both are put back the way they were found before the agent says
it has left.

Win9x also asks _HeapFree for what it wants: the second argument is
flags, and the size that is passed by default is read as such. Giving
the memory back there still takes the machine down and is left for
later; NT gives it back.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 37b512de90 barebone: Give the guest its memory back on close
The code the host wrote into the guest stayed there for as long as the
machine ran. The agent says when it is off that code, in a word the host
reads over the stub, and the memory goes back to the guest once it does.

Two more things a closing device owes the guest: a thread of Win9x ends
with VMMTerminateThread wherever it returns, since the thunk that VMM
enters it through cannot, and a machine left standing still stays that
way, thus set it going before letting go of the stub.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås db4fc849b0 barebone: Place one agent copy in a process
A process was given a copy each time one was asked for, thus a session
that came after another one had left added a second image, a second
thread and a second arena to it, and both stood in front of the loader.
The half that places them answers with the copy the process already has.

Win9x has a service for a thread that is done: VMMTerminateThread ends
it and gives back what it held, which the thunk VMM enters it through
cannot do.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 9edfacb5d8 barebone: Let the agent leave when the device goes
The agent stayed in the guest after the device that put it there was
gone, thus the next one found two of them and never got an answer. The
closing device asks it to leave: it answers, unloads the scripts, tells
the copies to leave, gives the line back and resets the device, so that
nothing points into the image. The host lets go of the link, the
monitor and the stub, which take one client at a time.

A ring 0 thread of Win9x cannot end yet: the thunk VMM calls spins
where the thread would return, thus the half that is done blocks on a
token nothing signals.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 8e391bcee1 tests: Wake the barebone test only while it waits
The handler woke the test on every message from the script, and it went
on waking it after the wait it was written for. A message that came in
while the test was closing the manager thus carried it past the close,
which then ran on with nobody waiting for it: the device was still
being taken apart when the test said it was done.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås a6b948712c barebone: Remove a hostlink left by an earlier run
The machine outlives the runs that plug into it, thus a hostlink that
was never unplugged keeps the names the next run needs, and opening the
next one says the chardev already exists. Take the leftovers out before
adding: unplugging is asked for and not done at once, so the chardev is
tried until the guest has let the port go.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 5cc7117c77 barebone: Snapshot Win9x processes in an agent
CreateToolhelp32Snapshot used to fault when a thread the agent made
called it, thus the kernel half had to hand the list over. It works now,
and a test says so: the snapshot walks six processes and finds the one
the copy is in.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås e8a9d6d1ea barebone: Find an export of a module by name
The module of a copy answered only when its exports were enumerated:
Gum asks the module itself for a name and the agent left that out, thus
getExportByName found nothing that enumerateExports lists.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås ff7f70bcea barebone: Watch the Win9x loader with Interceptor
A hook in shared code belongs to one process now, thus the copy attaches
to LoadLibraryA, LoadLibraryExA and FreeLibrary with the Interceptor,
the way the NT copy does. The stub that was written by hand, its table
of processes and the two arena words that named it are gone.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 0ae07cbdaa barebone: Hook Win9x shared code for one process
The code of a library on this system is the same memory in every
process, thus a hook that Gum wrote into one was run by them all, and
ring 3 cannot even make such a page writable. Gum now writes on a page
of its own, and the kernel half puts a guard in front of the code: the
guard names a process and leads to that process's hook, and every other
process goes on to the guard before it, the last of which runs the code
that was there before. Gum's own relocator moves that code, which the
devkit does not declare, thus the agent names it itself.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås e34002579e barebone: Hold a spawned Win9x process at entry
Do here what NT does: let the process run its own loader and stop it at
its first instruction, where none of its code has run yet. Ring 3 cannot
write the private space of another process on this system, thus the copy
asks the kernel half through the arena, and the kernel half finds the
first instruction in the headers of the image as well.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 6db5917ed6 barebone: Hold a spawned WinNT process at entry
The loader must run for a process to be usable, and it takes the
process down if a thread of ours runs it first. Thus make the process
the way the system does, with the maker in kernel32 that tells the
session server about it, and let it start itself with a jump to itself
written over its first instruction. It is stopped there, before any of
its own code, and attaching now works while it is held.
2026-08-20 13:50:05 +02:00
Ole André Vadla Ravnås 3fb975a32b tests: Let the Barebone device options default 2026-08-18 21:16:08 +02:00
Ole André Vadla Ravnås 63a23371ae api: Carry toplevel functions into the public API
Only the base VAPI was scanned for them, and the prototype pattern
predates VALA_EXTERN, so a function declared in frida.vala reached
neither the header nor the VAPI nor the GIR.

List init_with_runtime by hand alongside the rest of the lifetime
API, and leave the merge to icon_from_png and icon_from_rgba.
2026-08-18 21:16:08 +02:00
Ole André Vadla Ravnås 02c6631782 device-manager: Let a Barebone device be presented
Take a name and an icon through options, the way a remote device takes
its own, so an embedder can say what the machine behind the stub is.

Build the icon with icon_from_png or icon_from_rgba, which is what a
provider's own icon is made of.
2026-08-18 21:16:08 +02:00
Ole André Vadla Ravnås 5fdb3d5402 barebone: Reach the loader in a held NT process
A process that is still held has neither a list of its libraries nor a
heap of its own. Take the loader's address from a process that runs,
since this system puts it at the same place in every process, and pass
it through the arena. The copy makes a heap of its own when it finds
none, and the thread that carries the copy no longer walks a list that
is not there.
2026-08-18 17:23:37 +02:00
Ole André Vadla Ravnås e4f17a9802 device-manager: Take a name for a Barebone device
The caller knows what the target is, whereas the backend has only its
own name to offer.
2026-08-18 16:43:33 +02:00
Ole André Vadla Ravnås b6ef27640a barebone: Leave the module registry its reference
The registry is a singleton the caller does not own, and every
module we told it about let go of a reference all the same. It
was thus taken apart under our feet, which unhooked the loader
from inside its own stand-in and left the thread there for good:
CreateProcessW never came back in a process the agent was in.

Making a process the way the loader does is no longer needed,
thus that stand-in goes.
2026-08-18 16:03:58 +02:00
Ole André Vadla Ravnås a400720771 barebone: Sleep and wake on the token in a copy
A copy of the agent has more threads than one, and both entry
points threw the token away and used the event that the kernel
half sets. A wake thus released whichever thread happened to be
waiting. Keep an event for each token, release every sleeper on
it, and let the loop keep the event the kernel half can set.
2026-08-18 16:03:58 +02:00
Ole André Vadla Ravnås cb36d358a0 barebone: Watch the Win9x loader for modules
The loader of this system lives in memory that every process
shares, thus a stand-in on it runs in all of them and a copy of
the agent cannot own it. Put a stub in the shared arena instead:
it asks who is running, and either leads to the copy that the
process has or goes straight on to the loader. The copy then
tells the registry from the thread that loads, before the
library is handed back to whoever asked for it.

Both entry points that load by name are covered. The wide ones
are stubs on this system: they set ERROR_CALL_NOT_IMPLEMENTED
and give back nothing.
2026-08-18 15:36:00 +02:00
Ole André Vadla Ravnås 2d0825b126 tests: Hook a Win9x process before it runs
Spawn holds the process, thus a stand-in put in place from a
script is there before the program's first instruction.
2026-08-18 15:36:00 +02:00
Ole André Vadla Ravnås 3dfb79dbe9 barebone: Query memory from a Win9x copy
A copy of the agent runs in ring 3, where the service that reads
the page tables belongs to the ring 0 half alone. Asking for the
protection of an address, or for the ranges of the process, thus
faulted in a copy. Take both from VirtualQuery instead.
2026-08-18 15:36:00 +02:00
Ole André Vadla Ravnås 7cfaeb8cf0 subprojects: Bump outdated 2026-08-18 15:32:05 +02:00
Ole André Vadla Ravnås 59e736fad7 submodules: Bump releng 2026-08-18 15:32:04 +02:00
Ole André Vadla Ravnås 78518b7b0c Require the platform's GIO half
Keep pkg-config's view of the API the same as the devkit's, which
now carries GUnixFDMessage.
2026-08-18 15:25:03 +02:00
Ole André Vadla Ravnås df8247bca2 Compile the device serial only for barebone
The counter is only used when adding a Barebone device, so it
became an unused field whenever that backend is off.
2026-08-18 13:20:33 +02:00
Ole André Vadla Ravnås 56ccdc87b3 barebone: Drop unowned where it is default 2026-08-18 13:19:14 +02:00
Ole André Vadla Ravnås 03cde6eb1a tests: Drop static from methods of a namespace
A method that is not in a class has nothing to be static about.
2026-08-18 12:42:39 +02:00
Ole André Vadla Ravnås cbb2f13976 barebone: Tell the registry of a module once
The loader calls in again for a module the process already has,
and each call put the module in the registry anew. Keep the ones
we told about, and hold the registry while telling it, the way
the Windows backend of Gum does.
2026-08-18 12:41:56 +02:00
Ole André Vadla Ravnås da6d0d8521 barebone: Send every byte of a frame to the host
A frame that did not fit the ring went out cut short, and the
rest was thrown away. The host then waited for bytes that never
came, and every command after that timed out. Send what fits and
leave the rest at the head of the queue, since the host reads a
length and then that many bytes.
2026-08-18 12:41:56 +02:00
Ole André Vadla Ravnås e92c387472 barebone: Defer the wake out of the interrupt
The routine of an interrupt runs above the level at which the
dispatcher of the system may be touched, and it set the event
there. That left the thread the wake was for asleep now and
then, and the command the host waited for timed out. Queue a
deferred call from the routine and set the event from there.

This was the flake: a sweep that missed one run in two or four
now passes.
2026-08-18 12:41:56 +02:00
Ole André Vadla Ravnås d4e65ef7a1 barebone: Sleep and wake on the token given
A token names one wait of one thread, and both entry points of
the loop library threw it away: one event served every waiter,
so a wake released whichever thread happened to be waiting and
left the thread it was meant for asleep. Two tokens could also
fall in the same slot and share an event.

Keep an event for each token, release every sleeper on it, and
let the loop keep the event a copy in a process can set.
2026-08-18 12:41:56 +02:00
Ole André Vadla Ravnås 30363e1e65 barebone: Let the session server know our threads
Making a thread asks the session server to take it on, and that
server answers only the threads it has a record of. It never
heard of a thread the agent made, thus such a thread waited
inside CsrClientCallServer for an answer that never came, and a
script calling CreateThread hung.

Put a copy of the agent in the server, and let it make the
record with the server's own CsrCreateThread, under the lock
that CsrLockProcessByClientId takes. The kernel half names each
thread it starts in that copy's arena. Threads then reach the
server as any other thread does.

Making a process still goes the way the loader does it, since
CreateProcessW does not come back in a process the agent is in.
2026-08-18 12:41:56 +02:00
Ole André Vadla Ravnås abfd615a6c barebone: Watch the loader from where it takes
A copy counts as running in a process only once its frames are
routed, and that happens after Gum starts. The watch on the loader
asked before then and never went on, thus the stand-ins were never
put in place and the registry alone reported a module.
2026-08-18 12:41:56 +02:00
Ole André Vadla Ravnås 1edd01be5c barebone: Document the agent's flavors
The README still spoke of two kernels, and named the arm64 SDK throughout
even though each flavor has one of its own.

Say which machine and Rust target each is built against, and how the
Windows ones ask for a target Rust does not name. Also fix the SDK name
the Linux Makefile suggests.
2026-08-18 10:05:12 +02:00
Ole André Vadla Ravnås 88871f7ad3 Guard the QMP client's UNIX socket use
UnixSocketAddress does not exist on Windows, so the build broke there.
Refuse a unix: address like the other transports do.
2026-08-18 00:12:09 +02:00
Ole André Vadla Ravnås d252d7f8b1 subprojects: Bump outdated 2026-08-17 21:10:40 +02:00
Ole André Vadla Ravnås ae5f2e49bc submodules: Bump releng 2026-08-17 21:10:40 +02:00
Ole André Vadla Ravnås 6bf69ace8e Make the generated vapi self-contained
Delegates the public API refers to were never emitted, so the vapi did
not resolve WebRequestHeaderFunc, and RpcClient.call needs json-glib.

Emit the referenced delegates, and declare json-glib as a dependency.
2026-08-17 20:48:03 +02:00
Ole André Vadla Ravnås 34e930c4cc Keep Gee and Json out of the public API
The Barebone config types exposed Gee.List and Gee.Map through their
properties, and implemented Json.Serializable. Bindings cannot resolve
either.

Make those properties internal and reach them through clear/add/enumerate
methods, like PeerOptions does for relays. Drop Json.Serializable from
the generated gir and vapi.
2026-08-17 20:48:03 +02:00
Ole André Vadla Ravnås 98b3b82037 Say how a Barebone config reaches the backend
A device added by the user is configured through the config itself,
the environment variable applying only to the default device.
2026-08-17 19:13:13 +02:00
Ole André Vadla Ravnås 5972ab3a7d Add API for removing a Barebone device
Give each such device an ID of its own, device IDs having to be
unique, and keep the Barebone entry points together after the remote
ones.
2026-08-17 19:12:50 +02:00
Ole André Vadla Ravnås 3ca20ab9e8 Move the Barebone config types to the public API
The device manager's entry point takes a config, thus its type must
exist even where the backend does not, or the build breaks on every
target without it.
2026-08-17 18:58:24 +02:00
Ole André Vadla Ravnås 3e1b189109 api: Keep abstract classes in the public API
The generator dropped every abstract class, so a subclass reached the
GIR with a parent that was not there.
2026-08-17 18:57:44 +02:00
Ole André Vadla Ravnås 4b3e1b2dc7 barebone: Watch the NT loader for modules
Stand in front of the loader's way in and way out, thus a script
hears about a library the moment it arrives and can instrument it
before the process uses it.

Win9x keeps to the list it gets at injection: its loader lives in
memory that every process shares, so standing in front of it would
send every process in the system through the agent.
2026-08-17 17:38:56 +02:00
Ole André Vadla Ravnås 8a6759e1b5 barebone: Enumerate an NT process's modules
The loader keeps the list in the process itself, thus the copy reads
it where it runs, and each module says what it exports through its
own headers. A script in a process now finds a function by name, as
it does on Win9x.
2026-08-17 17:24:24 +02:00
Ole André Vadla Ravnås c989617b16 barebone: Spawn and resume on Windows NT
Make the process through the loader's own entry points, which take
wide strings and leave the first thread held. The ones in kernel32
ask a thread for state that only the loader gives the threads it
starts, and a thread that the agent made has none of it.

Reading what a held process is called faults, nothing having touched
those pages yet. Thus read it the way that recovers from a fault,
where a plain read took the system down.
2026-08-17 16:43:02 +02:00
Ole André Vadla Ravnås e531cafe57 barebone: Enumerate a Win9x process's modules
What a process has mapped is written down where only ring 0 can read
it, thus the kernel half hands the list over when it injects, and the
copy reads it from memory of its own ring. Each module then says what
it exports through its own headers, so a script can find a function
by name.

The list is what the process had when the copy arrived. Also take a
backslash for a separator when naming a module.
2026-08-17 16:19:07 +02:00
Ole André Vadla Ravnås 9bd2f246c4 barebone: Tell script messages apart by process
Each copy numbers its scripts from one, thus two attached processes
gave the host the same name twice and each session heard both. A
message now says which process it comes from.
2026-08-17 15:52:43 +02:00
Ole André Vadla Ravnås 6b2d5e76c0 barebone: Spawn and resume on Win9x
Only ring 3 can make a process. Thus one process holds a copy of the
agent, and that copy makes each new process held and resumes it when
asked.

A held process runs none of its own code yet, thus a session in it
starts before the program does.
2026-08-17 14:48:52 +02:00
Ole André Vadla Ravnås 8eb7d3e5dd barebone: Name the linker scripts alike
Each script says which kernel and which word size it is for.
2026-08-17 14:48:52 +02:00
Ole André Vadla Ravnås 6021127a17 barebone: Inject from the guest, not the host
The agent keeps the writable half as it was before it ran, and it
keeps its own relocations in the image. Thus it can make a copy for
a process itself: it copies that half and moves each address in it
to where the copy runs, on both kernels.

The host now asks only for the process. It reads no image, stops the
guest for nothing, and writes no memory.
2026-08-17 14:48:52 +02:00
Ole André Vadla Ravnås e7ba5aa46f barebone: Lend the Win9x code to ring 3
Show the pages of the kernel half at a second address instead of
putting a second copy of the image there. The process then runs the
same code, and only the half that gets written to is new memory,
which takes 5 MB less and leaves the host 134 KB to write in place
of 5.2 MB.

A position-independent image cannot name its data in an instruction,
thus the service wrappers and the fault thunk read the program
counter and reach the table from there. A second address for a page
that the memory manager does not own takes the user bit only.
2026-08-17 14:48:52 +02:00
Ole André Vadla Ravnås 93c0d173d9 barebone: Build the XNU agent against the SDK
Use the bare soft-float SDK and picolibc, as the other flavors do,
in place of an external toolchain and newlib. This retires the libc
stubs that only newlib asks for, and the symbol lookup that the blob
flavors already share.

The flavor did not build: the kernel facade had grown a fault
reporter and a CPU state that this backend never got. It reports no
fault yet, the exception vectors being unhooked.
2026-08-17 14:48:52 +02:00
Ole André Vadla Ravnås 9219ee6bd8 barebone: Attach to a process on 32-bit NT
Manufacture the ZwCreateThread stub for both word sizes, seed the
stack the way a cdecl entry reads it, and set the data segments,
which the kernel takes from the context on x86.

The agent is position-independent now, thus the process runs the
same code as the kernel half, as it does on x86_64. A fault thunk
cannot name data in such an image, thus it asks for the record it
resumes from, and it returns to the previous handler through the
stack, which also retires a global.
2026-08-17 14:48:51 +02:00
Ole André Vadla Ravnås 99c29a9891 barebone: Run scripts in the attached process on NT
The copy answered nothing because two things still went to the
kernel half's world: replies went to the hostlink, which belongs
to that half and is not reachable from ring 3, and the process id
came from a flavor gate that only Win9x passed. Both now follow
the ring the code runs in.

attach(pid) works: the script runs in the target, says which
process it is in, and messages reach it and come back.
2026-08-17 09:18:46 +02:00
Ole André Vadla Ravnås e6f078e196 barebone: Make the NT thread the way the system does
This kernel exports no ZwCreateThread, and a thread of the
system's that is sent to ring 3 by hand makes Windows tear down
the hostlink's interrupt. But a Zw stub is only a service number:
it builds the frame the dispatcher expects, loads an index and
jumps to it. Read those two addresses out of an exported stub,
take the index from ntdll's own stub for the service, and write a
stub with an indirect jump, which pool memory needs.

The copy now runs on a thread the kernel made, with a block of
its own. It comes up through Gum, says which process it woke up
in, and takes the frames the kernel forwards. Lending its text
read-only is enough, so nothing writable is shared.

What it does not do yet is answer: the script it is asked for
never finishes being created.
2026-08-17 08:53:14 +02:00
Ole André Vadla Ravnås df59e43004 barebone: Bootstrap the NT copy through ntdll
The thread that descends has no block of its own, thus the copy
lost gs at its first syscall. ntdll's NtCreateThread is a syscall
stub, which needs no block, so the descended thread now only asks
for a real thread and retires. The kernel gives that thread a
block, and the copy runs through Gum and the JS context on it.

The descent underneath is unsound, though: a thread made by
PsCreateSystemThread that executes in ring 3 makes Windows tear
down the hostlink's interrupt, with the copy doing nothing but
spin. That is the next thing to replace.
2026-08-16 22:59:22 +02:00
Ole André Vadla Ravnås f613a8788f subprojects: Bump outdated 2026-08-16 22:23:24 +02:00
Ole André Vadla Ravnås d12e6146ba submodules: Bump releng 2026-08-16 22:23:23 +02:00
Ole André Vadla Ravnås 1d7d1ff878 barebone: Select the agent's primitives once
Each primitive that differs between the two halves examined the
privilege level and then did one of two things. Put the two sets
in groups instead, and let the entry point of the half that runs
in a process select its group before it does anything else. Each
function is now one thing, and each flavor keeps that half in a
file of its own.
2026-08-16 21:39:41 +02:00
Ole André Vadla Ravnås c7160ba8ff barebone: Stand the NT copy up in ring 3
Gum comes up in the injected copy now, and the JS context with it.
What stood in the way was a handful of primitives still reaching
for kernel addresses from ring 3: the page tables it may not read,
the thread id it may not ask the kernel for, and yielding. Each
now goes through the front door instead.

Placement no longer hangs either. The event the loop sleeps on was
only recorded if it won a race for a slot in the hashed table, and
a token it collided with could take that slot first, leaving the
copy to be handed a null object to open.
2026-08-16 20:14:38 +02:00
Ole André Vadla Ravnås c2d0c59acf barebone: Start the NT agent's copy in a process
This kernel exports no way to make a user thread, so one is made
the long way: a thread of the system's inside the target, which
hands itself down a ring through a frame laid out by hand. What it
lacks on the way down is a block of its own, so it is given one.

The copy gets that far and finds its way around ntdll; what it
does not survive yet is bringing up Gum, so the test that asks for
all of it is red.
2026-08-16 20:14:38 +02:00
Ole André Vadla Ravnås 4092a9c185 barebone: Add the NT agent's ring 3 half
The copy the host places in a process runs the same code as the
kernel half and tells itself apart by the ring it is in, since
that is the one thing the shared image cannot carry. What it needs
underneath comes from ntdll, found through the loader's own list.
2026-08-16 20:14:38 +02:00
Ole André Vadla Ravnås 16e215c466 barebone: Let the NT halves wake each other
Neither side can reach anything of the other's, but a handle
travels: the loop's event is made one the object manager knows,
and each attached process gets one of its own to wait on. Both
handles are left in the arena.
2026-08-16 20:14:38 +02:00
Ole André Vadla Ravnås 82ee4438cb barebone: Route the hostlink's interrupt on NT
The device raised its line from the start and nothing ever came of
it: the chipset leaves the link of a device no driver claimed
switched off, so the assertion reached no controller at all. Point
the link at the interrupt its config space already names.
2026-08-16 20:14:38 +02:00
Ole André Vadla Ravnås bd0e97e31f barebone: Pump the NT relay from the main loop
A GLib timeout source at the rate the relay wants leaves the NT
agent alive but never answering, even with an empty callback, while
the loop itself keeps coming around at that very rate. Drive the
relay from there instead.
2026-08-16 20:14:38 +02:00
Ole André Vadla Ravnås 9653f6fd83 barebone: Give the NT halves a channel
The two copies do not share an address space, so what passes
between them is a region each sees at an address of its own, and
everything in it is self-relative: a pointer stored there would
mean nothing on the other side. Placing a copy now sets one up
and answers with both views of it.

The routing that carries a frame to the copy running inside a
process is no longer one flavor's, so its gates widen to cover
this one.
2026-08-16 20:14:38 +02:00
Ole André Vadla Ravnås d042b214c1 barebone: Place the NT agent in a process
The code is lent to the process as it stands, since code is only
ever read and the image is position independent; the half that
gets written to is made fresh, which is what keeps the two
copies from sharing state. Where the code lands is the memory
manager's to choose, so nothing is at a fixed address, but how
far the writable half sits from it belongs to the image and is
asked for by address.

The fresh half is pool the kernel can still name, so the host
fills it in where it is, having relocated the image for where
the process will run it.
2026-08-16 20:14:38 +02:00
Ole André Vadla Ravnås 294f9a64d7 barebone: Close the handle to a thread we start
Nothing here waits for one, and a handle left open keeps the
thread around long after it has seen itself out.
2026-08-16 20:14:38 +02:00
Ole André Vadla Ravnås b93de2d8c8 barebone: Build the Windows agent PIC
Its text is to be mapped a second time into each process being
instrumented, shared, since only what is written to has to be
private, and text read from two addresses at once cannot hold
either of them. That settles the code model too: reaching the
rest of the image RIP-relative needs none.

Such an image states its relocations twice, the ones it was built
from and the dynamic ones a loader applies, and only the latter
covers the tables the linker synthesized, so on a position-
independent image it is the whole story. It also says what it
wants in its addend rather than in the slot, which is left empty
-- taking the slot instead leaves every pointer in the image
holding the base and nothing else.
2026-08-16 20:14:38 +02:00
Ole André Vadla Ravnås 88b4c57c9b barebone: Report what an NT thread was doing
Win9x asks a service for a thread's registers rather than
reading them out of anything, and NT has one too, exported since
the kernel was young. It will only answer about a thread with a
user-mode half to report: asked about one living entirely in the
kernel, it walks off the end of that thread's stack and takes
the machine down with a 0x50. A process without a block of its
own has no such threads, which is what tells the two apart, and
a thread cannot be asked about itself.

The shape of a CONTEXT is part of the architecture's own
contract, so unlike everything else read out of the kernel here
those offsets are simply known.
2026-08-16 20:14:38 +02:00
Ole André Vadla Ravnås 8284f0d0d4 tests: Run the NT suite against both word sizes
The same guest is described the same way whatever its width, so
the tests take the prefix that names it and a second suite aims
them at a 64-bit one. One of them is new: the compiler backend
has to agree with the kernel about where arguments go, which is
a different answer per word size and one nothing else here would
notice getting wrong.
2026-08-16 20:14:38 +02:00
Ole André Vadla Ravnås 67e4bd2c9e barebone: Widen the NT agent to 64 bits
Every kernel call goes through the convention of the word size it
was built for, and the pointers the agent reads out of the kernel
follow suit. A gate is twice as wide and keeps its handler in
three pieces; a long-mode interrupt frame carries the stack
pointer, so a caught fault resumes through the frame rather than
through a record of its own, and one carrying an error code has
that taken off as well before iret reads it. The self-map walk
moves out of winnt.rs, where the 32-bit form asks about each page
in turn and the 64-bit one descends four levels instead.

The image is built large-model, so its sections come l-prefixed
and the linker script has to say where they go, and the target
Rust ships is no use: it is kernel-model and position
independent, which the C archives cannot be linked against.

Two things a 64-bit kernel is stricter about. It bugchecks with
0xc4 (0x91) if a system service is entered from a stack it did
not hand out, and the agent has run on one of its own since
bring-up to have room for the script runtime, so anything reached
that way is now done by a thread which never left. And its
processes and threads are about twice the size, which is past
where the walk deriving their list looked.

While here, two things that were wrong at either width: a process
reports where it came from in whichever form it was given, and
only a drive letter wants the directory of symbolic links in
front of it; and the two forms of the optional header put their
data directories in different places, which is why a 64-bit
image appeared to have no resources at all.
2026-08-16 20:14:38 +02:00
Ole André Vadla Ravnås 1d097f2f5a barebone: Teach the host to drive a Windows kernel
Three things it had to learn, none of which a System V target
ever asks for.

Windows passes the first four arguments in rcx, rdx, r8 and r9,
and has the caller leave four spill slots beneath the return
address rather than a red zone above it, so the machine takes
the convention from the kernel it is talking to. While here,
skip past the return address when reaching for a stacked
argument.

The address a call returns to is one the rest of the kernel runs
through as well, so arriving there was never enough on its own:
only the call we made comes back on the stack we handed it.
Both x86 machines now say so, the 32-bit one allowing for a
callee that took the arguments off.

And a relocation against the text section is only nothing to do
where the code reaches its own image with an instruction-encoded
displacement. Reaching it by absolute address, as the large code
model does, needs every one of them applied.
2026-08-16 20:14:38 +02:00
Ole André Vadla Ravnås 7730f1c65d barebone: Map out a 64-bit kernel on Windows NT
The control region is no longer at an address fixed since NT was
young: a 64-bit kernel puts it wherever it likes and reaches it
through GS, which points at it in kernel mode and at the current
thread's own block in user mode. Only the half that is in GS
right now can be read back, so a processor caught in user mode is
let run on and caught again.

Everything the walk reads afterwards is the same structure at
twice the width, save the version block, which was the 64-bit
form on a 32-bit kernel all along.
2026-08-16 20:14:38 +02:00
Ole André Vadla Ravnås 21ec918bfa barebone: Read both forms of the optional header
The data directories start in different places depending on
which form an image was built in, and a 64-bit kernel is
built in the one we were not reading.
2026-08-16 20:14:38 +02:00
Ole André Vadla Ravnås 3171b8bade barebone: Widen a module's and symbol's address
Both were kept in 32 bits, which is all a kernel below the
4 GiB line ever needs, and all any flavor has needed so far.
The 64-bit Windows kernels sit far above it, and the address
is absolute wherever no image was configured, so the field
has to hold one.

The width reaches the agent through the config it is handed
and the symbol table packed alongside it, so both change
with it. The entry laid over that table says what it is
worth: it must pack exactly as written, or the name that
follows moves.
2026-08-16 20:14:38 +02:00
Ole André Vadla Ravnås cf3a9616ff barebone: Answer questions about symbols
The host already hands over a symbol table, so the calls
that turn an address into a name were answering with
nothing for want of asking it. Wire up the four of them,
plus the global export lookup, for every flavor the host
injects.

Also say what a live-guest script sent when it fails to
match, since the message is the only account of what
happened in the guest.
2026-08-16 20:14:38 +02:00
Ole André Vadla Ravnås 5c7bff1561 barebone: Enumerate ranges on both Windows flavors
Process.enumerateRanges answered with nothing, so Memory.scan
had nowhere to look. There is no VirtualQuery to lean on for
kernel space, but both flavors can read their own page
tables: NT through the tables it maps into itself, Win9x by
asking VMM to copy an entry.

Only the half above the split is worth reporting, since the
other one belongs to whichever process happens to be
current. Answering gum_memory_query_protection falls out of
the same read.
2026-08-16 20:13:04 +02:00
Ole André Vadla Ravnås 05c46c9e50 barebone: Add a Windows NT flavor
Run scripts in ring 0 on Windows NT, starting with XP on
x86, alongside the Win9x flavor.

The host finds its way around without a PDB: the processor
control region sits at a fixed address and points at the
kernel debugger's version block, which names the loaded
module list, and every module's exports become symbols. The
agent is brought up on a thread entering
NtWaitForSingleObject, at PASSIVE_LEVEL on its own stack.

NT's exports are ordinary stdcall rather than VMM's
register-based services, so the slots are called directly
with no thunks. Memory comes from the non-paged pool, where
a driver's own code lives; threads from PsCreateSystemThread,
running on a stack of the agent's own, since a system thread
gets a few pages and cannot be asked for more.

Faults are caught by taking the gates for #UD, #GP and #PF.
The kernel takes paging faults constantly, so anything
raised outside the agent goes straight on to the handler we
displaced; a caught one resumes through a record, since Gum
unwinds onto a stack of its own that iret would not reload.

Processes come from the list head the debugger's data block
publishes and the accessors the kernel exports, their path
and argv from each one's own user-mode block, their icons
from the image on disk. Threads follow the caller's own
links round to its process.

The pool is mapped with large pages, which cannot be
narrowed and need not be, and hands out blocks aligned to
less than a page, so the allocator now names each argument
its target expects.
2026-08-16 20:13:04 +02:00
Ole André Vadla Ravnås 1f5e54c8d9 tests: Boot a Windows XP guest from vm.py
Same treatment Windows 95 gets, so the two share everything
but their name, their processor, and where their disk hangs.
The image exported from VMware puts the system disk on a
SCSI adapter, and handed over as IDE the kernel bugchecks.
2026-08-16 20:13:04 +02:00
Ole André Vadla Ravnås 3aa6ea65d5 barebone: Extract a PE reader from win9x
The Windows NT flavor walks the same export directories, and
Vala flattens namespace constants, so two copies of the PE
layout will not compile. Turn the targeted lookup into an
enumerator both can use.

Read the directory a page at a time: part of an image may be
paged out, and taking it in one go turns one absent page in
the middle into no exports at all.
2026-08-16 20:13:04 +02:00
Ole André Vadla Ravnås f8c576118c barebone: Close the handle to a thread we start
The same on Win9x as on NT: nothing here waits for one, and a handle
left open keeps the thread around long after it has seen itself out.
2026-08-16 20:13:04 +02:00
Ole André Vadla Ravnås 1a5f90f5ca barebone: Let the agent's loop sleep until woken
The loop came around every 50 ms whether or not anything had
happened, because that slice was what actually delivered frames.
Now that the hostlink's interrupt arrives, iterate blocking and
let the wake do it. The copy injected into a target keeps a slice
of its own: nothing on the other side can wake it.
2026-08-16 20:13:04 +02:00
Ole André Vadla Ravnås 22f8823838 barebone: Reset receive state before dispatch
Handling a command can issue a synchronous host RPC, which re-enters
process() to collect its reply. The virtio hostlink dispatched a
completed frame while its receive state still said one was complete, so
the nested pass could hand the same frame over again.

The other two hostlinks already detach the frame and reset first; this
makes virtio agree with them.
2026-08-16 20:13:03 +02:00
Ole André Vadla Ravnås 3efdb6d821 barebone: Park on an invalid opcode in the agent
Windows answers a fault in its own kernel by retrying the instruction.
An opcode never becomes valid, so one of ours left to it stops the
machine with nothing to show for it.

Faults arrive with EBP pointing at the frame the processor and VMM's
dispatcher built -- registers, error code, EIP, CS, flags -- so the
faulting address and ring are there to read. When an invalid opcode in
ring 0 turns out to be ours and Gum does not want it, say so and park
the thread.

Everything else still goes to Windows: a page fault on a page it has yet
to bring in is how paging works, and declining that one is what lets
Gum's own recovery see it.
2026-08-16 20:13:03 +02:00
Ole André Vadla Ravnås a4e48811f4 barebone: Scan memory ranges on x86
Memory.scan said the machine did not support it. The other backends
place a scanner in the guest and run it there; x86 reads the ranges and
matches them here instead, which for the sizes a script asks about costs
less than getting a scanner in place would.

Masked tokens kept the value where the mask belonged, so they matched
whatever the value happened to be.
2026-08-16 20:13:03 +02:00
Ole André Vadla Ravnås b552b3a157 barebone: Find VMM without sweeping the arena
Locating VMM's descriptor block asked the machine to enumerate its
ranges, which walks the whole address space a table at a time over GDB,
and then swept every range in the system arena.

Windows maps the page tables into every context, so the pages at the
bottom of the arena -- where VMM is, and nothing else needs looking at --
are one read away. Targets without that mapping, such as the synthetic
one the unit test builds, still go the long way round.

2.9s to 0.1s on a live guest.
2026-08-16 20:13:03 +02:00
Ole André Vadla Ravnås e35d1f9973 barebone: Reclaim the injected agent's memory
Detaching left the copy's arena, stack and image behind, and handing
them back broke the next injection into that process.

The thread was still in them. It reaches the agent by way of a stub that
ends in a spin, so returning from the entry point parked it there for
good; it now leaves through ExitThread instead. With both threads really
gone, the pages go back, and the shared arena hands the very same ones
out again.
2026-08-16 20:13:03 +02:00
Ole André Vadla Ravnås 93b46273d0 tests: Snapshot and rewind the Windows 95 guest
Booting takes minutes, so the live tests want a snapshot to rewind onto,
and getting one has so far been a matter of knowing which QMP calls to
make in which order.

Readiness is something to look at rather than wait out: the mode change
out of text is the shell coming up, and the adapter Windows cannot find
leaves a dialog over the desktop that has to go first.

Rewinding has an order to it too. The snapshot is of a machine that never
had a hostlink port, and the port a run leaves behind goes away
asynchronously, so restoring too early leaves QEMU unable to load the
device's state -- and every rewind after that runs a wedged machine.
2026-08-16 20:13:03 +02:00
Ole André Vadla Ravnås 3235509874 barebone: Tear down the injected agent on detach
A session held the copy in its target for good: its scripts stayed
loaded and both of its threads kept running. Sessions now own the
scripts they create and discard those when they close, and the copy
itself goes when the last session watching that process does.

Each attached process gets its own copy, too. They are placed in the
shared arena, so the one image the host used to reuse had two processes
writing the same globals.

The copy's pages stay behind. Handing them back leaves the target unable
to take another injection, so they are kept rather than freed into a
state that breaks the next attach.
2026-08-16 20:13:03 +02:00
Ole André Vadla Ravnås 437756a2a0 barebone: Route agent frames to the target
Scripts in an attached process needed their own commands, and posting to
one would have needed a third. The framing gains a destination instead:
zero is the agent in the kernel, anything else a process it injected a
copy into, and that half passes the frame along and relays whatever
comes back without looking inside.

The copy already knows how to answer, since it runs the same dispatch
over an arena channel rather than the hostlink, so create, load, destroy
and post all work by addressing them at it.
2026-08-16 20:13:03 +02:00
Ole André Vadla Ravnås 87ff1f02cb barebone: Run scripts in the target on Win9x
Scripts for an attached pid ran in the kernel half, which is the wrong
process to be inspecting. Pass the source to the copy injected there
instead, and carry its messages back through the shared arena, since
that copy has no hostlink of its own.

Loading stays a step of its own: a script that ran the moment it was
created would be sending before the host had anywhere to put it.
2026-08-16 20:13:03 +02:00
Ole André Vadla Ravnås 965e8d1142 barebone: Report the target's process id on Win9x
Gum's barebone backend has no process to speak of, so it answered zero.
The injected copy knows better: KERNEL32 tells it which process it woke
up in.
2026-08-16 20:13:03 +02:00
Ole André Vadla Ravnås f82e921410 barebone: Run Gum in the injected agent
The hostlink belongs to the kernel half: the injected copy has its own
transport global, never initialized, and driving the same virtio queue
from both halves wedged the one the host talks to. Give the injected
copy a loop that only dispatches its own work.

The Exceptor stays behind too, since fault handling is machine-wide and
seizing it from the half already using it is not the injected copy's
call. Reporting moves to the worker, so an answer means the thread that
runs scripts came up.
2026-08-16 20:13:03 +02:00
Ole André Vadla Ravnås 7b8e6c401a barebone: Give the injected agent its own image
Both halves shared one image, so they shared its globals: bringing the
user-mode side up tore down the state the kernel side was running on.
Place a second, separately relocated copy in the target instead, and
start the injected thread at its entry point.
2026-08-16 20:13:03 +02:00
Ole André Vadla Ravnås da0badaa57 barebone: Add an allocator over reserved memory
Injection halts the guest to write, so a region that has to be asked for
while it runs cannot be allocated from inside.
2026-08-16 20:13:03 +02:00
Ole André Vadla Ravnås efa84266aa barebone: Tolerate paged-out KERNEL32 exports
Sweeping for the image means most candidates are not KERNEL32, and one
carrying a PE header points the export directory anywhere at all. Parts
of the real name table are paged out too, and a debugger read cannot
fault those in.
2026-08-16 20:13:03 +02:00
Ole André Vadla Ravnås 8ccf2ab2b4 barebone: Bring up a worker in the injected agent
Everything the user-mode half goes on to do runs on threads
of its own, so it starts one before reporting in. attach()
answering now means the agent is able to work, not merely
that it was placed.
2026-08-16 00:16:02 +02:00
Ole André Vadla Ravnås 65c7d261b1 barebone: Create the agent thread in the target
CreateThread builds a thread against whichever process
KERNEL32 believes it is in, which is the worker, not the
target. Its inner creator takes the process explicitly, so
the thread's own records belong to where it will run and
nothing has to be patched afterwards.

Create it suspended: the first frame is seeded on a stack
carved from the calling process, so the target needs those
bytes at the same address before the thread may run. The
seeded frame comes from Get_Thread_Context; TCB+0x1c is
only filled in once a thread has run.

Neither the creator nor the resume behind ResumeThread is
exported, so both are found by the shape of the code that
calls them.
2026-08-15 23:25:22 +02:00
Ole André Vadla Ravnås 2646e54ce0 barebone: Wait and wake in Win9x user mode
One auto-reset event per token slot, mirroring the
semaphores the kernel half uses.

spawn_thread stays kernel-only: creating a thread from the
injected one runs with a mismatched identity and takes the
guest down, though the thread itself does run.
2026-08-15 22:17:47 +02:00
Ole André Vadla Ravnås 7807fdfc39 barebone: Support attach(pid) on Win9x
Inject the agent's own user-mode half into the target and
run it there. The image is shared, so a thread created in
KERNEL32's worker is retargeted at the target's memory
context and enters the same code the kernel half runs.

Which half is asking is read off the current privilege
level; a stored flag would have each answering for the
other, since both share one image.
2026-08-15 22:00:05 +02:00
Ole André Vadla Ravnås 2d8b24aadf barebone: Resolve KERNEL32 exports in the agent
Parts of the export name table are paged out, and a GDB
read cannot fault them in, so the host only publishes the
base and the agent walks the directory itself.
2026-08-15 22:00:05 +02:00
Ole André Vadla Ravnås 92a822d60a barebone: Report each thread's registers on Win9x
VWIN32 hands out a thread's ring-3 context the way GetThreadContext
does, so ask it, and tell Gum the context is there only for the
threads it answered for.
2026-08-15 11:16:30 +02:00
Ole André Vadla Ravnås 8c55c83596 barebone: Restore newspaper order in win9x.vala
Adding the KERNEL32 lookups landed them between a comment and the
function it belonged to, and ahead of two callees that run before
them. Pure movement, no change in behaviour.
2026-08-15 09:25:56 +02:00
Ole André Vadla Ravnås 81c5919f30 barebone: Enumerate ring 0 threads on Win9x
Walk the scheduler's own list and report each thread by its handle,
which is what current_thread_id already hands out. The ring-3 half
of a Win32 thread belongs to its process, not to us.
2026-08-15 08:54:53 +02:00
Ole André Vadla Ravnås 7632e8cf1d barebone: Report each process's argv on Win9x
Now that the name comes from the module table, the command line is
free to be what it is. Split it the way Windows does, where a run
of backslashes escapes only a quote and then only half of them
survive.
2026-08-15 08:36:58 +02:00
Ole André Vadla Ravnås 00792d83e4 barebone: Name processes the way Windows does
The command line is empty for everything Windows starts itself, so
WELCOME.EXE, SYSTRAY.EXE and both KERNEL32 processes came back
nameless. Take the path from the module table instead, reached as
GetModuleFileName reaches it: the process's own MODREF at PDB+0x94,
whose index picks the entry holding the name.
2026-08-15 08:08:36 +02:00
Ole André Vadla Ravnås db71c4f876 barebone: Report real process ids on Win9x
We handed out the process database pointer, but Windows XORs that
with a per-boot value before anyone sees it. Ask KERNEL32 where it
keeps the value rather than hardcoding: GetCurrentProcessId()
tail-calls a helper that loads it, and both ids go through there.
2026-08-14 23:10:58 +02:00
Ole André Vadla Ravnås 635247442d barebone: Follow VMM's chain to reach every VxD
Sweeping the arena only recognises a VxD by its service table, so
the 23 that export nothing were invisible, and the name check
rejected lowercase, losing VNetSup, mmdevldr, DiskTSD, voltrack
and scsi1hlp. Sweep only until VMM turns up, then walk DDB_Next:
68 VxDs instead of 42, and far less to read.

VXDLDR knows the objects it scattered each dynamically loaded VxD
into, so take the extent from the object holding the descriptor
block. Statically linked VxDs sit inside VMM32.VXD and have no
image to ask about; they keep the span of their services.
2026-08-14 22:51:46 +02:00
Ole André Vadla Ravnås c7567aa977 barebone: Enumerate the guest's VxDs on Win9x
The descriptor block scan already knows every VxD and every
service it exports, so hand that to the agent as modules and let
Gum resolve exports out of the symbol table it already carries.
VxDs we have no ordinal list for get numbered service names.

Each module spans its own service addresses. Note VxD code is
split across the arena, so those spans overlap and an export list
reaches beyond the VxD that owns it.
2026-08-14 21:40:06 +02:00
Ole André Vadla Ravnås e0af18d591 barebone: Say when the guest has not booted
Attaching before Windows reaches protected mode found no page
tables to scan, hence no VxD, and the failure surfaced three
layers down as a missing Get_System_Time.
2026-08-14 21:29:22 +02:00
Ole André Vadla Ravnås 4a85e30cf5 Ignore the kernel module build output 2026-08-14 20:46:58 +02:00
Ole André Vadla Ravnås 1b51fea313 barebone: Report process icons on Win9x
Read the icon group straight out of the executable's PE resources
over IFSMgr's ring-0 file I/O, and hand the raw resources to the
host, which turns the DIBs into RGBA.

Resolving IFSMgr needs its ordinals: the DDK does ship them, in an
assembly include declaring the table without parentheses, which is
why the earlier sweep for service tables missed it.
2026-08-14 20:36:59 +02:00
Ole André Vadla Ravnås b61402808b barebone: Create the agent thread via VWIN32
VMMCreateThread leaves TCB_ClientPtr pointing at raw stack memory.
The moment VMM simulates a call down into the VM, it pushes onto
that garbage client stack and spins in an endless fault loop,
wedging the guest. Any file read not served from cache does so.

VWIN32 builds the thread properly and gives it a stack, so the
segment helpers go too.
2026-08-14 20:35:32 +02:00
Ole André Vadla Ravnås f1998c37e1 barebone: Enumerate the guest's processes on Win9x
Win32 threads name the process they belong to in VWIN32's
per-thread block, so the list is the deduplicated set of
those, and the image path is where the command line starts.
The kernel's own processes have neither and stay unnamed.
2026-08-14 17:21:42 +02:00
Ole André Vadla Ravnås 95d8293b39 barebone: Start the agent when the kernel is ready
VMM will not build a thread from the borrowed context the
host enters us on, and only says so by never returning, so
a booted guest left the agent with no worker at all and
every request timed out. Deferring belongs to how we are
entered rather than to making a thread, so _start asks to
run when the kernel is ready; where entry is already safe
that is a direct call.
2026-08-14 17:21:42 +02:00
Ole André Vadla Ravnås ad226053f0 barebone: Resolve VWIN32 and thread services
Name VWIN32's ordinals so its services resolve like VMM's,
and add slots and thunks for walking VMs and threads and
for switching memory context. Together these are what
process enumeration needs from the guest.
2026-08-14 15:13:44 +02:00
Ole André Vadla Ravnås dcbdb0d092 barebone: Run scripts in the Windows 95 kernel
Resolve VMM services by walking the DDB chain and its
service table, enter the agent from a thread already
calling Get_System_Time, and carry the hostlink over
virtio-serial on PCI. Wake on the hostlink IRQ, and hand
ring-0 faults to Exceptor before the kernel sees them.
2026-08-14 12:54:48 +02:00
Ole André Vadla Ravnås 4f125feacb gdb: Write memory with binary packets
Halves what goes on the wire, with a fallback to the hex
form for stubs that decline X.
2026-08-14 12:54:48 +02:00
Ole André Vadla Ravnås e9cd6e5120 barebone: Select kernel flavor from config
Flavor was inferred from the presence of an image config, so any
target supplying symbols got XNU's bring-up dance and its demand
for a kernel_base. Win9x needs neither, yet does need symbols to
fill the agent's service slots.

Add an explicit "kernel" field, defaulting to the old inference.
2026-08-14 12:54:28 +02:00
Ole André Vadla Ravnås deeb15aa24 tests: Cover mappings that outgrow a leaf table
Two adjacent tables, the first all but full, so a run of three has to
carry on into the second. Checks the entries land in both and that
releasing puts each back, and that reprotecting across the boundary
still touches only what was asked for.

The guest cases prove this at agent size, but only when pointed at one.
2026-08-14 12:54:28 +02:00
Ole André Vadla Ravnås 4d1ad3fa81 barebone: Reprotect a leaf table at a time
Every page was costing two walks from the root and a write of its own,
so protecting an agent-sized range spent fifty seconds asking the same
questions. A range covers whole tables, so walk to each one once and
rewrite the entries it holds in a single go.

Five megabytes now takes a second and a half.
2026-08-14 12:54:28 +02:00
Ole André Vadla Ravnås cc96b2d886 barebone: Map runs that outgrow a leaf table
A run has to be contiguous in virtual address space, not in the tables
describing it, so gather it a segment at a time and put each segment
back where it came from on release. A gap anywhere above the leaves
still starts the search over, the addresses either side of it not being
adjacent.

Five megabytes of agent needs twelve hundred pages, where a table holds
a thousand and twenty-four, or half that under PAE.
2026-08-14 12:54:28 +02:00
Ole André Vadla Ravnås b21fc1247f tests: Load a whole agent into an x86 guest
Everything the loader had been shown so far was a page or two, which
says nothing about an image of thousands. Point it at a real agent and
it turns out one cannot be placed at all: the search for somewhere to
put it never leaves a single leaf table, and a table only reaches four
megabytes, or two under PAE.

Skipped unless pointed at a blob, since none is small enough to keep.
2026-08-14 12:54:28 +02:00
Ole André Vadla Ravnås e6bcc3c8af barebone: Add a Win9x flavor of the injected agent
What the host injects is no longer XNU's alone, so the feature naming a
flavor is what asks for one at all, and the linker script laying out its
slots follows the flavor rather than the word size.

VMM answers the kernel facade through slots the host fills in, as it
does for XNU. Nothing here is specific to 95: the same services carry
98 and Me.

Its Gum backend is the short one: these processors have no execute
permission to withhold and no write protection worth the name, so code
is writable where it lies and the aliasing the others do falls away.
Page permissions go through VMM, and there are no exports to walk.

What was XNU's only by accident becomes every injected flavor's: the
entrypoint, the config it parses, and the C library shims that answer
picolibc wherever it is linked. Vsock stays XNU's.
2026-08-14 12:54:23 +02:00
Ole André Vadla Ravnås d7b330d06d barebone: Name virtio's kernel calls in the facade
We drive the device ourselves, so what the host kernel offers has no
bearing on it. Only three calls underneath were XNU's — mapping the
registers, resolving a ring's physical address, and taking the
interrupt — so the facade names them and each kernel answers.

The write barrier is the one place the architecture shows through.
2026-08-13 18:33:02 +02:00
Ole André Vadla Ravnås af6f1eec42 tests: Inject a real ELF into an x86 guest
The relocations an image carries had only ever been applied to made-up
data, and the loader had only ever run against AArch64. Put a genuine
one through the whole path — inject, relocate, call its entry point —
and let the guest report where it ended up.
2026-08-13 18:16:13 +02:00
Ole André Vadla Ravnås 1e4abea59e barebone: Add an x86 helper that reports its bias
Something small enough to inject and prove nothing more than that the
image arrived: it hands back the address of a static, which is only
worth reading once the load bias has been applied, along with what it
finds there.
2026-08-13 18:16:13 +02:00
Ole André Vadla Ravnås bb809b4dbf barebone: Tell ld which x86 to prelink for
A relocatable link defaults to the host's own format, so prelinking a
32-bit agent on a 64-bit builder is refused outright. The arm64 branch
gets its answer from the cross linker and needs no such hint.
2026-08-13 17:57:18 +02:00
Ole André Vadla Ravnås 8ddd84061c tests: Boot a Windows 95 guest for the stub
The image is whatever the caller points at, since it is not ours to
fetch. TCG rather than KVM: the stub behaves there, and it sidesteps
the divide overflow a fast processor provokes in this kernel.

Nothing reaches the serial line to say the boot finished, so give it a
budget and leave the verdict to whoever attaches.
2026-08-13 17:57:18 +02:00
Ole André Vadla Ravnås f0c69bb108 barebone: Separate kernel flavor from injection
Quiescing the target and letting it go again are the kernel's business,
not the injector's: XNU wants EL1, a thread parked in thread_block and
a permission template read off it, and the VZ bridge wants a detach
rather than a continue. A target that is already halted wants none of
this, so it no longer has to supply a kernelcache, a load base or a
thread_block just to be injected into.
2026-08-13 17:57:18 +02:00
Ole André Vadla Ravnås ac65b7213b subprojects: Bump outdated 2026-08-13 17:54:51 +02:00
Ole André Vadla Ravnås 694964fffd submodules: Bump releng 2026-08-13 17:54:50 +02:00
Ole André Vadla Ravnås 77bb47a543 subprojects: Bump outdated 2026-08-13 16:21:08 +02:00
Ole André Vadla Ravnås ca38e91d1c submodules: Bump releng 2026-08-13 16:21:07 +02:00
Jamie 65e713c762 windows: Tweak ACLs to improve injection success rate 2026-08-13 15:05:23 +02:00
Ole André Vadla Ravnås fb701b8a67 tests: Check that an x86 guest hook fires
Hook a stub that answers 42 with a probe that stamps a marker, then
call it: the answer must survive the detour and the marker must show
the probe ran. Disabling it puts the stub back as it was.

Group the guest-driven tests in their own namespace while here.
2026-08-13 14:46:09 +02:00
Ole André Vadla Ravnås ec27b1872c barebone: Hook x86 guests inline
Lay out a GumCpuContext the way pushax already leaves the registers,
with the vector pointer above it and the instruction pointer below,
and hand its address to the probe. What follows the call is the
displaced code, relocated, and a branch back past the redirect.

Both bitnesses differ only in pointer width, so they share this.
2026-08-13 14:46:09 +02:00
Ole André Vadla Ravnås 5339e16d66 tests: Spell QEMU in test paths 2026-08-13 14:45:51 +02:00
Ole André Vadla Ravnås 0fc8ab428f tests: Exercise guest calls and x86-64 mapping
Write a stub that sums its arguments into a mapped page and call it,
which puts the whole register and stack handoff under test, and run
the mapping check on x86-64 as well.

Pin the guest's memory size so the stub lands on a page the kernel
reports as usable rather than in the tables reserved above it.
2026-08-13 14:45:51 +02:00
Ole André Vadla Ravnås ca04379c29 barebone: Call into x86 guests
Hijack a halted vCPU: point it at the callee with the return address
and arguments in place, then breakpoint the instruction it came from
and let it run until it lands back there. i386 passes both on the
stack and has no red zone to step over, x86-64 splits them.

Map pages on x86-64 too, the walk behind it already being shared.
2026-08-13 14:45:51 +02:00
Ole André Vadla Ravnås b0c073d983 gdb: Size register writes from the target
A register's width came from the Variant holding it, so every integer
went out 64 bits wide. On a 32-bit target that doubles each field of
the G packet and shifts the rest, landing ESP's value in EIP. Take the
width from the target's own description instead, and stop skipping the
narrower registers on stubs without bulk transfer.
2026-08-13 14:45:51 +02:00
Ole André Vadla Ravnås 6136bdc869 tests: Check x86 page mapping against QEMU
Map two pages into a booted guest and let QEMU's own view of the tables
confirm where they landed, then check the run is gone once released.
2026-08-13 13:54:43 +02:00
Ole André Vadla Ravnås f6e2605e3f barebone: Map pages into x86 guests
Reuse a hole in the guest's own page tables rather than allocating new
ones: descend the levels that already permit RWX and claim a run of
absent leaf entries. Restoring the entries the run displaced undoes it.

Skip a run starting at VA 0, so nothing lands where a null dereference
would find it.
2026-08-13 13:54:43 +02:00
Ole André Vadla Ravnås 1c04ae8143 barebone: Keep bindgen away from max_align_t
bindgen parses the headers with x87's 80-bit long double while the C
half is compiled -mlong-double-64, so its layout assertion for a type
nothing here uses fails on 32-bit x86.
2026-08-13 13:38:41 +02:00
Ole André Vadla Ravnås e3e1480a69 barebone: Port the agent crate to 32-bit x86
GLib's lock structures are pointer-aligned, so the overlays standing in
for them cannot hardcode 8-byte alignment: on 32-bit that both
over-aligns them and rounds their size past GLib's. Take the alignment
from the target, and give the recursive mutex's owner the pointer-sized
slot it already documents.

Narrow the thread ID where Gum and the overlay want a word, and hand
g_memdup2 a gsize rather than a u64.
2026-08-13 13:38:41 +02:00
Ole André Vadla Ravnås 73bb8d5a04 barebone: Key agent build on word size too
The kernel calls both x86 bitnesses x86, so ARCH alone cannot say
which one to build. Derive BITS alongside it and select the Rust
target, the SDK flavour and the C flags on the pair.

A target given as a spec file reaches cargo behind its own unstable
flag, so carry the whole --target argument in one variable.
2026-08-13 13:38:41 +02:00
Ole André Vadla Ravnås 0bf656dbbd barebone: Add bare-metal i686 Rust target
Rust ships no freestanding 32-bit x86 target, so spell one out. Its
rustc-abi is softfloat, which returns doubles in EAX:EDX and keeps
x87 out of the generated code, matching what clang emits for the C
half with +soft-float.

Widen the page size and cache flush shims to cover both x86
bitnesses.
2026-08-13 13:38:41 +02:00
Ole André Vadla Ravnås 96d2bfcc34 subprojects: Bump outdated 2026-08-13 13:34:47 +02:00
Ole André Vadla Ravnås ca77bf66c2 submodules: Bump releng 2026-08-13 13:34:46 +02:00
Ole André Vadla Ravnås 0e377306c4 subprojects: Bump outdated 2026-08-13 13:01:49 +02:00
Ole André Vadla Ravnås 65639716c5 submodules: Bump releng 2026-08-13 13:01:49 +02:00
Ole André Vadla Ravnås 713d87ddea tests: Bridge the kmod guest to a host socket
Given a socket, QEMU grows a virtio-serial port the guest can be reached
through, and the init copies between it and the device node the module
exposes. Both ends take a single opener, so each gets one descriptor
covering both directions, and a port with nobody attached on the host
side reads EOF, hence the restarts.

The kernel cannot work the port itself: kernel_read wants read_iter,
which virtio-console does not have.
2026-08-13 11:54:36 +02:00
Ole André Vadla Ravnås d9ebcc3622 barebone: Reach a resident agent over a socket
A guest kernel holding a virtio-serial port open puts the other end of
the link on the host as a socket, so let the resident-agent path connect
to one as readily as it opens a device node.
2026-08-13 11:54:33 +02:00
Ole André Vadla Ravnås b1c3e7cb14 subprojects: Bump outdated 2026-08-13 00:18:54 +02:00
Ole André Vadla Ravnås 317cb41703 submodules: Bump releng 2026-08-13 00:18:53 +02:00
Ole André Vadla Ravnås 1ee29ddfb7 tests: Stress the kmod guest's hooked paths
Read /proc from several tasks at once across several CPUs, which is what
puts the hooks under contention on the paths where the caller holds
locks that forbid sleeping. Fail the run when the kernel reports a
voluntary context switch inside an RCU read-side critical section, that
being what such a mistake looks like from the outside.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 0610db53ee tests: Mount devtmpfs in the kmod guest
Without /dev every redirect to /dev/null in the init failed, so the
reads meant to put the module's hooks through their paces were doing
nothing at all.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås a8ae43d771 barebone: Spin rather than sleep in hook context
A hook runs on the stack of whoever called the function it replaced, and
that caller may hold an RCU read lock or a spinlock. Sleeping there is
something the kernel complains about at best and deadlocks on at worst,
so wait by spinning when the context forbids it. An RCU read-side
critical section is still preemptible, which is what lets whoever we are
waiting on make progress.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås c4e01ffbfd barebone: Stop waiving declaration-after-statement
The file has none, so all the waiver did was keep quiet whichever
compilers check for it.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 8a298204fe barebone: Compile the agent without jump tables
An IBT kernel leaves NOTRACK_EN clear, so the NOTRACK jump a compiler
emits for a jump table faults like any other indirect branch landing
somewhere unmarked. The Rust half already avoided them; this is the C
half, and objtool's "notrack prefix found" is what says so.

What survives is libffi's hand-written asm, which nothing on the load
path reaches.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås de7e633df6 barebone: Unseal the code allocator too
It arrived after the rest and was left resolving through the raw lookup,
which meant the first slab allocation faulted rather than the bootstrap.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 05f64d14d6 barebone: Unseal the landing pads we call through
Every function whose address the kernel itself never takes has had its
landing pad poisoned at boot, which is the kernel saying nothing should
reach it through a pointer. Most of what the shim resolves is such a
function, and a kprobe reports the address after the pad either way, so
rewind to the entry and put the pad back.

MODULE_NAME comes along because a module that dies in its init path
stays wedged under its own name until the next boot, and bring-up wants
more attempts than that.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås fcfd2f21a1 barebone: Resolve symbols past the kprobe's ENDBR
A kprobe may not be planted on the ENDBR64 opening a function, so the
address it reports is the one after it, and calling that is what a CPU
with IBT enabled exists to refuse.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 369f26f296 barebone: Allocate code slabs near kernel text
A trampoline reaches the function it hooks with a direct branch only
from within range of the text, and vmalloc space is nowhere near it. Out
of range x86 goes through memory instead, landing mid-function where no
ENDBR64 marks the way, so a kernel with IBT enabled faults rather than
arrives. The module region is what execmem hands out.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 0830507c41 tests: Boot a guest that loads the kmod
The GDB stub guests have no userspace, so nothing there can insmod. This
one boots the kernel the module was built against — by default the one
running, vermagic tying a module to exactly one — loads it over a
busybox initramfs, reads /proc to put the cloak's hooks through their
paces, and leaves a shell on the console.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 3250dd952c barebone: Keep the agent's relocations loadable
x86_64-unknown-none is a PIC target that routes calls to external
symbols through the GOT, and the module loader takes neither: it rejects
R_X86_64_GOTPCREL outright.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 1d2eacff10 barebone: Document the x86_64 kmod flavour
Building for the running kernel is short enough to lead with; the arm64
walkthrough keeps the device-flavoured detour.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 90e3021646 barebone: Port the Linux kmod to x86_64
Makefile covers both architectures now, defaulting to the one it is
built on, and the ABI the kernel imposes differs: ENDBR64 on every
address-taken function for IBT, and switches compiled as comparisons,
objtool being unable to follow a jump table. The blob and the module
object it is linked into both have to be excused from objtool
regardless.

task_struct lost thread_group and fs_struct lost its lock along the way,
and neither is announced by anything the source can test for, so
Makefile asks the headers it is compiled against.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 3ec763cb4e barebone: Port the Linux agent to x86_64
The page size is a constant where arm64 reads it out of tcr_el1, and x86
keeps its caches coherent itself, so there is nothing to clear. Both the
CC variable and bindgen's triple follow from the Rust target.

gchar is signed here, unlike on arm64, which the three call sites
passing Rust bytes straight to GLib had relied on.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 3b35e1bf6e barebone: Fix trimming of the agent's libc
sort and join disagreed on collation, so the members the agent
supersedes were only partly dropped: picolibc's malloc, free, write and
gettimeofday all survived into the prelink alongside the agent's own.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås ac7b7e6744 barebone: Assign in the body, not in declarations
A declaration block should declare and nothing else, so the two
initialisers that called out to do their work now assign where the work
belongs.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 49ceb4a9fc barebone: Test x86-64 walk against a QEMU guest
Parameterize the guest by architecture, and boot a 64-bit
kernel through the same path. Its direct map is made of
1 GiB pages and its text is the only thing left executable,
so both are checked against what the guest itself reports.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås ec7aed65fa barebone: Apply x86-64 relocations
Handle enough of R_X86_64_* to load an agent linked at zero,
mirroring what the IA32 machine does.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås e534e22bfb barebone: Change x86-64 page protection
Clear NX on the way to granting execute, the guest kernels we
attach to having it enabled throughout.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås 6ee4919615 barebone: Walk x86-64 page tables
Implement enumerate_ranges() and translate_address() on top
of the shared walk, including 1 GiB and 2 MiB pages. Teach
the fake stub to pose as a 64-bit target, so that the kernel
half exercises the sign extension.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås 8552c0573e barebone: Rename the x86-64 call frame
It was called Arm64CallFrame, having been copied from there.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås cd344237d0 barebone: Share x86 page table walking
Drive the walk from a table of levels rather than from the
two-level and PAE cases spelled out, and move it out of the
IA32 machine. Long mode is another entry in that table, PAE
being a subset of its format.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås a03b25adfc barebone: Test IA32 walk against a QEMU guest
Boot a stock Linux kernel with no root filesystem, and check
the walk against that guest's own view of its tables. Skip
the test where QEMU is unavailable.

Wait for the boot on the serial console before attaching, as
QEMU drops every GDB packet but Ctrl-C while the guest runs.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås ee39332362 barebone: Apply IA32 relocations
Handle enough of R_386_* to load an agent linked at zero:
shift the absolute ones by the load bias, and leave the
PC-relative ones alone.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås e6fc64cda4 barebone: Change IA32 page protection
Widen the entries above the leaf as needed, access rights
being the AND of every level's. Refuse large pages rather
than split them.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås c258b1d928 barebone: Walk IA32 page tables
Implement enumerate_ranges() and translate_address() for
two-level and PAE paging, including 4 MiB and 2 MiB pages.

Fall back to parsing the control registers out of the
monitor's register dump, as QEMU's x86 stub leaves them out
of its target description.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås 34ebbbbee6 barebone: Share range coalescing
Move it out of the ARM64 machine, as the IA32 machine needs
the same pass over its walk output.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås d3ff6ed321 barebone: Await restore of stub addressing mode
Hold on to any failure across the restore instead of firing
it and forgetting it: the reply is otherwise still in flight
when the next command goes out, that command's predicate
consumes it, and its caller is left with an empty response
and the stub still in physical mode.

Vala rejects yield inside finally, hence the explicit
handling.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås ed0ac90f59 subprojects: Bump outdated 2026-08-12 13:00:06 +02:00
Ole André Vadla Ravnås fba1f24676 submodules: Bump releng 2026-08-12 13:00:05 +02:00
Ole André Vadla Ravnås 903fb22273 barebone: Refresh the Linux kmod README
Document the prctl injection and cloaking channel, and drop the stale
CONFIG_VSOCKETS requirement — the module uses the /dev/frida chardev.
2026-08-12 12:52:39 +02:00
Ole André Vadla Ravnås 6e41df0484 build: Enable the Barebone backend on Android
Android is the primary target for the in-kernel agent, so pick the
Barebone backend up by default there like the other 64-bit hosts.
2026-08-12 12:41:43 +02:00
Ole André Vadla Ravnås f4959b4f5b linux: Add a kernel-assisted injector
Inject through the Frida kernel module when it is loaded, instead of
ptrace: stage the loader in a fresh region, give the bootstrap thread a
private TLS, spawn it into the target and let the agent connect back.
Cloak the loader's region, stack, TLS and thread so it leaves no trace.
Fall back to the existing paths when the module is absent.
2026-08-12 12:41:43 +02:00
Ole André Vadla Ravnås 16ef053845 barebone: Add injection and cloaking to the kmod
Drive the in-kernel agent from userspace over a token-gated magic-prctl
channel: allocate, free, read and write memory in a target pid, and spawn
a real CLONE_THREAD there, adopting its mm, files, creds, namespaces and
fs so the thread is a native member.

Hide cloaked threads, ranges and fds from /proc and keep thread counts
consistent, with a cloaked thread seeing through its own cloak. Enforce
through the in-kernel Gum Interceptor, installed once the worker thread
has brought up Gum.
2026-08-12 12:41:43 +02:00
Ole André Vadla Ravnås 5baa62b03e barebone: Bump clang-sys for libclang 22
Under libclang 22 the pinned clang-sys 1.8.1 silently lowers records to
opaque one-byte structs, so the agent's bindgen output loses every
field and the Rust build fails against the generated bindings. Bump
clang-sys to 1.9.1 (and bindgen to 0.72.1) to build against modern clang.
2026-08-12 12:41:43 +02:00
Ole André Vadla Ravnås 72a5fdf715 subprojects: Bump outdated 2026-08-11 11:41:38 +02:00
Ole André Vadla Ravnås b0c3df8b76 barebone: Note that deps.py needs releng
It is a submodule, so a clone without --recurse-submodules has no
deps.py to sync the SDK with.
2026-08-05 11:49:47 +02:00
Ole André Vadla Ravnås 4cdc54457f barebone: Document the kmod build from a release
Walks from a published GumJS devkit to a loaded module, so nobody has to
build Gum to get one. The Makefile now derives the compiler flags from
the SDK, since they have to agree with it down to the ABI.
2026-08-05 11:37:01 +02:00
Ole André Vadla Ravnås c812943c45 subprojects: Prepare for release 2026-08-05 00:32:48 +02:00
Ole André Vadla Ravnås 2e9521d006 subprojects: Bump outdated 2026-08-05 00:31:57 +02:00
Ole André Vadla Ravnås 65667a1973 submodules: Bump releng 2026-08-05 00:31:57 +02:00
Ole André Vadla Ravnås 2e50475515 barebone: Build the kmod against the SDK
Retires the script that assembled a libc by hand: the soft-float SDK now
ships one, and what the agent replaces is worked out from the archives
rather than listed. Adds KOUT for building against a kernel tree that
was configured elsewhere.
2026-08-04 17:20:21 +02:00
Ole André Vadla Ravnås fca672a9c1 barebone: Let CC carry flags in the agent build
The probe ran the compiler bare, so a bare-metal clang looked for headers
without a sysroot and found none. Split the variable the way cc-rs does
and pass the rest along.
2026-08-04 12:57:50 +02:00
Ole André Vadla Ravnås a8d9cacb1d barebone: Keep the libc lock object alive
Nothing references it, so it was dropped before the linker saw it and the
C library's own lock implementation came in after all, colliding with
ours. Only visible against a libc whose lock stubs live in a member that
gets pulled for other reasons.
2026-08-04 12:55:35 +02:00
Ole André Vadla Ravnås 76a9ac10f2 subprojects: Bump outdated 2026-08-02 18:29:05 +02:00
Ole André Vadla Ravnås 4a9900d165 barebone: Document how to reach the kmod agent
Serve it from a frida-server on the target with --device barebone, so
the device node is opened right next to itself and the host talks to
an ordinary frida-server over adb forward.

Also write down where a matching KDIR comes from. The kernel config
alone is not enough, but nothing needs a kernel build either.
2026-07-31 19:47:10 +02:00
Ole André Vadla Ravnås 4c29710d97 barebone: Fix the Linux agent's ABI mismatches
The prelink whole-archives the Rust staticlib, so Rust's own
compiler_builtins answered the C library's calls to __muldf3 and
friends. It is built for the ordinary AAPCS and takes its double in
d0, while everything else here is -mabi=aapcs-soft and passes it in
x0. Nothing faulted: the arithmetic simply returned garbage, and
GLib, whose hash tables grow on a floating-point threshold, then
never grew one. The first table to fill turned the next miss into an
endless probe loop. Drop those definitions and let the SDK's
compiler-rt answer instead.

That retires the byte-loop memcpy and memmove, which had been put
there to work around the same hang.

Separately, x18 is the kernel's shadow call stack pointer, and
RUSTFLAGS only reaches crates Cargo compiles: core, alloc and
compiler_builtins ship prebuilt and wrote to it in 269 places.
Rebuild them from source with -Zfixed-x18.
2026-07-31 18:40:00 +02:00
Ole André Vadla Ravnås 883e51bb1e submodules: Bump releng 2026-07-31 15:50:18 +02:00
Ole André Vadla Ravnås a8af49e5b3 barebone: Connect to an agent already resident
An agent loaded from inside the target brings its own transport, so
there is nothing to inject and no debugger stub to drive. Open the
device it offers and speak the same protocol over it, with the framing
details it used to take from the stub supplied directly.
2026-07-31 15:50:18 +02:00
Ole André Vadla Ravnås 872fb83da4 barebone: Run the agent as a Linux kernel module
The agent was written for XNU, where it is injected from outside by a
remote stub. Here it starts from the inside instead: a .ko whose init
path spawns the worker, with a character device standing in for the
hostlink.

Everything the kernel side needs lives in a C shim compiled against the
target's own headers, since struct layouts, inline helpers and which of
kallsyms' entry points happen to be exported all vary by release. The
Rust half reaches it through a facade that each flavour fills in.

Scripts are driven through the asynchronous half of the script API: the
thread pumping the JS context cannot also be the one waiting on it.

Neither flavour is the default one, since neither is what you get by
being on a particular host: both are picked deliberately.
2026-07-31 15:50:18 +02:00
289 changed files with 94348 additions and 4164 deletions

No files matched your search

+16 -8
View File
@@ -20,8 +20,8 @@ jobs:
strategy:
matrix:
include:
- { id: windows-arm64, runner: '"windows-11-arm"', sys: CLANGARM64, pkg: 'mingw-w64-clang-aarch64-clang' }
- { id: windows-x86_64, runner: '"windows-latest"', sys: MINGW64, pkg: 'mingw-w64-x86_64-gcc' }
- { id: windows-arm64, runner: '"windows-11-arm"', sys: CLANGARM64, pkg: 'mingw-w64-clang-aarch64-clang', opts: "'-Dfrida-gum:v8=disabled'" }
- { id: windows-x86_64, runner: '"windows-latest"', sys: UCRT64, pkg: 'mingw-w64-ucrt-x86_64-gcc' }
- { id: windows-x86, runner: '"windows-latest"', sys: MINGW32, pkg: 'mingw-w64-i686-gcc' }
- { id: macos-x86_64, runner: '"macos-latest"' }
- { id: macos-arm64, runner: '"macos-latest"' }
@@ -71,7 +71,7 @@ jobs:
--host=${{ matrix.id }} `
--enable-compiler-backend `
${{ env.FRIDA_CORE_OPTIONS }} `
-- --force-fallback-for=openssl
-- --force-fallback-for=openssl ${{ matrix.opts }}
.\make
env:
MSYS2_LOCATION: ${{ steps.msys2.outputs.msys2-location }}
@@ -658,7 +658,7 @@ jobs:
matrix:
include:
- { arch: x86_64, runner: windows-latest, msvc-arch: amd64, vcpkg-triplet: x64-windows-release }
- { arch: arm64, runner: windows-11-arm, msvc-arch: arm64, vcpkg-triplet: arm64-windows-release }
- { arch: arm64, runner: windows-11-arm, msvc-arch: arm64, vcpkg-triplet: arm64-windows-release, opts: "'-Dfrida-gum:v8=disabled'" }
fail-fast: false
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
@@ -668,9 +668,17 @@ jobs:
with:
submodules: recursive
- uses: ilammy/msvc-dev-cmd@v1
with:
arch: ${{ matrix.msvc-arch }}
- name: Set up MSVC environment
run: |
$vs = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -products * -latest -property installationPath
$before = @{}
Get-ChildItem env: | ForEach-Object { $before[$_.Name] = $_.Value }
cmd /c "`"$vs\VC\Auxiliary\Build\vcvarsall.bat`" ${{ matrix.msvc-arch }} >nul && set" | ForEach-Object {
$name, $value = $_ -split '=', 2
if ($before[$name] -ne $value) {
"$name=$value" >> $env:GITHUB_ENV
}
}
- uses: actions/setup-python@v6
with:
@@ -721,7 +729,7 @@ jobs:
--enable-shared `
--without-prebuilds=sdk `
"--prefix=$env:RUNNER_TEMP\frida-prefix" `
-- --force-fallback-for=openssl,libnghttp2 -Dcompiler_backend=disabled -Dtests=disabled
-- --force-fallback-for=openssl,libnghttp2 -Dcompiler_backend=disabled -Dtests=disabled ${{ matrix.opts }}
- name: Compile
run: python releng/meson/meson.py compile -C build
+9 -1
View File
@@ -2,7 +2,15 @@
/build/
/deps/
/src/android-helper/build/
/src/barebone/agent/target/
/src/barebone/agent/linux/*.ko
/src/barebone/agent/linux/*.mod
/src/barebone/agent/linux/*.mod.c
/src/barebone/agent/linux/*.o
/src/barebone/agent/linux/*.o.bin
/src/barebone/agent/linux/.*.cmd
/src/barebone/agent/linux/Module.symvers
/src/barebone/agent/linux/modules.order
/src/barebone/agent/target*/
/src/barebone/agent/xpacks/
/src/barebone/helpers/memory-scanner/target/
/src/barebone/helpers/target/
+38
View File
@@ -13,6 +13,44 @@
# include <gioapple.h>
#elif defined (HAVE_GIOOPENSSL)
# include <gioopenssl.h>
#endif
#ifdef HAVE_PROSPERO
# include "frida-prospero.h"
# include <unistd.h>
#endif
#ifdef HAVE_PROSPERO
FridaProsperoAgentArgs frida_prospero_agent_args = { .fifo_fd = -1 };
int
main (int argc, char * argv[], char * envp[])
{
FridaProsperoAgentArgs * args = &frida_prospero_agent_args;
FridaProsperoInjectorState injector_state;
guint8 hello_byte = FRIDA_PROSPERO_HELLO_BYTE;
FridaUnloadPolicy unload_policy = FRIDA_UNLOAD_POLICY_IMMEDIATE;
guint8 policy_byte;
guint32 worker_id;
injector_state.fifo_fd = args->fifo_fd;
injector_state.agent_ctrlfd = args->agent_ctrlfd;
injector_state.mapped_range = &args->mapped_range;
write (injector_state.fifo_fd, &hello_byte, sizeof (hello_byte));
frida_agent_main (GSIZE_TO_POINTER (args->agent_parameters), &unload_policy, &injector_state);
policy_byte = unload_policy;
worker_id = gum_process_get_current_thread_id ();
write (injector_state.fifo_fd, &policy_byte, sizeof (policy_byte));
write (injector_state.fifo_fd, &worker_id, sizeof (worker_id));
close (injector_state.fifo_fd);
return 0;
}
#endif
void
+19 -18
View File
@@ -128,6 +128,12 @@ namespace Frida.Agent {
mapped_range = injector_state.mapped_range;
#endif
#if PROSPERO
var prospero_state = (ProsperoInjectorState *) opaque_injector_state;
if (prospero_state != null)
mapped_range = prospero_state.mapped_range;
#endif
if (cached_agent_path == null) {
cached_agent_range = detect_own_range_and_path (mapped_range, out cached_agent_path);
Gum.Cloak.add_range (cached_agent_range);
@@ -157,6 +163,19 @@ namespace Frida.Agent {
}
#endif
#if PROSPERO
string? agent_parameters_with_transport_uri = null;
if (prospero_state != null) {
int agent_ctrlfd = prospero_state.agent_ctrlfd;
prospero_state.agent_ctrlfd = -1;
fdt_padder.move_descriptor_if_needed (ref agent_ctrlfd);
agent_parameters_with_transport_uri = "socket:%d%s".printf (agent_ctrlfd, agent_parameters);
agent_parameters = agent_parameters_with_transport_uri;
}
#endif
var ignore_scope = new ThreadIgnoreScope (FRIDA_THREAD);
shared_instance = new Runner (agent_parameters, cached_agent_path, cached_agent_range);
@@ -292,8 +311,6 @@ namespace Frida.Agent {
start.begin ((owned) padder);
start_alive_task_dumper ();
main_loop.run ();
main_context.pop_thread_default ();
@@ -364,16 +381,6 @@ namespace Frida.Agent {
padder = null;
}
// TEMPORARY DEBUG HACK: dump alive GTasks every 30 seconds.
private void start_alive_task_dumper () {
var source = new TimeoutSource.seconds (30);
source.set_callback (() => {
GLib.info ("%s", g_task_describe_alive_tasks ());
return Source.CONTINUE;
});
source.attach (main_context);
}
private void keep_running_eternalized () {
agent_gthread = new Thread<bool> ("frida-eternal-agent", () => {
var ignore_scope = new ThreadIgnoreScope (FRIDA_THREAD);
@@ -936,8 +943,6 @@ namespace Frida.Agent {
private void on_session_closed (BaseAgentSession base_session) {
LiveAgentSession session = (LiveAgentSession) base_session;
GLib.info ("detach-trace: Agent emitting provider closed id=%s", session.id.handle);
closed (session.id);
unregister_session (session);
@@ -1738,8 +1743,4 @@ namespace Frida.Agent {
return repeat;
});
}
// TEMPORARY DEBUG HACK: only present when GLib is built with G_ENABLE_DEBUG.
[CCode (cname = "g_task_describe_alive_tasks")]
private extern string g_task_describe_alive_tasks ();
}
+8
View File
@@ -0,0 +1,8 @@
{
global:
frida_agent_main;
frida_prospero_agent_args;
local:
*;
};
+4
View File
@@ -42,6 +42,8 @@ if build_agent
symscript = 'frida-agent-glibc.version'
elif host_os == 'android'
symscript = 'frida-agent-android.version'
elif host_is_prospero
symscript = 'frida-agent-prospero.version'
else
symscript = 'frida-agent.version'
endif
@@ -54,6 +56,8 @@ if build_agent
extra_link_args += '-Wl,-Bsymbolic'
endif
extra_link_args += prospero_crt_link_args
raw_agent = shared_library('frida-agent-raw', agent_sources,
name_prefix: 'lib',
c_args: frida_component_cflags,
+42
View File
@@ -0,0 +1,42 @@
#ifndef __FRIDA_PROSPERO_H__
#define __FRIDA_PROSPERO_H__
#ifdef HAVE_PROSPERO
#include <gum/gum.h>
G_BEGIN_DECLS
#define FRIDA_PROSPERO_AGENT_ARGS_SYMBOL "frida_prospero_agent_args"
#define FRIDA_PROSPERO_HELLO_BYTE 0xff
typedef struct _FridaProsperoAgentArgs FridaProsperoAgentArgs;
struct _FridaProsperoAgentArgs
{
GumAddress agent_parameters;
gint fifo_fd;
gint agent_ctrlfd;
GumMemoryRange mapped_range;
};
typedef struct _FridaProsperoAppLaunchContext FridaProsperoAppLaunchContext;
struct _FridaProsperoAppLaunchContext
{
guint32 size;
guint32 user_id;
guint32 app_opt;
guint64 crash_report;
guint32 check_flag;
};
extern int sceUserServiceInitialize (void * params);
extern int sceUserServiceGetForegroundUser (guint32 * user_id);
extern int sceSystemServiceLaunchApp (const gchar * title_id, gchar ** argv, FridaProsperoAppLaunchContext * ctx);
G_END_DECLS
#endif
#endif
+12
View File
@@ -90,6 +90,18 @@ namespace Frida.FS {
yield write_all_bytes (f, new Bytes (t.data), cancellable);
}
public Bytes read_all_bytes_sync (File f) throws Error {
try {
uint8[] contents;
f.load_contents (null, out contents, null);
return new Bytes.take ((owned) contents);
} catch (GLib.Error e) {
if (e is IOError.NOT_FOUND)
throw new Error.INVALID_ARGUMENT ("%s", e.message);
throw new Error.PERMISSION_DENIED ("%s", e.message);
}
}
public async Bytes read_all_bytes (File f, Cancellable? cancellable) throws Error, IOError {
try {
return yield f.load_bytes_async (cancellable, null);
+1 -1
View File
@@ -23,7 +23,7 @@ base_sources = [
extra_vala_args = []
if host_os == 'windows'
base_sources += 'windows.vala'
base_sources += ['windows.vala', 'windows-glue.c']
else
base_sources += 'posix.vala'
extra_vala_args += '--pkg=posix'
+124
View File
@@ -220,4 +220,128 @@ namespace Frida {
Posix.tcsetattr (fd, 0, tios);
}
#endif
namespace ChildProcess {
public async WaitResult wait_for_next_stop (uint pid, Cancellable? cancellable) throws Error, IOError {
var main_context = MainContext.get_thread_default ();
bool timed_out = false;
var timeout_source = new TimeoutSource.seconds (5);
timeout_source.set_callback (() => {
timed_out = true;
return Source.REMOVE;
});
timeout_source.attach (main_context);
int status = 0;
uint[] delays = { 0, 1, 2, 5, 10, 20, 50, 250 };
try {
for (uint i = 0; !timed_out && !cancellable.set_error_if_cancelled (); i++) {
int res = Posix.waitpid ((Posix.pid_t) pid, out status, Posix.WNOHANG);
if (res == -1)
throw new Error.NOT_SUPPORTED ("Unable to wait for next stop: %s", strerror (errno));
if (res != 0)
break;
uint delay_ms = (i < delays.length) ? delays[i] : delays[delays.length - 1];
var delay_source = new TimeoutSource (delay_ms);
delay_source.set_callback (wait_for_next_stop.callback);
delay_source.attach (main_context);
var cancel_source = new CancellableSource (cancellable);
cancel_source.set_callback (wait_for_next_stop.callback);
cancel_source.attach (main_context);
yield;
cancel_source.destroy ();
delay_source.destroy ();
}
} finally {
timeout_source.destroy ();
}
if (timed_out)
throw new Error.TIMED_OUT ("Unexpectedly timed out while waiting for stop from process with PID %u", pid);
return WaitResult (pid, status);
}
}
public struct WaitResult {
public uint pid;
public int status;
public WaitKind kind;
public uint exit_status;
public int term_signal;
public int stop_signal;
public WaitResult (uint pid, int status) {
this.pid = pid;
this.status = status;
kind = WaitKind.OTHER;
exit_status = 0;
term_signal = 0;
stop_signal = 0;
if (PosixStatus.is_exit (status)) {
kind = WaitKind.EXITED;
exit_status = PosixStatus.parse_exit_status (status);
} else if (PosixStatus.is_signaled (status)) {
kind = WaitKind.SIGNALED;
term_signal = PosixStatus.parse_termination_signal (status);
} else if (PosixStatus.is_stopped (status)) {
kind = WaitKind.STOPPED;
stop_signal = PosixStatus.parse_stop_signal (status);
}
}
public void check_stopped () throws Error {
switch (kind) {
case WaitKind.EXITED:
throw new Error.NOT_SUPPORTED ("Target exited with status %u", exit_status);
case WaitKind.SIGNALED:
throw new Error.NOT_SUPPORTED ("Target terminated with signal %u", term_signal);
case WaitKind.STOPPED:
return;
default:
throw new Error.NOT_SUPPORTED ("Unexpected status: 0x%08x", status);
}
}
}
public enum WaitKind {
EXITED,
SIGNALED,
STOPPED,
OTHER
}
namespace PosixStatus {
[CCode (cname = "WIFEXITED", cheader_filename = "sys/wait.h")]
private extern bool is_exit (int status);
[CCode (cname = "WIFSIGNALED", cheader_filename = "sys/wait.h")]
private extern bool is_signaled (int status);
[CCode (cname = "WIFSTOPPED", cheader_filename = "sys/wait.h")]
private extern bool is_stopped (int status);
[CCode (cname = "WEXITSTATUS", cheader_filename = "sys/wait.h")]
private extern uint parse_exit_status (int status);
[CCode (cname = "WTERMSIG", cheader_filename = "sys/wait.h")]
private extern Posix.Signal parse_termination_signal (int status);
[CCode (cname = "WSTOPSIG", cheader_filename = "sys/wait.h")]
private extern Posix.Signal parse_stop_signal (int status);
}
}
+8 -3
View File
@@ -361,8 +361,6 @@ namespace Frida {
private async void teardown_peer_connection_and_emit_closed () {
schedule_on_frida_thread (() => {
GLib.info ("detach-trace: AgentMessageTransmitter %p emitting closed, nice_agent=%p",
this, nice_agent);
if (nice_agent != null)
close_nice_resources_and_emit_closed.begin ();
else
@@ -600,7 +598,6 @@ namespace Frida {
private async void teardown_peer_connection_and_emit_closed () {
schedule_on_frida_thread (() => {
GLib.info ("detach-trace: AgentMessageTransmitter %p emitting closed", this);
closed ();
return Source.REMOVE;
});
@@ -1155,6 +1152,14 @@ namespace Frida {
}
#endif
#if PROSPERO
public struct ProsperoInjectorState {
public int fifo_fd;
public int agent_ctrlfd;
public Gum.MemoryRange? mapped_range;
}
#endif
/**
* Why spawn gating was disabled on a {@link Device}.
*/
+2 -1
View File
@@ -726,7 +726,8 @@ namespace Frida {
try {
remote_address = sc.get_remote_address ();
} catch (GLib.Error e) {
assert_not_reached ();
remove_connection (peer);
return;
}
incoming (peer, remote_address);
+376
View File
@@ -0,0 +1,376 @@
#include "frida-base.h"
#include <windows.h>
#define FRIDA_TYPE_WINDOWS_HANDLE_STREAM (frida_windows_handle_stream_get_type ())
#define FRIDA_TYPE_WINDOWS_HANDLE_INPUT_STREAM (frida_windows_handle_input_stream_get_type ())
#define FRIDA_TYPE_WINDOWS_HANDLE_OUTPUT_STREAM (frida_windows_handle_output_stream_get_type ())
#define FRIDA_WINDOWS_HANDLE_STREAM(o) ((FridaWindowsHandleStream *) (o))
#define FRIDA_WINDOWS_HANDLE_INPUT_STREAM(o) ((FridaWindowsHandleInputStream *) (o))
#define FRIDA_WINDOWS_HANDLE_OUTPUT_STREAM(o) ((FridaWindowsHandleOutputStream *) (o))
typedef struct _FridaWindowsHandleStream FridaWindowsHandleStream;
typedef struct _FridaWindowsHandleStreamClass FridaWindowsHandleStreamClass;
typedef struct _FridaWindowsHandleInputStream FridaWindowsHandleInputStream;
typedef struct _FridaWindowsHandleInputStreamClass FridaWindowsHandleInputStreamClass;
typedef struct _FridaWindowsHandleOutputStream FridaWindowsHandleOutputStream;
typedef struct _FridaWindowsHandleOutputStreamClass FridaWindowsHandleOutputStreamClass;
struct _FridaWindowsHandleStream
{
GIOStream parent;
HANDLE handle;
HANDLE read_complete;
HANDLE read_cancel;
HANDLE write_complete;
HANDLE write_cancel;
GInputStream * input;
GOutputStream * output;
};
struct _FridaWindowsHandleStreamClass
{
GIOStreamClass parent_class;
};
struct _FridaWindowsHandleInputStream
{
GInputStream parent;
FridaWindowsHandleStream * stream;
};
struct _FridaWindowsHandleInputStreamClass
{
GInputStreamClass parent_class;
};
struct _FridaWindowsHandleOutputStream
{
GOutputStream parent;
FridaWindowsHandleStream * stream;
};
struct _FridaWindowsHandleOutputStreamClass
{
GOutputStreamClass parent_class;
};
static GIOStream * frida_windows_handle_stream_new (HANDLE handle);
static void frida_windows_handle_stream_finalize (GObject * object);
static GInputStream * frida_windows_handle_stream_get_input_stream (GIOStream * stream);
static GOutputStream * frida_windows_handle_stream_get_output_stream (GIOStream * stream);
static gboolean frida_windows_handle_stream_close_fn (GIOStream * stream, GCancellable * cancellable, GError ** error);
static gssize frida_windows_handle_input_stream_read (GInputStream * base, void * buffer, gsize count,
GCancellable * cancellable, GError ** error);
static gboolean frida_windows_handle_input_stream_close (GInputStream * base, GCancellable * cancellable, GError ** error);
static gssize frida_windows_handle_output_stream_write (GOutputStream * base, const void * buffer, gsize count,
GCancellable * cancellable, GError ** error);
static gboolean frida_windows_handle_output_stream_close (GOutputStream * base, GCancellable * cancellable,
GError ** error);
static gboolean frida_windows_handle_stream_await (FridaWindowsHandleStream * self, HANDLE complete, HANDLE cancel,
GCancellable * cancellable, GError ** error);
static void frida_windows_handle_stream_on_cancel (GCancellable * cancellable, gpointer user_data);
G_DEFINE_TYPE (FridaWindowsHandleStream, frida_windows_handle_stream, G_TYPE_IO_STREAM)
G_DEFINE_TYPE (FridaWindowsHandleInputStream, frida_windows_handle_input_stream, G_TYPE_INPUT_STREAM)
G_DEFINE_TYPE (FridaWindowsHandleOutputStream, frida_windows_handle_output_stream, G_TYPE_OUTPUT_STREAM)
GIOStream *
frida_windows_named_pipe_open_client (const gchar * path, GError ** error)
{
HANDLE handle;
WCHAR * path_utf16;
DWORD last_error;
path_utf16 = (WCHAR *) g_utf8_to_utf16 (path, -1, NULL, NULL, NULL);
if (path_utf16 == NULL)
goto invalid_path;
handle = CreateFileW (path_utf16,
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL);
last_error = GetLastError ();
g_free (path_utf16);
if (handle == INVALID_HANDLE_VALUE)
goto cannot_open;
return frida_windows_handle_stream_new (handle);
invalid_path:
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, "Invalid pipe path");
return NULL;
}
cannot_open:
{
g_set_error (error,
G_IO_ERROR,
g_io_error_from_win32_error (last_error),
"Unable to open %s (CreateFile returned 0x%08lx)",
path, last_error);
return NULL;
}
}
static GIOStream *
frida_windows_handle_stream_new (HANDLE handle)
{
FridaWindowsHandleStream * stream;
FridaWindowsHandleInputStream * input;
FridaWindowsHandleOutputStream * output;
stream = g_object_new (FRIDA_TYPE_WINDOWS_HANDLE_STREAM, NULL);
stream->handle = handle;
stream->read_complete = CreateEvent (NULL, TRUE, FALSE, NULL);
stream->read_cancel = CreateEvent (NULL, TRUE, FALSE, NULL);
stream->write_complete = CreateEvent (NULL, TRUE, FALSE, NULL);
stream->write_cancel = CreateEvent (NULL, TRUE, FALSE, NULL);
input = g_object_new (FRIDA_TYPE_WINDOWS_HANDLE_INPUT_STREAM, NULL);
input->stream = stream;
stream->input = G_INPUT_STREAM (input);
output = g_object_new (FRIDA_TYPE_WINDOWS_HANDLE_OUTPUT_STREAM, NULL);
output->stream = stream;
stream->output = G_OUTPUT_STREAM (output);
return G_IO_STREAM (stream);
}
static void
frida_windows_handle_stream_class_init (FridaWindowsHandleStreamClass * klass)
{
GObjectClass * object_class = G_OBJECT_CLASS (klass);
GIOStreamClass * stream_class = G_IO_STREAM_CLASS (klass);
object_class->finalize = frida_windows_handle_stream_finalize;
stream_class->get_input_stream = frida_windows_handle_stream_get_input_stream;
stream_class->get_output_stream = frida_windows_handle_stream_get_output_stream;
stream_class->close_fn = frida_windows_handle_stream_close_fn;
}
static void
frida_windows_handle_stream_init (FridaWindowsHandleStream * self)
{
self->handle = INVALID_HANDLE_VALUE;
}
static void
frida_windows_handle_stream_finalize (GObject * object)
{
FridaWindowsHandleStream * self = FRIDA_WINDOWS_HANDLE_STREAM (object);
g_clear_object (&self->input);
g_clear_object (&self->output);
CloseHandle (self->read_complete);
CloseHandle (self->read_cancel);
CloseHandle (self->write_complete);
CloseHandle (self->write_cancel);
if (self->handle != INVALID_HANDLE_VALUE)
CloseHandle (self->handle);
G_OBJECT_CLASS (frida_windows_handle_stream_parent_class)->finalize (object);
}
static GInputStream *
frida_windows_handle_stream_get_input_stream (GIOStream * stream)
{
return FRIDA_WINDOWS_HANDLE_STREAM (stream)->input;
}
static GOutputStream *
frida_windows_handle_stream_get_output_stream (GIOStream * stream)
{
return FRIDA_WINDOWS_HANDLE_STREAM (stream)->output;
}
static gboolean
frida_windows_handle_stream_close_fn (GIOStream * stream, GCancellable * cancellable, GError ** error)
{
FridaWindowsHandleStream * self = FRIDA_WINDOWS_HANDLE_STREAM (stream);
SetEvent (self->read_cancel);
SetEvent (self->write_cancel);
if (self->handle != INVALID_HANDLE_VALUE)
{
CancelIo (self->handle);
CloseHandle (self->handle);
self->handle = INVALID_HANDLE_VALUE;
}
return TRUE;
}
static void
frida_windows_handle_input_stream_class_init (FridaWindowsHandleInputStreamClass * klass)
{
GInputStreamClass * stream_class = G_INPUT_STREAM_CLASS (klass);
stream_class->read_fn = frida_windows_handle_input_stream_read;
stream_class->close_fn = frida_windows_handle_input_stream_close;
}
static void
frida_windows_handle_input_stream_init (FridaWindowsHandleInputStream * self)
{
}
static gssize
frida_windows_handle_input_stream_read (GInputStream * base, void * buffer, gsize count, GCancellable * cancellable,
GError ** error)
{
FridaWindowsHandleStream * stream = FRIDA_WINDOWS_HANDLE_INPUT_STREAM (base)->stream;
OVERLAPPED overlapped = { 0, };
DWORD bytes_transferred, last_error;
if (stream->handle == INVALID_HANDLE_VALUE)
return 0;
overlapped.hEvent = stream->read_complete;
if (!ReadFile (stream->handle, buffer, count, NULL, &overlapped) && GetLastError () != ERROR_IO_PENDING)
goto failure;
if (!frida_windows_handle_stream_await (stream, stream->read_complete, stream->read_cancel, cancellable, error))
return -1;
if (!GetOverlappedResult (stream->handle, &overlapped, &bytes_transferred, FALSE))
goto failure;
return bytes_transferred;
failure:
{
last_error = GetLastError ();
if (last_error == ERROR_BROKEN_PIPE || last_error == ERROR_PIPE_NOT_CONNECTED)
return 0;
g_set_error (error,
G_IO_ERROR,
g_io_error_from_win32_error (last_error),
"Error reading from pipe");
return -1;
}
}
static gboolean
frida_windows_handle_input_stream_close (GInputStream * base, GCancellable * cancellable, GError ** error)
{
return TRUE;
}
static void
frida_windows_handle_output_stream_class_init (FridaWindowsHandleOutputStreamClass * klass)
{
GOutputStreamClass * stream_class = G_OUTPUT_STREAM_CLASS (klass);
stream_class->write_fn = frida_windows_handle_output_stream_write;
stream_class->close_fn = frida_windows_handle_output_stream_close;
}
static void
frida_windows_handle_output_stream_init (FridaWindowsHandleOutputStream * self)
{
}
static gssize
frida_windows_handle_output_stream_write (GOutputStream * base, const void * buffer, gsize count,
GCancellable * cancellable, GError ** error)
{
FridaWindowsHandleStream * stream = FRIDA_WINDOWS_HANDLE_OUTPUT_STREAM (base)->stream;
OVERLAPPED overlapped = { 0, };
DWORD bytes_transferred, last_error;
if (stream->handle == INVALID_HANDLE_VALUE)
goto closed;
overlapped.hEvent = stream->write_complete;
if (!WriteFile (stream->handle, buffer, count, NULL, &overlapped) && GetLastError () != ERROR_IO_PENDING)
goto failure;
if (!frida_windows_handle_stream_await (stream, stream->write_complete, stream->write_cancel, cancellable, error))
return -1;
if (!GetOverlappedResult (stream->handle, &overlapped, &bytes_transferred, FALSE))
goto failure;
return bytes_transferred;
closed:
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_CLOSED, "Pipe is closed");
return -1;
}
failure:
{
last_error = GetLastError ();
g_set_error (error,
G_IO_ERROR,
g_io_error_from_win32_error (last_error),
"Error writing to pipe");
return -1;
}
}
static gboolean
frida_windows_handle_output_stream_close (GOutputStream * base, GCancellable * cancellable, GError ** error)
{
return TRUE;
}
static gboolean
frida_windows_handle_stream_await (FridaWindowsHandleStream * self, HANDLE complete, HANDLE cancel,
GCancellable * cancellable, GError ** error)
{
gulong handler_id = 0;
HANDLE events[2];
if (cancellable != NULL)
handler_id = g_cancellable_connect (cancellable, G_CALLBACK (frida_windows_handle_stream_on_cancel), cancel, NULL);
events[0] = complete;
events[1] = cancel;
WaitForMultipleObjects (G_N_ELEMENTS (events), events, FALSE, INFINITE);
if (cancellable != NULL)
{
g_cancellable_disconnect (cancellable, handler_id);
if (g_cancellable_set_error_if_cancelled (cancellable, error))
{
CancelIo (self->handle);
return FALSE;
}
}
if (WaitForSingleObject (cancel, 0) == WAIT_OBJECT_0)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_CLOSED, "Pipe is closed");
return FALSE;
}
return TRUE;
}
static void
frida_windows_handle_stream_on_cancel (GCancellable * cancellable, gpointer user_data)
{
HANDLE cancel = (HANDLE) user_data;
SetEvent (cancel);
}
+4
View File
@@ -1,4 +1,8 @@
namespace Frida {
namespace WindowsNamedPipe {
public extern IOStream open_client (string path) throws GLib.Error;
}
public sealed class StdioPipes : Object {
public OutputStream input {
get;
+4
View File
@@ -55,6 +55,10 @@ if host_os_family in ['linux', 'qnx']
extra_link_args += ['-Wl,-Bsymbolic']
endif
if host_is_prospero
extra_link_args += [prospero_crt_link_args, '-Wl,--defsym=main=0']
endif
raw_gadget = shared_library('frida-gadget-raw', gadget_sources,
name_prefix: 'lib',
vala_args: [base_vala_args, payload_vala_args, extra_vala_args],
-6
View File
@@ -57,8 +57,6 @@ namespace Frida {
}
public async void close (Cancellable? cancellable) throws IOError {
GLib.info ("detach-trace: BaseAgentSession.close() id=%s close_request=%p", id.handle, close_request);
while (close_request != null) {
try {
yield close_request.future.wait_async (cancellable);
@@ -87,8 +85,6 @@ namespace Frida {
yield transmitter.close (cancellable);
GLib.info ("detach-trace: BaseAgentSession.close() done id=%s", id.handle);
close_request.resolve (true);
}
@@ -280,8 +276,6 @@ namespace Frida {
transmitter.new_candidates.disconnect (on_transmitter_new_candidates);
transmitter.candidate_gathering_done.disconnect (on_transmitter_candidate_gathering_done);
GLib.info ("detach-trace: BaseAgentSession emitting closed id=%s", id.handle);
closed ();
}
+7 -1
View File
@@ -35,15 +35,21 @@ namespace Frida {
interceptor.attach ((void *) Gum.Process.find_module_by_name ("kernel32.dll").find_export_by_name ("ExitProcess"),
listener);
#else
var libc = Gum.Process.get_libc_module ();
const string[] apis = {
"exit",
"_exit",
"abort",
};
#if PROSPERO
foreach (var symbol in apis) {
interceptor.attach ((void *) Gum.Module.find_global_export_by_name (symbol), listener);
}
#else
var libc = Gum.Process.get_libc_module ();
foreach (var symbol in apis) {
interceptor.attach ((void *) libc.find_export_by_name (symbol), listener);
}
#endif
#endif
}
+18 -5
View File
@@ -28,6 +28,7 @@
#undef feof
#undef ferror
#undef fgetwc
#undef fileno
#undef fopen
#undef fopen64
#undef fputwc
@@ -214,6 +215,7 @@ static void frida_stdio_unregister_stream (FILE * stream);
static void frida_stdio_register_dir (DIR * dirp);
static void frida_stdio_unregister_dir (DIR * dirp);
#endif
static void frida_stdio_ensure_registries (void);
static void frida_flush_all_streams (int * result);
static FridaFile * frida_file_get_impl (FILE * stream);
@@ -301,10 +303,7 @@ frida_libc_shim_init (void)
G_LOCK (frida_stdio);
frida_streams = g_hash_table_new (g_direct_hash, g_direct_equal);
#ifdef HAVE_FRIDA_DIR
frida_dirs = g_hash_table_new (g_direct_hash, g_direct_equal);
#endif
frida_stdio_ensure_registries ();
G_UNLOCK (frida_stdio);
@@ -638,7 +637,7 @@ __vsnprintf_chk (char * string, size_t size, int flags, size_t len, const char *
return gum_vsnprintf (string, size, format, args);
}
#ifdef HAVE_XLOCALE_H
#if defined (HAVE_XLOCALE_H) && !defined (HAVE_PROSPERO)
G_GNUC_INTERNAL int
sprintf_l (char * string, locale_t loc, const char * format, ...)
@@ -1904,6 +1903,7 @@ frida_stdio_register_stream (FILE * stream)
{
G_LOCK (frida_stdio);
frida_stdio_ensure_registries ();
g_hash_table_add (frida_streams, stream);
G_UNLOCK (frida_stdio);
@@ -1926,6 +1926,7 @@ frida_stdio_register_dir (DIR * dirp)
{
G_LOCK (frida_stdio);
frida_stdio_ensure_registries ();
g_hash_table_add (frida_dirs, dirp);
G_UNLOCK (frida_stdio);
@@ -1943,6 +1944,18 @@ frida_stdio_unregister_dir (DIR * dirp)
#endif
static void
frida_stdio_ensure_registries (void)
{
if (frida_streams != NULL)
return;
frida_streams = g_hash_table_new (g_direct_hash, g_direct_equal);
#ifdef HAVE_FRIDA_DIR
frida_dirs = g_hash_table_new (g_direct_hash, g_direct_equal);
#endif
}
static void
frida_flush_all_streams (int * result)
{
+15 -2
View File
@@ -12,14 +12,27 @@ frida_pipe_get_sddl_string_for_pipe (void)
#define DACL_ACE_EVERYONE_RW L"(A;;GRGW;;;WD)"
#define SACL_START L"S:"
#define SACL_ACE_LOWINTEGRITY_NORW L"(ML;;NWNR;;;LW)"
#define DACL_ACE_SYSTEM_RW L"(A;;GRGW;;;SY)"
#define DACL_ACE_ADMINS_RW L"(A;;GRGW;;;BA)"
if (frida_pipe_is_windows_8_or_greater ())
{
return DACL_START_NOINHERIT DACL_ACE_APPCONTAINER_RW DACL_ACE_EVERYONE_RW SACL_START SACL_ACE_LOWINTEGRITY_NORW;
return DACL_START_NOINHERIT
DACL_ACE_APPCONTAINER_RW
DACL_ACE_EVERYONE_RW
DACL_ACE_SYSTEM_RW
DACL_ACE_ADMINS_RW
SACL_START
SACL_ACE_LOWINTEGRITY_NORW;
}
else if (frida_pipe_is_windows_vista_or_greater ())
{
return DACL_START_NOINHERIT DACL_ACE_EVERYONE_RW SACL_START SACL_ACE_LOWINTEGRITY_NORW;
return DACL_START_NOINHERIT
DACL_ACE_EVERYONE_RW
DACL_ACE_SYSTEM_RW
DACL_ACE_ADMINS_RW
SACL_START
SACL_ACE_LOWINTEGRITY_NORW;
}
else
{
+23 -6
View File
@@ -273,6 +273,15 @@ if host_os != host_os_family
cdata.set('HAVE_' + host_os.to_upper(), 1)
endif
host_is_prospero = host_os_family == 'freebsd' and cc.get_define('__PROSPERO__') != ''
prospero_crt_link_args = []
if host_is_prospero
cdata.set('HAVE_PROSPERO', 1)
vala_flags += '--define=PROSPERO'
prospero_crt_link_args += \
run_command(cc.cmd_array(), '-print-file-name=crt1.o', check: true).stdout().strip()
endif
cpu_defines = [
['x86', 'HAVE_I386'],
['x86_64', 'HAVE_I386'],
@@ -527,7 +536,7 @@ have_droidy_backend = get_option('droidy_backend') \
have_socket_backend = get_option('socket_backend').allowed()
have_barebone_backend = get_option('barebone_backend') \
.disable_if(not (quickjs_dep.found() and lzfse_dep.found())) \
.disable_auto_if(not host_docks_mobile_devices) \
.disable_auto_if(not host_docks_mobile_devices and host_os != 'android') \
.allowed()
compiler_backend_opt = get_option('compiler_backend') \
.disable_auto_if(host_os == 'watchos')
@@ -595,9 +604,9 @@ if compiler_backend_opt.allowed()
endif
endif
elif compiler_backend_opt.enabled()
error('Need Go >= 1.24 to build the Compiler backend powered by ESBuild and typescript-go')
error('Need Go >= 1.26 to build the Compiler backend powered by ESBuild and typescript-go')
else
warning('Building without Compiler backend: Go >= 1.24 not found')
warning('Building without Compiler backend: Go >= 1.26 not found')
endif
endif
@@ -757,7 +766,9 @@ if host_os_family == 'linux' and have_local_backend
backend_reqs_private += 'libbpf'
endif
if host_os_family in ['darwin', 'freebsd'] or (host_os_family == 'linux' and cc.has_function('openpty', prefix: '#include <pty.h>'))
if host_os_family == 'darwin' \
or (host_os_family == 'freebsd' and not host_is_prospero) \
or (host_os_family == 'linux' and cc.has_function('openpty', prefix: '#include <pty.h>'))
vala_flags += '--define=HAVE_OPENPTY'
endif
@@ -879,13 +890,19 @@ if host_os_family == 'darwin'
endif
if host_os == 'macos'
backend_libs_private += ['-Wl,-framework,AppKit']
if have_compiler_backend
backend_libs_private += ['-Wl,-framework,CoreServices']
endif
endif
if host_os in ['ios', 'tvos']
backend_libs_private += ['-Wl,-framework,CoreGraphics', '-Wl,-framework,UIKit']
endif
if host_os_family == 'freebsd'
if host_os_family == 'freebsd' and not host_is_prospero
backend_libs_private += ['-lutil']
endif
if host_is_prospero
backend_libs_private += ['-lSceSystemService', '-lSceUserService']
endif
if get_option('devkits').length() != 0
if get_option('default_library') != 'static'
@@ -952,7 +969,7 @@ if host_os_family == 'darwin'
executable_no_exports_link_args = ['-Wl,-exported_symbols_list,' + script]
executable_no_exports_link_depends = [script]
elif host_os_family != 'windows'
script_name = (host_os == 'freebsd') ? 'executable-no-exports-freebsd.version' : 'executable-no-exports.version'
script_name = (host_os == 'freebsd' and not host_is_prospero) ? 'executable-no-exports-freebsd.version' : 'executable-no-exports.version'
script = meson.project_source_root() / 'tools' / script_name
executable_no_exports_link_args = ['-Wl,--version-script,' + script]
executable_no_exports_link_depends = [script]
+1 -1
Submodule releng updated: abb8238970...4e51c61880.
+1 -1
View File
@@ -19,7 +19,7 @@ if host_os_family == 'darwin'
extra_link_args += ['-Wl,-exported_symbols_list,' + meson.current_source_dir() / symlist]
extra_link_depends += [symlist]
elif host_os_family != 'windows'
symscript = (host_os == 'freebsd') ? 'frida-server-freebsd.version' : 'frida-server.version'
symscript = (host_os == 'freebsd' and not host_is_prospero) ? 'frida-server-freebsd.version' : 'frida-server.version'
extra_link_args += ['-Wl,--version-script,' + meson.current_source_dir() / symscript]
extra_link_depends += [symscript]
endif
+9
View File
@@ -29,6 +29,8 @@ void CFLog (CFLogLevel level, CFStringRef format, ...);
#elif defined (HAVE_ANDROID)
# include <android/log.h>
#elif defined (HAVE_PROSPERO)
# include <ps5/klog.h>
#else
# include <stdio.h>
#endif
@@ -137,6 +139,13 @@ frida_server_on_log_message (const gchar * log_domain, GLogLevelFlags log_level,
}
__android_log_write (priority, log_domain, message);
#elif defined (HAVE_PROSPERO)
(void) user_data;
if (log_domain != NULL)
klog_printf ("%s: %s\n", log_domain, message);
else
klog_printf ("%s\n", message);
#else
FILE * file = NULL;
const gchar * severity = NULL;
+4 -87
View File
@@ -12,7 +12,6 @@ namespace Frida.Server {
private static string? directory = null;
#if !WINDOWS && !TVOS
private static bool daemonize = false;
private static string? output_path = null;
#endif
private static string? softener_flavor_str = null;
private static bool enable_preload = true;
@@ -43,7 +42,6 @@ namespace Frida.Server {
{ "directory", 'd', 0, OptionArg.STRING, ref directory, "Store binaries in DIRECTORY", "DIRECTORY" },
#if !WINDOWS && !TVOS
{ "daemonize", 'D', 0, OptionArg.NONE, ref daemonize, "Detach and become a daemon", null },
{ "output", 'o', 0, OptionArg.FILENAME, ref output_path, "Redirect output to FILE", "FILE" },
#endif
{ "policy-softener", 0, 0, OptionArg.STRING, ref softener_flavor_str, "Select policy softener", "system|internal" },
{ "disable-preload", 'P', OptionFlags.REVERSE, OptionArg.NONE, ref enable_preload, "Disable preload optimization", null },
@@ -80,13 +78,6 @@ namespace Frida.Server {
Environment.set_verbose_logging_enabled (verbose);
#if !WINDOWS && !TVOS
if (output_path != null && !daemonize) {
if (!redirect_output_to (output_path))
return 6;
}
#endif
EndpointParameters endpoint_params;
try {
endpoint_params = new EndpointParameters (listen_address, 0, parse_certificate (certpath), origin,
@@ -165,15 +156,12 @@ namespace Frida.Server {
Posix.setsid ();
var null_in = Posix.open ("/dev/null", Posix.O_RDONLY);
var null_out = Posix.open ("/dev/null", Posix.O_WRONLY);
Posix.dup2 (null_in, Posix.STDIN_FILENO);
Posix.dup2 (null_out, Posix.STDOUT_FILENO);
Posix.dup2 (null_out, Posix.STDERR_FILENO);
Posix.close (null_in);
if (output_path == null || !redirect_output_to (output_path)) {
var null_out = Posix.open ("/dev/null", Posix.O_WRONLY);
Posix.dup2 (null_out, Posix.STDOUT_FILENO);
Posix.dup2 (null_out, Posix.STDERR_FILENO);
Posix.close (null_out);
}
Posix.close (null_out);
}
var status = new uint8[1];
@@ -230,22 +218,6 @@ namespace Frida.Server {
return application.run ();
}
#if !WINDOWS && !TVOS
private static bool redirect_output_to (string path) {
int fd = Posix.open (path, Posix.O_WRONLY | Posix.O_CREAT | Posix.O_APPEND, 0644);
if (fd == -1) {
printerr ("Unable to open “%s”: %s\n", path, Posix.strerror (Posix.errno));
return false;
}
Posix.dup2 (fd, Posix.STDOUT_FILENO);
Posix.dup2 (fd, Posix.STDERR_FILENO);
Posix.close (fd);
return true;
}
#endif
namespace Environment {
public extern void init ();
public extern void set_verbose_logging_enabled (bool enabled);
@@ -302,15 +274,6 @@ namespace Frida.Server {
return false;
});
#if !WINDOWS && !TVOS
if (!daemonize)
process_commands.begin ();
#elif !WINDOWS
process_commands.begin ();
#endif
start_alive_task_dumper ();
exit_code = 0;
loop.run ();
@@ -318,45 +281,6 @@ namespace Frida.Server {
return exit_code;
}
#if !WINDOWS
// TEMPORARY DEBUG HACK: type “t” + Enter to dump alive GTasks.
private async void process_commands () {
var input = new DataInputStream (new UnixInputStream (Posix.STDIN_FILENO, false));
while (true) {
string? line;
try {
line = yield input.read_line_async (Priority.DEFAULT, io_cancellable);
} catch (GLib.Error e) {
return;
}
if (line == null)
return;
switch (line.strip ()) {
case "t":
g_task_print_alive_tasks ();
break;
case "":
break;
default:
printerr ("Unknown command; try “t”\n");
break;
}
}
}
#endif
// TEMPORARY DEBUG HACK: dump alive GTasks every 30 seconds.
private void start_alive_task_dumper () {
var source = new TimeoutSource.seconds (30);
source.set_callback (() => {
GLib.info ("%s", g_task_describe_alive_tasks ());
return Source.CONTINUE;
});
source.attach (MainContext.default ());
}
private async void start () {
try {
if (device_id != null && device_id != "local") {
@@ -431,11 +355,4 @@ namespace Frida.Server {
return new TlsCertificate.from_file (path);
}
// TEMPORARY DEBUG HACK: only present when GLib is built with G_ENABLE_DEBUG.
[CCode (cname = "g_task_print_alive_tasks")]
private extern void g_task_print_alive_tasks ();
[CCode (cname = "g_task_describe_alive_tasks")]
private extern string g_task_describe_alive_tasks ();
}
+38 -38
View File
@@ -1,15 +1,15 @@
[CCode (gir_namespace = "FridaDroidy", gir_version = "1.0")]
namespace Frida.Droidy {
public sealed class DeviceTracker : Object {
public signal void device_attached (DeviceDetails details);
[CCode (gir_namespace = "FridaAdb", gir_version = "1.0")]
namespace Frida {
public sealed class AdbDeviceTracker : Object {
public signal void device_attached (AdbDeviceDetails details);
public signal void device_detached (string serial);
private Client? client;
private AdbClient? client;
private Gee.HashMap<string, DeviceEntry> devices = new Gee.HashMap<string, DeviceEntry> ();
private Cancellable io_cancellable = new Cancellable ();
public async void open (Cancellable? cancellable = null) throws Error, IOError {
client = yield Client.open (cancellable);
client = yield AdbClient.open (cancellable);
client.message.connect (on_message);
try {
@@ -20,7 +20,7 @@ namespace Frida.Droidy {
client.message.disconnect (on_message);
client = null;
client = yield Client.open (cancellable);
client = yield AdbClient.open (cancellable);
var devices_encoded = yield client.request_data ("host:track-devices", cancellable);
yield update_devices (devices_encoded, cancellable);
}
@@ -117,12 +117,12 @@ namespace Frida.Droidy {
var still_attached = devices.has_key (entry.serial);
if (still_attached) {
entry.announced = true;
device_attached (new DeviceDetails (entry.serial, entry.name));
device_attached (new AdbDeviceDetails (entry.serial, entry.name));
}
}
private async string detect_name (string device_serial, Cancellable? cancellable) throws Error, IOError {
var output = yield ShellCommand.run ("getprop ro.product.model", device_serial, cancellable);
var output = yield AdbShellCommand.run ("getprop ro.product.model", device_serial, cancellable);
return output.chomp ();
}
@@ -149,7 +149,7 @@ namespace Frida.Droidy {
}
}
public sealed class DeviceDetails : Object {
public sealed class AdbDeviceDetails : Object {
public string serial {
get;
construct;
@@ -160,16 +160,16 @@ namespace Frida.Droidy {
construct;
}
public DeviceDetails (string serial, string name) {
public AdbDeviceDetails (string serial, string name) {
Object (serial: serial, name: name);
}
}
namespace ShellCommand {
namespace AdbShellCommand {
private const int CHUNK_SIZE = 4096;
public static async string run (string command, string device_serial, Cancellable? cancellable = null) throws Error, IOError {
var client = yield Client.open (cancellable);
var client = yield AdbClient.open (cancellable);
try {
yield client.request ("host:transport:" + device_serial, cancellable);
@@ -205,8 +205,8 @@ namespace Frida.Droidy {
}
}
public sealed class ShellSession : Object {
public signal void output (StdioPipe pipe, Bytes bytes);
public sealed class AdbShellSession : Object {
public signal void output (AdbStdioPipe pipe, Bytes bytes);
public signal void closed ();
private IOStream stream;
@@ -232,7 +232,7 @@ namespace Frida.Droidy {
public async void open (string device_serial, Cancellable? cancellable = null) throws Error, IOError {
assert (state == CLOSED);
var client = yield Client.open (cancellable);
var client = yield AdbClient.open (cancellable);
try {
yield client.request ("host:transport:" + device_serial, cancellable);
@@ -285,7 +285,7 @@ namespace Frida.Droidy {
return result.stdout_text;
}
public async ShellCommandResult run (string command, Cancellable? cancellable) throws Error, IOError {
public async AdbShellCommandResult run (string command, Cancellable? cancellable) throws Error, IOError {
if (state == CLOSED)
throw new Error.INVALID_OPERATION ("Shell session is closed");
@@ -524,7 +524,7 @@ namespace Frida.Droidy {
public string command;
private SourceFunc? handler;
public ShellCommandResult? result;
public AdbShellCommandResult? result;
public GLib.Error? error;
public ByteArray stdout_buffer = new ByteArray ();
@@ -550,7 +550,7 @@ namespace Frida.Droidy {
Bytes stdout_bytes = new Bytes.take ((owned) stdout_terminated);
Bytes stderr_bytes = new Bytes.take ((owned) stderr_terminated);
result = new ShellCommandResult (status, stdout_bytes, stderr_bytes);
result = new AdbShellCommandResult (status, stdout_bytes, stderr_bytes);
handler ();
handler = null;
@@ -589,12 +589,12 @@ namespace Frida.Droidy {
}
}
public enum StdioPipe {
public enum AdbStdioPipe {
STDOUT,
STDERR
}
public sealed class ShellCommandResult : Object {
public sealed class AdbShellCommandResult : Object {
public uint8 status {
get;
construct;
@@ -622,7 +622,7 @@ namespace Frida.Droidy {
construct;
}
public ShellCommandResult (uint8 status, Bytes stdout_bytes, Bytes stderr_bytes) {
public AdbShellCommandResult (uint8 status, Bytes stdout_bytes, Bytes stderr_bytes) {
Object (
status: status,
stdout_bytes: stdout_bytes,
@@ -631,12 +631,12 @@ namespace Frida.Droidy {
}
}
namespace FileSync {
namespace AdbFileSync {
private const size_t MAX_DATA_SIZE = 65536;
public static async void send (InputStream content, FileMetadata metadata, string remote_path, string device_serial,
public static async void send (InputStream content, AdbFileMetadata metadata, string remote_path, string device_serial,
Cancellable? cancellable = null) throws Error, IOError {
var client = yield Client.open (cancellable);
var client = yield AdbClient.open (cancellable);
try {
yield client.request ("host:transport:" + device_serial, cancellable);
@@ -686,7 +686,7 @@ namespace Frida.Droidy {
}
}
public sealed class FileMetadata : Object {
public sealed class AdbFileMetadata : Object {
public uint32 mode {
get;
set;
@@ -700,16 +700,16 @@ namespace Frida.Droidy {
}
}
public sealed class JDWPTracker : Object {
public sealed class AdbJDWPTracker : Object {
public signal void debugger_attached (uint pid);
public signal void debugger_detached (uint pid);
private Client? client;
private AdbClient? client;
private Gee.HashSet<uint> debugger_pids = new Gee.HashSet<uint> ();
private Cancellable io_cancellable = new Cancellable ();
public async void open (string device_serial, Cancellable? cancellable = null) throws Error, IOError {
client = yield Client.open (cancellable);
client = yield AdbClient.open (cancellable);
client.message.connect (on_message);
try {
@@ -768,7 +768,7 @@ namespace Frida.Droidy {
}
}
public sealed class Client : Object {
public sealed class AdbClient : Object {
public signal void closed ();
public signal void message (string payload);
@@ -793,7 +793,7 @@ namespace Frida.Droidy {
private const uint16 ADB_SERVER_DEFAULT_PORT = 5037;
private const size_t MAX_MESSAGE_LENGTH = 65536;
public static async Client open (Cancellable? cancellable = null) throws Error, IOError {
public static async AdbClient open (Cancellable? cancellable = null) throws Error, IOError {
SocketConnectable? connectable = null;
string? env_socket_address = Environment.get_variable ("ADB_SERVER_SOCKET");
string? env_server_address = Environment.get_variable ("ANDROID_ADB_SERVER_ADDRESS");
@@ -847,10 +847,10 @@ namespace Frida.Droidy {
throw new Error.NOT_SUPPORTED ("%s", e.message);
}
return new Client (stream);
return new AdbClient (stream);
}
public Client (IOStream stream) {
public AdbClient (IOStream stream) {
Object (stream: stream);
}
@@ -896,10 +896,10 @@ namespace Frida.Droidy {
yield request_with_type (message, RequestType.PROTOCOL_CHANGE, cancellable);
}
public async SyncSession request_sync_session (Cancellable? cancellable = null) throws Error, IOError {
public async AdbSyncSession request_sync_session (Cancellable? cancellable = null) throws Error, IOError {
yield request ("sync:", cancellable);
var session = new SyncSession (this);
var session = new AdbSyncSession (this);
PendingResponse pending = null;
pending = new PendingResponse (RequestType.SYNC, () => {
@@ -1154,15 +1154,15 @@ namespace Frida.Droidy {
}
}
public sealed class SyncSession : Object {
public Client client {
public sealed class AdbSyncSession : Object {
public AdbClient client {
get;
construct;
}
private Promise<bool> io_request = new Promise<bool> ();
public SyncSession (Client client) {
public AdbSyncSession (AdbClient client) {
Object (client: client);
}
+84 -2
View File
@@ -5,10 +5,92 @@ namespace Frida {
construct;
}
private AdbShellSession? shell;
public AndroidHelperClient (AndroidHelperTransport transport) {
Object (transport: transport);
}
public static async AndroidHelperClient open (string serial, Cancellable? cancellable) throws Error, IOError {
string instance_id = Uuid.string_random ().replace ("-", "");
string helper_path = "/data/local/tmp/frida-helper-" + instance_id + ".dex";
var helper_dex = new MemoryInputStream.from_bytes (
new Bytes.static (Frida.Data.Android.get_helper_dex_blob ().data));
var helper_meta = new AdbFileMetadata ();
helper_meta.mode = 0100644;
helper_meta.time_modified = new DateTime.now_utc ();
yield AdbFileSync.send (helper_dex, helper_meta, helper_path, serial, cancellable);
var shell = new AdbShellSession ();
try {
var output = new StringBuilder ();
bool waiting = false;
var output_handler = shell.output.connect ((pipe, bytes) => {
if (pipe == STDOUT) {
output.append ((string) bytes.get_data ());
if (waiting)
open.callback ();
}
});
try {
yield shell.open (serial, cancellable);
shell.send_command (("CLASSPATH=%s app_process /data/local/tmp " +
"--nice-name=re.frida.helper re.frida.Helper %s; rm -f %s; echo BYE.")
.printf (helper_path, instance_id, helper_path));
while (!output.str.has_prefix ("READY.\n")) {
waiting = true;
yield;
waiting = false;
if (output.str.has_prefix ("BYE.\n"))
throw new Error.NOT_SUPPORTED ("Unable to start helper");
}
} finally {
shell.disconnect (output_handler);
}
var client = yield AdbClient.open (cancellable);
try {
yield client.request ("host:transport:" + serial, cancellable);
yield client.request_protocol_change ("localabstract:/frida-helper-" + instance_id,
cancellable);
} catch (GLib.Error e) {
client.close.begin ();
throw e;
}
var helper = new AndroidHelperClient (new AndroidHelperStreamTransport (client.stream));
helper.shell = shell;
helper.transport.closed.connect (helper.release_shell);
return helper;
} catch (GLib.Error e) {
shell.close.begin ();
if (e is IOError.CANCELLED)
throw (IOError) e;
throw new Error.NOT_SUPPORTED ("%s", e.message);
}
}
public async void close (Cancellable? cancellable) throws IOError {
yield transport.close (cancellable);
if (shell != null) {
yield shell.close (cancellable);
shell = null;
}
}
private void release_shell () {
if (shell != null) {
shell.close.begin ();
shell = null;
}
}
public async HostApplicationInfo get_frontmost_application (FrontmostQueryOptions options,
Cancellable? cancellable) throws Error, IOError {
var stanza = new Json.Builder ();
@@ -408,13 +490,13 @@ namespace Frida {
}
public interface AndroidHelperTransport : Object {
public signal void closed ();
public abstract async void close (Cancellable? cancellable) throws IOError;
public abstract async Json.Node request (Json.Node stanza, Cancellable? cancellable) throws Error, IOError;
}
public sealed class AndroidHelperStreamTransport : Object, AndroidHelperTransport {
public signal void closed ();
public IOStream stream {
get;
construct;
Binary file not shown.
+39 -17
View File
@@ -18,7 +18,6 @@ import android.graphics.drawable.Drawable;
import android.os.Looper;
import android.os.Process;
import android.util.Base64;
import android.util.Base64OutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -645,7 +644,7 @@ public class HelperBackend {
"Unable to find application with identifier '" + pkgName + "'" +
((uid != 0) ? " belonging to uid " + uid : ""));
} catch (Throwable e) {
return error("NOT_SUPPORTED", e.toString());
return error("NOT_SUPPORTED", describe(e));
}
}
@@ -697,10 +696,13 @@ public class HelperBackend {
intent.setClassName(pkgName, activity);
}
if (mStartActivityAsUser != null)
startActivityViaAtm(intent, uid, pkgName);
else
startActivityViaAm(intent, uid, pkgName);
String callingPackage = ourCallingPackage();
int code = (mStartActivityAsUser != null)
? startActivityViaAtm(intent, uid, callingPackage)
: startActivityViaAm(intent, uid, callingPackage);
if (code < 0 || code > 3)
return error("NOT_SUPPORTED", "Activity launch was refused (start code " + code + ")");
return okVoid();
} catch (NameNotFoundException e) {
@@ -708,11 +710,18 @@ public class HelperBackend {
"Unable to find application with identifier '" + pkgName + "'" +
((uid != 0) ? " belonging to uid " + uid : ""));
} catch (Throwable e) {
return error("NOT_SUPPORTED", e.toString());
return error("NOT_SUPPORTED", describe(e));
}
}
private void startActivityViaAtm(Intent intent, int uid, String callingPackage) throws Exception {
private String ourCallingPackage() {
String[] packages = mPackageManager.getPackagesForUid(Process.myUid());
if (packages != null && packages.length > 0)
return packages[0];
return "com.android.shell";
}
private int startActivityViaAtm(Intent intent, int uid, String callingPackage) throws Exception {
if (mStartActivityAsUser == null)
throw new UnsupportedOperationException("startActivityAsUser unavailable");
@@ -729,10 +738,11 @@ public class HelperBackend {
args[mStartActivityCallingPackageIndex] = callingPackage;
args[mStartActivityUserIdIndex] = (uid != 0) ? uid : 0;
mStartActivityAsUser.invoke(mActivityTaskManager, args);
Object result = mStartActivityAsUser.invoke(mActivityTaskManager, args);
return (result instanceof Integer) ? (Integer) result : 0;
}
private void startActivityViaAm(Intent intent, int uid, String callingPackage) throws Exception {
private int startActivityViaAm(Intent intent, int uid, String callingPackage) throws Exception {
if (mStartActivityLegacy == null)
throw new UnsupportedOperationException("legacy startActivity unavailable");
@@ -753,7 +763,8 @@ public class HelperBackend {
if (mLegacyStartUserIdIndex != -1)
args[mLegacyStartUserIdIndex] = (uid != 0) ? uid : 0;
mStartActivityLegacy.invoke(mActivityManagerService, args);
Object result = mStartActivityLegacy.invoke(mActivityManagerService, args);
return (result instanceof Integer) ? (Integer) result : 0;
}
private JSONArray sendBroadcast(JSONArray request) throws JSONException {
@@ -782,7 +793,7 @@ public class HelperBackend {
"Unable to find application with identifier '" + pkgName + "'" +
((uid != 0) ? " belonging to uid " + uid : ""));
} catch (Throwable e) {
return error("NOT_SUPPORTED", e.toString());
return error("NOT_SUPPORTED", describe(e));
}
}
@@ -810,7 +821,7 @@ public class HelperBackend {
"Unable to find application with identifier '" + pkgName + "'" +
((uid != 0) ? " belonging to uid " + uid : ""));
} catch (Throwable e) {
return error("NOT_SUPPORTED", e.toString());
return error("NOT_SUPPORTED", describe(e));
}
}
@@ -837,7 +848,7 @@ public class HelperBackend {
return okBoolean(false);
} catch (Throwable e) {
return error("NOT_SUPPORTED", e.toString());
return error("NOT_SUPPORTED", describe(e));
}
}
@@ -869,6 +880,17 @@ public class HelperBackend {
return r;
}
private static String describe(Throwable t) {
Throwable cause = t;
while (cause instanceof InvocationTargetException && cause.getCause() != null)
cause = cause.getCause();
String message = cause.getMessage();
return (message != null)
? cause.getClass().getName() + ": " + message
: cause.getClass().getName();
}
private String getFrontmostPackageName() {
List<ActivityManager.RunningTaskInfo> tasks = mActivityManager.getRunningTasks(1);
if (tasks.isEmpty()) {
@@ -959,10 +981,10 @@ public class HelperBackend {
icon.setBounds(0, 0, width, height);
icon.draw(canvas);
ByteArrayOutputStream output = new ByteArrayOutputStream();
bitmap.compress(CompressFormat.PNG, 100, new Base64OutputStream(output, Base64.NO_WRAP));
ByteArrayOutputStream png = new ByteArrayOutputStream();
bitmap.compress(CompressFormat.PNG, 100, png);
return output.toString();
return Base64.encodeToString(png.toByteArray(), Base64.NO_WRAP);
}
private Map<String, List<RunningAppProcessInfo>> getAppProcesses() {
+87 -11
View File
@@ -21,10 +21,19 @@ CORE_TAG_FIELD = f"{{{CORE_NAMESPACE}}}field"
CORE_TAG_CONSTRUCTOR = f"{{{CORE_NAMESPACE}}}constructor"
CORE_TAG_METHOD = f"{{{CORE_NAMESPACE}}}method"
OBJECT_TYPE_PATTERN = re.compile(r"\bpublic\s+(sealed )?(class|interface)\s+(\w+)\b")
INTERNAL_INTERFACES = {
"Frida.HostSessionHub",
"Frida.AgentMessageSink",
"FridaBase.AgentMessageSink",
"Json.Serializable",
}
OBJECT_TYPE_PATTERN = re.compile(r"\bpublic\s+(sealed |abstract )?(class|interface)\s+(\w+)\b")
TOPLEVEL_NAMES = [
"frida.vala",
"linux-kernel-image.vala",
"xnu-kernelcache.vala",
"package-manager.vala",
"control-service.vala",
"portal-service.vala",
@@ -131,6 +140,7 @@ def emit_header(api, output_dir):
output_header_file.write("\n\n/* Library lifetime */")
output_header_file.write("\nvoid frida_init (void);")
output_header_file.write("\nvoid frida_init_with_runtime (FridaRuntime runtime);")
output_header_file.write("\nvoid frida_shutdown (void);")
output_header_file.write("\nvoid frida_deinit (void);")
output_header_file.write("\nGMainContext * frida_get_main_context (void);")
@@ -230,10 +240,10 @@ def emit_gir(api: ApiSpec, core_gir: str, base_gir: str, output_dir: Path, docs:
for elem in core_elements + base_elements:
if tag_name == "class":
for child in list(elem):
if (child.tag == CORE_TAG_IMPLEMENTS and child.get("name") in {"Frida.HostSessionHub", "Frida.AgentMessageSink",
"FridaBase.AgentMessageSink"}) \
if (child.tag == CORE_TAG_IMPLEMENTS and child.get("name") in INTERNAL_INTERFACES) \
or child.tag == CORE_TAG_FIELD \
or child.get("name").startswith("_"):
or child.get("name").startswith("_") \
or (child.tag == CORE_TAG_CONSTRUCTOR and is_async_constructor(child)):
elem.remove(child)
merged_namespace.append(elem)
@@ -241,6 +251,13 @@ def emit_gir(api: ApiSpec, core_gir: str, base_gir: str, output_dir: Path, docs:
merge_and_transform_elements("interface", object_type_names)
merge_and_transform_elements("enumeration", enum_type_names | error_type_names)
function_names = {func.name for func in api.functions}
for source_root in [core_root, base_root]:
source_namespace = source_root.find("namespace", GIR_NAMESPACES)
for function in source_namespace.findall("function", GIR_NAMESPACES):
if function.get("name") in function_names:
merged_namespace.append(function)
for source_root in [core_root, base_root]:
for record in source_root.findall(".//record", GIR_NAMESPACES):
owner = record.get(f"{{{GLIB_NAMESPACE}}}is-gtype-struct-for")
@@ -277,6 +294,12 @@ def emit_gir(api: ApiSpec, core_gir: str, base_gir: str, output_dir: Path, docs:
def filter_elements(elements: List[ET.Element], spec_set: Set[str]):
return [elem for elem in elements if elem.get("name") in spec_set]
def is_async_constructor(constructor: ET.Element) -> bool:
for type_elem in constructor.iter(f"{{{CORE_NAMESPACE}}}type"):
if type_elem.get("name") == "Gio.Cancellable":
return True
return False
@dataclass
class DocComment:
body: str
@@ -604,10 +627,14 @@ def emit_vapi(api, output_dir):
output_vapi_file.write("[CCode (cheader_filename = \"frida-core.h\", cprefix = \"Frida\", lower_case_cprefix = \"frida_\")]")
output_vapi_file.write("\nnamespace Frida {")
output_vapi_file.write("\n\tpublic static void init ();")
output_vapi_file.write("\n\tpublic static void init_with_runtime (Frida.Runtime runtime);")
output_vapi_file.write("\n\tpublic static void shutdown ();")
output_vapi_file.write("\n\tpublic static void deinit ();")
output_vapi_file.write("\n\tpublic static unowned GLib.MainContext get_main_context ();")
for delegate in referenced_delegates(api):
output_vapi_file.write("\n\t" + delegate)
for object_type in api.object_types:
output_vapi_file.write("\n\n\t%s" % object_type.vapi_declaration)
sections = []
@@ -642,6 +669,35 @@ def emit_vapi(api, output_dir):
output_deps_file.write("glib-2.0\n")
output_deps_file.write("gobject-2.0\n")
output_deps_file.write("gio-2.0\n")
output_deps_file.write("json-glib-1.0\n")
def referenced_delegates(api) -> List[str]:
members = []
for object_type in api.object_types:
members.extend(object_type.vapi_properties)
members.extend(object_type.vapi_methods)
members.extend(object_type.vapi_signals)
if object_type.vapi_constructor is not None:
members.append(object_type.vapi_constructor)
for func in api.functions:
members.append(func.vapi_declaration)
text = "\n".join(members)
return [declaration for name, declaration in api.delegates
if re.search(r"\bFrida\." + name + r"\b", text) is not None]
def strip_internal_interfaces(declaration: str) -> str:
head, sep, rest = declaration.partition(" : ")
if not sep:
return declaration
bases_part, brace_sep, tail = rest.partition("{")
kept = [b.strip() for b in bases_part.split(",") if b.strip() not in INTERNAL_INTERFACES]
if not kept:
return f"{head} {brace_sep}{tail}"
return f"{head} : {', '.join(kept)} {brace_sep}{tail}"
def emit_symbol_maps_from_source(output_dir: Path):
with OutputFile(output_dir / 'frida-core.version') as f:
@@ -800,8 +856,7 @@ def parse_api(frida_version, frida_version_components, api_version, toplevel_cod
if stripped_line == "}":
ignoring = False
else:
if stripped_line.startswith("public abstract") \
or stripped_line.startswith("public class Promise") \
if stripped_line.startswith("public class Promise") \
or stripped_line.startswith("public interface Future") \
or stripped_line.startswith("public class CF"):
ignoring = True
@@ -821,7 +876,7 @@ def parse_api(frida_version, frida_version_components, api_version, toplevel_cod
ignoring = True
else:
current_object_type = object_type_by_name[name]
current_object_type.vapi_declaration = stripped_line
current_object_type.vapi_declaration = strip_internal_interfaces(stripped_line)
elif stripped_line == "}":
current_enum = None
current_object_type = None
@@ -850,17 +905,26 @@ def parse_api(frida_version, frida_version_components, api_version, toplevel_cod
enum.vapi_declaration = m.group(1)
enum.vapi_members.extend([line.lstrip() for line in m.group(2).strip().split("\n")])
functions = [f for f in parse_vapi_functions(base_vapi) if function_is_public(f.name)]
functions = [f for f in parse_vapi_functions(core_vapi + base_vapi) if function_is_public(f.name)]
for f in functions:
m = re.search(r"^[\w\*]+ frida_{}.+?;".format(f.name), all_headers, re.MULTILINE | re.DOTALL)
f.c_prototype = beautify_cprototype(m.group(0))
m = re.search(r"^(?:VALA_EXTERN )?[\w\*]+ frida_{}.+?;".format(f.name), all_headers, re.MULTILINE | re.DOTALL)
prototype = beautify_cprototype(m.group(0))
if prototype.startswith("VALA_EXTERN "):
prototype = prototype[12:]
f.c_prototype = prototype
return ApiSpec(frida_version, frida_version_components, api_version, object_types, functions, enum_types, error_types)
delegates = [(m.group(1), m.group(0).strip())
for m in re.finditer(r"^\tpublic delegate .+?\b(\w+) \(.*\);$", core_vapi + base_vapi, re.MULTILINE)]
return ApiSpec(frida_version, frida_version_components, api_version, object_types, functions, enum_types, error_types,
delegates)
def function_is_public(name):
return not name.startswith("_") and \
not name.startswith("throw_") and \
name not in MANUALLY_DECLARED_FUNCTIONS and \
name not in {
"on_pending_garbage",
"generate_certificate",
"get_dbus_context",
"invalidate_dbus_context",
@@ -881,6 +945,17 @@ def function_is_public(name):
"make_stdio_pipe",
}
MANUALLY_DECLARED_FUNCTIONS = {
"init",
"init_with_runtime",
"shutdown",
"deinit",
"get_main_context",
"unref",
"version",
"version_string",
}
def parse_vala_object_types(source) -> List[ApiObjectType]:
return [ApiObjectType(m.group(3), m.group(2)) for m in OBJECT_TYPE_PATTERN.finditer(source, re.MULTILINE)]
@@ -896,6 +971,7 @@ class ApiSpec:
functions: List[ApiFunction]
enum_types: List[ApiEnum]
error_types: List[ApiEnum]
delegates: List[tuple]
class ApiEnum:
def __init__(self, name):
+8 -1
View File
@@ -132,13 +132,20 @@ if not is_shared
endif
core_public_dep = declare_dependency(kwargs: core_public_dep_kwargs)
core_public_reqs = ['glib-2.0', 'gobject-2.0', 'gio-2.0', 'json-glib-1.0']
if host_os_family == 'windows'
core_public_reqs += 'gio-windows-2.0'
else
core_public_reqs += 'gio-unix-2.0'
endif
pkg = import('pkgconfig')
pkg_kwargs = {
'filebase': f'frida-core-@api_version@',
'name': 'Frida Core Library',
'version': frida_version,
'description': 'Instrumentation toolkit for developers, reverse-engineers, and security researchers',
'requires': ['glib-2.0', 'gobject-2.0', 'gio-2.0', 'json-glib-1.0'],
'requires': core_public_reqs,
'subdirs': f'frida-@api_version@',
'extra_cflags': ['-I${frida_includedir}'],
'variables': installed_variables,
File diff suppressed because it is too large. Load diff
+4 -4
View File
@@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "bindgen"
version = "0.72.0"
version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags",
"cexpr",
@@ -63,9 +63,9 @@ checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "clang-sys"
version = "1.8.1"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
dependencies = [
"glob",
"libc",
+25
View File
@@ -4,6 +4,31 @@ version = "1.0.0"
edition = "2024"
build = "build.rs"
[lib]
name = "frida_barebone_agent"
path = "src/lib.rs"
crate-type = ["rlib", "staticlib"]
# The XNU flavour is a freestanding executable the host injects and calls into.
# The Linux flavour is the staticlib above, linked into a .ko by linux/Kbuild.
[[bin]]
name = "frida-barebone-agent"
path = "src/main.rs"
required-features = ["blob"]
[features]
# Flavors the host injects and calls into, as opposed to the module Linux loads.
blob = []
# The XNU flavour itself, and the two ways it is packaged: a blob the host injects
# into a running kernel, and a kext that kernel loads.
xnu-core = []
xnu = ["xnu-core", "blob"]
xnu-kext = ["xnu-core"]
win9x = ["blob"]
winnt = ["blob"]
linux = []
linux-injected = ["blob"]
[profile.release]
panic = "abort"
opt-level = "z"
+68 -17
View File
@@ -1,30 +1,81 @@
## Getting the C toolchain
## Getting the SDK and the devkit
npm install -g xpm
xpm install
export PATH=$PWD/xpacks/.bin:$PATH
The agent is built against the bare soft-float SDK, which carries picolibc and the
compiler runtime, and against a GumJS devkit built for the same flavor. Which flavor
is the target's, listed under Targets below; this walks through the XNU one. No
prebuilt bundles are published for these, so build both:
## How to build Gum
machine=none-arm64-softfloat_nopic
./configure \
--host=aarch64-none-elf \
--enable-gumjs \
--with-devkits=gum,gumjs \
--with-devkit-symbol-scope=original
make
export GUMJS_DEVKIT_DIR=$PWD/build/bindings/gumjs/devkit
releng/deps.py build --bundle=sdk --host=$machine
mkdir -p ~/sdk-$machine
tar -C ~/sdk-$machine -xf deps/sdk-$machine.tar.xz
gum=~/src/frida-gum
mkdir -p $gum/build/$machine
(cd $gum/build/$machine \
&& FRIDA_DEPS=$HOME/src/frida-core/deps CC=clang CXX=clang++ AR=llvm-ar \
RANLIB=llvm-ranlib NM=llvm-nm STRIP=llvm-strip \
../../configure --host=$machine --enable-gumjs \
--with-devkits=gumjs \
&& python3 ../../releng/meson/meson.py configure -Ddevkit_symbol_scope=original \
&& ninja)
## Building
export PATH=$PWD/xpacks/.bin:$PATH
export CC_aarch64_unknown_none=aarch64-none-elf-gcc
export AR_aarch64_unknown_none=aarch64-none-elf-ar
export RANLIB_aarch64_unknown_none=aarch64-none-elf-ranlib
sdk=~/sdk-$machine
export GUMJS_DEVKIT_DIR=$gum/build/$machine/bindings/gumjs/devkit
export CC="clang -I$sdk/include -I$sdk/include/glib-2.0 -I$sdk/lib/glib-2.0/include \
-I$sdk/include/capstone -I$sdk/include/json-glib-1.0"
export RUSTFLAGS="-L native=$sdk/lib"
RUSTC_BOOTSTRAP=1 cargo build --release --features xnu \
--target aarch64-unknown-none-softfloat --target-dir target-xnu \
-Z build-std=core,alloc,compiler_builtins
Rust names no bare-metal target for the Windows guests, so the agent spells them
out and they are asked for by path:
RUSTC_BOOTSTRAP=1 cargo build --release --features win9x \
--target ./i686-unknown-none-pic.json -Z json-target-spec \
--target-dir target-win9x -Z build-std=core,alloc,compiler_builtins
## Targets
The agent supports four kernels. One of them has to be picked; none is a default,
since none follows from the host you build on:
- `xnu` — a freestanding ELF executable that the Barebone backend
injects into a running kernel from the outside, over a remote stub.
- `win9x` — the same, for a 32-bit Windows 9x guest.
- `winnt` — the same, for a Windows NT guest, either word size, on x86 or arm64.
- `linux` — a static library that becomes a kernel module, loaded from the
inside. See `linux/README.md`.
Each flavor has an SDK and a devkit of its own, since the addressing differs: an
image the host places goes in position-independent, a Linux module is absolute,
and NT's 64-bit half is called in the Microsoft ABI.
| Flavor | Machine | Rust target |
|---------|-----------------------------------|----------------------------------|
| `xnu` | `none-arm64-softfloat_nopic` | `aarch64-unknown-none-softfloat` |
| `win9x` | `none-x86-softfloat_pic` | `i686-unknown-none-pic.json` |
| `winnt` | `none-x86-softfloat_pic` | `i686-unknown-none-pic.json` |
| `winnt` | `none-x86_64-softfloat_pic_msabi` | `x86_64-unknown-none-pic.json` |
| `winnt` | `none-arm64-softfloat_pic` | `aarch64-unknown-none-pic.json` |
| `linux` | `none-<arch>-softfloat_nopic` | see `linux/README.md` |
Everything above the `kernel` module is shared; each backend supplies the same
set of primitives (logging, allocation, threads, waiting, time, symbols) plus its
half of Gum's platform backend (`gum_xnu.rs` / `gum_windows.rs` / `gum_linux.rs`).
## Development loop
export FRIDA_BAREBONE_CONFIG=$PWD/etc/xnu.json
cargo build --release && make -C ~/src/frida-python && killall -9 qemu-system-aarch64 && sleep 2 && frida -D barebone -p 0
cargo build --release --features xnu --target aarch64-unknown-none-softfloat \
&& make -C ~/src/frida-python \
&& killall -9 qemu-system-aarch64 && sleep 2 \
&& frida -D barebone -p 0
## Speeding up loop
@@ -0,0 +1,21 @@
{
"abi": "softfloat",
"arch": "aarch64",
"crt-objects-fallback": "false",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32",
"disable-redzone": true,
"features": "+v8a,+strict-align,-neon,+reserve-x18",
"linker": "rust-lld",
"linker-flavor": "gnu-lld",
"llvm-target": "aarch64-unknown-none",
"max-atomic-width": 128,
"panic-strategy": "abort",
"plt-by-default": false,
"position-independent-executables": true,
"relocation-model": "pic",
"rustc-abi": "softfloat",
"stack-probes": {
"kind": "inline"
},
"target-pointer-width": 64
}
+150
View File
@@ -0,0 +1,150 @@
SECTIONS {
.text : {
*(.text.entrypoint)
*(.text*)
}
.rel.dyn : {
_agent_relocs_start = .;
*(.rel.dyn)
_agent_relocs_end = .;
}
.rodata : {
*(.rodata*)
}
.kernel_addrs : ALIGN(4) {
. = ALIGN(4);
__printk = .; . += 4;
_printk = .; . += 4;
_panic = .; . += 4;
_kthread_create_on_node = .; . += 4;
_wake_up_process = .; . += 4;
_fget = .; . += 4;
_kernel_read = .; . += 4;
_filp_open = .; . += 4;
_kernel_write = .; . += 4;
___kmalloc_noprof = .; . += 4;
_kmalloc = .; . += 4;
___kmalloc = .; . += 4;
_kfree = .; . += 4;
_vmalloc = .; . += 4;
_execmem_alloc = .; . += 4;
_set_memory_rw = .; . += 4;
_set_memory_ro = .; . += 4;
_set_memory_x = .; . += 4;
_set_memory_nx = .; . += 4;
_modules = .; . += 4;
_register_module_notifier = .; . += 4;
_unregister_module_notifier = .; . += 4;
_execmem_free = .; . += 4;
_module_alloc = .; . += 4;
_module_memfree = .; . += 4;
___init_waitqueue_head = .; . += 4;
_prepare_to_wait_event = .; . += 4;
_finish_wait = .; . += 4;
___wake_up = .; . += 4;
_autoremove_wake_function = .; . += 4;
_schedule_hrtimeout_range = .; . += 4;
_pci_get_domain_bus_and_slot = .; . += 4;
_pci_irq_vector = .; . += 4;
_of_irq_parse_and_map_pci = .; . += 4;
_pci_dev_put = .; . += 4;
_schedule = .; . += 4;
_ktime_get_mono_fast_ns = .; . += 4;
_ktime_get_real_ts64 = .; . += 4;
_getnstimeofday64 = .; . += 4;
_send_sig = .; . += 4;
_free_irq = .; . += 4;
_request_threaded_irq = .; . += 4;
_vmap = .; . += 4;
___pv_offset = .; . += 4;
_pgprot_kernel = .; . += 4;
_ioremap = .; . += 4;
_of_find_node_opts_by_path = .; . += 4;
_of_irq_get = .; . += 4;
_of_node_put = .; . += 4;
_init_task = .; . += 4;
_tasklist_lock = .; . += 4;
__raw_read_lock = .; . += 4;
__raw_read_unlock = .; . += 4;
__raw_read_lock_irqsave = .; . += 4;
__raw_read_unlock_irqrestore = .; . += 4;
__raw_write_lock = .; . += 4;
__raw_write_unlock = .; . += 4;
__raw_write_lock_irqsave = .; . += 4;
__raw_write_unlock_irqrestore = .; . += 4;
_detach_pid = .; . += 4;
_free_pids = .; . += 4;
_get_task_exe_file = .; . += 4;
_file_path = .; . += 4;
_fput = .; . += 4;
_copy_from_kernel_nofault = .; . += 4;
_get_task_mm = .; . += 4;
_kthread_use_mm = .; . += 4;
_kthread_unuse_mm = .; . += 4;
_mmput = .; . += 4;
_vm_mmap = .; . += 4;
_vm_munmap = .; . += 4;
_tracepoint_probe_register = .; . += 4;
_tracepoint_probe_unregister = .; . += 4;
___tracepoint_sched_process_fork = .; . += 4;
___tracepoint_sched_process_exit = .; . += 4;
___tracepoint_sched_process_exec = .; . += 4;
___tracepoint_task_rename = .; . += 4;
___set_task_comm = .; . += 4;
_vunmap = .; . += 4;
_arm_copy_to_user = .; . += 4;
_down_read = .; . += 4;
_up_read = .; . += 4;
_get_user_pages_unlocked = .; . += 4;
_get_user_pages_remote = .; . += 4;
_user_mode_thread = .; . += 4;
_fget_task = .; . += 4;
_find_task_by_vpid = .; . += 4;
___start_BTF = .; . += 4;
___stop_BTF = .; . += 4;
_call_usermodehelper_setup = .; . += 4;
_call_usermodehelper_exec = .; . += 4;
_arm_copy_from_user = .; . += 4;
_fixup_exception = .; . += 4;
_init_net = .; . += 4;
_sock_create_kern = .; . += 4;
_kernel_connect = .; . += 4;
_kernel_sendmsg = .; . += 4;
_kernel_recvmsg = .; . += 4;
_sock_release = .; . += 4;
_patch_text = .; . += 4;
}
/* Everything above is only ever read, so a process being instrumented can be shown the very
same pages; everything below it each copy needs its own of. */
. = ALIGN(4096);
_agent_private_start = .;
.data.rel.ro : {
*(.data.rel.ro*)
}
.got : {
*(.got*)
}
.init_array : {
_agent_init_start = .;
*(.init_array*)
_agent_init_end = .;
}
.data : {
*(.data*)
}
.bss : {
*(.bss*)
}
. = ALIGN(4);
_heap_start = .;
}
VERSION {
{
global:
_start;
local:
*;
};
}
+156
View File
@@ -0,0 +1,156 @@
SECTIONS {
.text : {
*(.text.entrypoint)
*(.text*)
}
.rela.dyn : {
_agent_relocs_start = .;
*(.rela.dyn)
_agent_relocs_end = .;
}
.rodata : {
*(.rodata*)
}
.kernel_addrs : ALIGN(8) {
. = ALIGN(8);
__printk = .; . += 8;
_printk = .; . += 8;
_panic = .; . += 8;
_kthread_create_on_node = .; . += 8;
_wake_up_process = .; . += 8;
_fget = .; . += 8;
_kernel_read = .; . += 8;
_filp_open = .; . += 8;
_kernel_write = .; . += 8;
___kmalloc_noprof = .; . += 8;
_kmalloc = .; . += 8;
___kmalloc = .; . += 8;
_kfree = .; . += 8;
_vmalloc = .; . += 8;
_execmem_alloc = .; . += 8;
_set_memory_rw = .; . += 8;
_set_memory_ro = .; . += 8;
_set_memory_x = .; . += 8;
_set_memory_nx = .; . += 8;
_modules = .; . += 8;
_register_module_notifier = .; . += 8;
_unregister_module_notifier = .; . += 8;
_execmem_free = .; . += 8;
_module_alloc = .; . += 8;
_module_memfree = .; . += 8;
___init_waitqueue_head = .; . += 8;
_prepare_to_wait_event = .; . += 8;
_finish_wait = .; . += 8;
___wake_up = .; . += 8;
_autoremove_wake_function = .; . += 8;
_schedule_hrtimeout_range = .; . += 8;
_pci_get_domain_bus_and_slot = .; . += 8;
_pci_irq_vector = .; . += 8;
_of_irq_parse_and_map_pci = .; . += 8;
_pci_dev_put = .; . += 8;
_schedule = .; . += 8;
_ktime_get_mono_fast_ns = .; . += 8;
_ktime_get_real_ts64 = .; . += 8;
_getnstimeofday64 = .; . += 8;
_send_sig = .; . += 8;
_free_irq = .; . += 8;
_request_threaded_irq = .; . += 8;
_generic_ioremap_prot = .; . += 8;
___ioremap = .; . += 8;
_vmap = .; . += 8;
_memstart_addr = .; . += 8;
_init_task = .; . += 8;
_tasklist_lock = .; . += 8;
__raw_read_lock = .; . += 8;
__raw_read_unlock = .; . += 8;
__raw_read_lock_irqsave = .; . += 8;
__raw_read_unlock_irqrestore = .; . += 8;
__raw_write_lock = .; . += 8;
__raw_write_unlock = .; . += 8;
__raw_write_lock_irqsave = .; . += 8;
__raw_write_unlock_irqrestore = .; . += 8;
_detach_pid = .; . += 8;
_free_pids = .; . += 8;
_get_task_exe_file = .; . += 8;
_file_path = .; . += 8;
_fput = .; . += 8;
_copy_from_kernel_nofault = .; . += 8;
_get_task_mm = .; . += 8;
_kthread_use_mm = .; . += 8;
_kthread_unuse_mm = .; . += 8;
_mmput = .; . += 8;
_vm_mmap = .; . += 8;
_vm_munmap = .; . += 8;
_tracepoint_probe_register = .; . += 8;
_tracepoint_probe_unregister = .; . += 8;
___tracepoint_sched_process_fork = .; . += 8;
___tracepoint_sched_process_exit = .; . += 8;
___tracepoint_sched_process_exec = .; . += 8;
___tracepoint_task_rename = .; . += 8;
___set_task_comm = .; . += 8;
_vunmap = .; . += 8;
___arch_copy_to_user = .; . += 8;
_down_read = .; . += 8;
_up_read = .; . += 8;
_get_user_pages_unlocked = .; . += 8;
_get_user_pages_remote = .; . += 8;
___arch_copy_from_user = .; . += 8;
_do_futex = .; . += 8;
_user_mode_thread = .; . += 8;
_apply_to_existing_page_range = .; . += 8;
_caches_clean_inval_pou = .; . += 8;
_do_madvise = .; . += 8;
_fget_task = .; . += 8;
_find_task_by_vpid = .; . += 8;
___start_BTF = .; . += 8;
___stop_BTF = .; . += 8;
_call_usermodehelper_setup = .; . += 8;
_call_usermodehelper_exec = .; . += 8;
_access_process_vm = .; . += 8;
_fixup_exception = .; . += 8;
_aarch64_insn_patch_text_nosync = .; . += 8;
_aarch64_insn_patch_text = .; . += 8;
_kthread_worker_fn = .; . += 8;
_vring_interrupt = .; . += 8;
_init_net = .; . += 8;
_sock_create_kern = .; . += 8;
_kernel_connect = .; . += 8;
_kernel_sendmsg = .; . += 8;
_kernel_recvmsg = .; . += 8;
_sock_release = .; . += 8;
_sock_def_readable = .; . += 8;
}
/* Everything above is only ever read, so a process being instrumented can be shown the very
same pages; everything below it each copy needs its own of. */
. = ALIGN(4096);
_agent_private_start = .;
.data.rel.ro : {
*(.data.rel.ro*)
}
.got : {
*(.got*)
}
.init_array : {
_agent_init_start = .;
*(.init_array*)
_agent_init_end = .;
}
.data : {
*(.data*)
}
.bss : {
*(.bss*)
}
. = ALIGN(8);
_heap_start = .;
}
VERSION {
{
global:
_start;
local:
*;
};
}
+146
View File
@@ -0,0 +1,146 @@
SECTIONS {
.text : {
*(.text.entrypoint)
*(.text*)
}
.rel.dyn : {
_agent_relocs_start = .;
*(.rel.dyn)
_agent_relocs_end = .;
}
.rodata : {
*(.rodata*)
}
.kernel_addrs : ALIGN(4) {
. = ALIGN(4);
__printk = .; . += 4;
_printk = .; . += 4;
_panic = .; . += 4;
_kthread_create_on_node = .; . += 4;
_wake_up_process = .; . += 4;
_fget = .; . += 4;
_kernel_read = .; . += 4;
_filp_open = .; . += 4;
_kernel_write = .; . += 4;
___kmalloc_noprof = .; . += 4;
_kmalloc = .; . += 4;
_kfree = .; . += 4;
_vmalloc = .; . += 4;
_execmem_alloc = .; . += 4;
_set_memory_rw = .; . += 4;
_set_memory_ro = .; . += 4;
_set_memory_x = .; . += 4;
_set_memory_nx = .; . += 4;
_modules = .; . += 4;
_register_module_notifier = .; . += 4;
_unregister_module_notifier = .; . += 4;
_text_poke_kgdb = .; . += 4;
_execmem_free = .; . += 4;
_module_alloc = .; . += 4;
_module_memfree = .; . += 4;
___init_waitqueue_head = .; . += 4;
_prepare_to_wait_event = .; . += 4;
_finish_wait = .; . += 4;
___wake_up = .; . += 4;
_autoremove_wake_function = .; . += 4;
_schedule_hrtimeout_range = .; . += 4;
_pci_get_domain_bus_and_slot = .; . += 4;
_pci_irq_vector = .; . += 4;
_pci_dev_put = .; . += 4;
_schedule = .; . += 4;
_ktime_get_mono_fast_ns = .; . += 4;
_ktime_get_real_ts64 = .; . += 4;
_send_sig = .; . += 4;
_free_irq = .; . += 4;
_request_threaded_irq = .; . += 4;
_vmap = .; . += 4;
_current_task = .; . += 4;
_ioremap = .; . += 4;
_ioremap_nocache = .; . += 4;
___default_kernel_pte_mask = .; . += 4;
_init_task = .; . += 4;
_tasklist_lock = .; . += 4;
__raw_read_lock = .; . += 4;
__raw_read_unlock = .; . += 4;
__raw_read_lock_irqsave = .; . += 4;
__raw_read_unlock_irqrestore = .; . += 4;
__raw_write_lock = .; . += 4;
__raw_write_unlock = .; . += 4;
__raw_write_lock_irqsave = .; . += 4;
__raw_write_unlock_irqrestore = .; . += 4;
_detach_pid = .; . += 4;
_free_pids = .; . += 4;
_get_task_exe_file = .; . += 4;
_file_path = .; . += 4;
_fput = .; . += 4;
_copy_from_kernel_nofault = .; . += 4;
_get_task_mm = .; . += 4;
_kthread_use_mm = .; . += 4;
_kthread_unuse_mm = .; . += 4;
_mmput = .; . += 4;
_vm_mmap = .; . += 4;
_vm_munmap = .; . += 4;
_tracepoint_probe_register = .; . += 4;
_tracepoint_probe_unregister = .; . += 4;
___tracepoint_sched_process_fork = .; . += 4;
___tracepoint_sched_process_exit = .; . += 4;
___tracepoint_sched_process_exec = .; . += 4;
_vunmap = .; . += 4;
__copy_to_user = .; . += 4;
_down_read = .; . += 4;
_up_read = .; . += 4;
_get_user_pages_unlocked = .; . += 4;
_get_user_pages_remote = .; . += 4;
_do_futex = .; . += 4;
_user_mode_thread = .; . += 4;
___start_BTF = .; . += 4;
___stop_BTF = .; . += 4;
_call_usermodehelper_setup = .; . += 4;
_call_usermodehelper_exec = .; . += 4;
__copy_from_user = .; . += 4;
_access_process_vm = .; . += 4;
_fget_task = .; . += 4;
_find_task_by_vpid = .; . += 4;
_init_net = .; . += 4;
_sock_create_kern = .; . += 4;
_kernel_connect = .; . += 4;
_kernel_sendmsg = .; . += 4;
_kernel_recvmsg = .; . += 4;
_sock_release = .; . += 4;
_register_die_notifier = .; . += 4;
_unregister_die_notifier = .; . += 4;
}
/* Everything above is only ever read, so a process being instrumented can be shown the very
same pages; everything below it each copy needs its own of. */
. = ALIGN(4096);
_agent_private_start = .;
.data.rel.ro : {
*(.data.rel.ro*)
}
.got : {
*(.got*)
}
.init_array : {
_agent_init_start = .;
*(.init_array*)
_agent_init_end = .;
}
.data : {
*(.data*)
}
.bss : {
*(.bss*)
}
. = ALIGN(4);
_heap_start = .;
}
VERSION {
{
global:
_start;
local:
*;
};
}
+154
View File
@@ -0,0 +1,154 @@
SECTIONS {
.text : {
*(.text.entrypoint)
*(.text*)
}
.rela.dyn : {
_agent_relocs_start = .;
*(.rela.dyn)
_agent_relocs_end = .;
}
.rodata : {
*(.rodata*)
}
.kernel_addrs : ALIGN(8) {
. = ALIGN(8);
__printk = .; . += 8;
_printk = .; . += 8;
_panic = .; . += 8;
_kthread_create_on_node = .; . += 8;
_wake_up_process = .; . += 8;
_fget = .; . += 8;
_kernel_read = .; . += 8;
_filp_open = .; . += 8;
_kernel_write = .; . += 8;
___kmalloc_noprof = .; . += 8;
_kmalloc = .; . += 8;
___kmalloc = .; . += 8;
_kfree = .; . += 8;
_vmalloc = .; . += 8;
_execmem_alloc = .; . += 8;
_set_memory_rw = .; . += 8;
_set_memory_ro = .; . += 8;
_set_memory_x = .; . += 8;
_set_memory_nx = .; . += 8;
_modules = .; . += 8;
_register_module_notifier = .; . += 8;
_unregister_module_notifier = .; . += 8;
_text_poke_kgdb = .; . += 8;
_execmem_free = .; . += 8;
_module_alloc = .; . += 8;
_module_memfree = .; . += 8;
___init_waitqueue_head = .; . += 8;
_prepare_to_wait_event = .; . += 8;
_finish_wait = .; . += 8;
___wake_up = .; . += 8;
_autoremove_wake_function = .; . += 8;
_schedule_hrtimeout_range = .; . += 8;
_pci_get_domain_bus_and_slot = .; . += 8;
_pci_irq_vector = .; . += 8;
_pci_dev_put = .; . += 8;
_schedule = .; . += 8;
_ktime_get_mono_fast_ns = .; . += 8;
_ktime_get_real_ts64 = .; . += 8;
_getnstimeofday64 = .; . += 8;
_send_sig = .; . += 8;
_free_irq = .; . += 8;
_request_threaded_irq = .; . += 8;
_vmap = .; . += 8;
_page_offset_base = .; . += 8;
_current_task = .; . += 8;
_cpu_current_top_of_stack = .; . += 8;
_register_die_notifier = .; . += 8;
_unregister_die_notifier = .; . += 8;
_ioremap = .; . += 8;
_ioremap_nocache = .; . += 8;
___default_kernel_pte_mask = .; . += 8;
_init_task = .; . += 8;
_tasklist_lock = .; . += 8;
__raw_read_lock = .; . += 8;
__raw_read_unlock = .; . += 8;
__raw_read_lock_irqsave = .; . += 8;
__raw_read_unlock_irqrestore = .; . += 8;
__raw_write_lock = .; . += 8;
__raw_write_unlock = .; . += 8;
__raw_write_lock_irqsave = .; . += 8;
__raw_write_unlock_irqrestore = .; . += 8;
_detach_pid = .; . += 8;
_free_pids = .; . += 8;
_get_task_exe_file = .; . += 8;
_file_path = .; . += 8;
_fput = .; . += 8;
_copy_from_kernel_nofault = .; . += 8;
_get_task_mm = .; . += 8;
_kthread_use_mm = .; . += 8;
_kthread_unuse_mm = .; . += 8;
_mmput = .; . += 8;
_vm_mmap = .; . += 8;
_vm_munmap = .; . += 8;
_tracepoint_probe_register = .; . += 8;
_tracepoint_probe_unregister = .; . += 8;
___tracepoint_sched_process_fork = .; . += 8;
___tracepoint_sched_process_exit = .; . += 8;
___tracepoint_sched_process_exec = .; . += 8;
___tracepoint_task_rename = .; . += 8;
___set_task_comm = .; . += 8;
_vunmap = .; . += 8;
__copy_to_user = .; . += 8;
_down_read = .; . += 8;
_up_read = .; . += 8;
_get_user_pages_unlocked = .; . += 8;
_get_user_pages_remote = .; . += 8;
_do_futex = .; . += 8;
_user_mode_thread = .; . += 8;
___start_BTF = .; . += 8;
___stop_BTF = .; . += 8;
_call_usermodehelper_setup = .; . += 8;
_call_usermodehelper_exec = .; . += 8;
__copy_from_user = .; . += 8;
_access_process_vm = .; . += 8;
_fget_task = .; . += 8;
_find_task_by_vpid = .; . += 8;
_init_net = .; . += 8;
_sock_create_kern = .; . += 8;
_kernel_connect = .; . += 8;
_kernel_sendmsg = .; . += 8;
_kernel_recvmsg = .; . += 8;
_sock_release = .; . += 8;
_sock_def_readable = .; . += 8;
_of_irq_parse_and_map_pci = .; . += 8;
}
/* Everything above is only ever read, so a process being instrumented can be shown the very
same pages; everything below it each copy needs its own of. */
. = ALIGN(4096);
_agent_private_start = .;
.data.rel.ro : {
*(.data.rel.ro*)
}
.got : {
*(.got*)
}
.init_array : {
_agent_init_start = .;
*(.init_array*)
_agent_init_end = .;
}
.data : {
*(.data*)
}
.bss : {
*(.bss*)
}
. = ALIGN(8);
_heap_start = .;
}
VERSION {
{
global:
_start;
local:
*;
};
}
+97
View File
@@ -0,0 +1,97 @@
SECTIONS {
.text : {
*(.text.entrypoint)
*(.text*)
}
.rel.dyn : {
_agent_relocs_start = .;
*(.rel.dyn)
_agent_relocs_end = .;
}
.rodata : {
*(.rodata*)
}
.kernel_addrs : ALIGN(4) {
. = ALIGN(4);
_Get_Cur_VM_Handle = .; . += 4;
_Get_Sys_VM_Handle = .; . += 4;
_Schedule_Global_Event = .; . += 4;
_Create_Semaphore = .; . += 4;
_Wait_Semaphore = .; . += 4;
_Signal_Semaphore = .; . += 4;
_Get_Next_VM_Handle = .; . += 4;
_Release_Time_Slice = .; . += 4;
_Set_Global_Time_Out = .; . += 4;
_Cancel_Time_Out = .; . += 4;
_Get_System_Time = .; . += 4;
__HeapAllocate = .; . += 4;
__HeapFree = .; . += 4;
__PageAllocate = .; . += 4;
__PageFree = .; . += 4;
__CopyPageTable = .; . += 4;
__MapPhysToLinear = .; . += 4;
_VMM_Add_DDB = .; . += 4;
_VMM_Remove_DDB = .; . += 4;
__RegOpenKey = .; . += 4;
__RegCloseKey = .; . += 4;
__RegEnumKey = .; . += 4;
__RegQueryValueEx = .; . += 4;
__PageReserve = .; . += 4;
__PageCommit = .; . += 4;
_Hook_VMM_Fault = .; . += 4;
_Unhook_VMM_Fault = .; . += 4;
_Fatal_Error_Handler = .; . += 4;
_Get_Cur_Thread_Handle = .; . += 4;
_Get_Initial_Thread_Handle = .; . += 4;
_Get_Next_Thread_Handle = .; . += 4;
__Debug_Printf_Service = .; . += 4;
__ContextSwitch = .; . += 4;
__PageModifyPermissions = .; . += 4;
__GetCurrentContext = .; . += 4;
_VPICD_Virtualize_IRQ = .; . += 4;
_VPICD_Force_Default_Behavior = .; . += 4;
_VPICD_Phys_EOI = .; . += 4;
_VPICD_Physically_Unmask = .; . += 4;
__VWIN32_QueueUserApc = .; . += 4;
__VWIN32_Get_Thread_Context = .; . += 4;
__VWIN32_Set_Thread_Context = .; . += 4;
__VWIN32_CreateRing0Thread = .; . += 4;
_IFSMgr_Ring0_FileIO = .; . += 4;
_KERNEL32_Base = .; . += 4;
_KERNEL32_ProcessIdObfuscator = .; . += 4;
_KERNEL32_ModuleTable = .; . += 4;
__PageCommitPhys = .; . += 4;
}
/* Everything above is only ever read, so a process being instrumented can be shown the very
same pages; everything below it each copy needs its own of. */
. = ALIGN(4096);
_agent_private_start = .;
.data.rel.ro : {
*(.data.rel.ro*)
}
.got : {
*(.got*)
}
.init_array : {
_agent_init_start = .;
*(.init_array*)
_agent_init_end = .;
}
.bss : {
*(.bss*)
}
. = ALIGN(4);
_heap_start = .;
}
VERSION {
{
global:
_start;
frida_win9x_user_main;
local:
*;
};
}
+112
View File
@@ -0,0 +1,112 @@
SECTIONS {
.text : {
*(.text.entrypoint)
*(.text*)
}
.rela.dyn : {
_agent_relocs_start = .;
*(.rela.dyn)
_agent_relocs_end = .;
}
.rodata : {
*(.rodata*)
}
.kernel_addrs : ALIGN(8) {
. = ALIGN(8);
_ExAllocatePoolWithTag = .; . += 8;
_ExFreePoolWithTag = .; . += 8;
_PsCreateSystemThread = .; . += 8;
_PsGetCurrentThreadId = .; . += 8;
_KeInitializeEvent = .; . += 8;
_KeWaitForSingleObject = .; . += 8;
_KeSetEvent = .; . += 8;
_KeInitializeDpc = .; . += 8;
_PsSetCreateThreadNotifyRoutine = .; . += 8;
_PsRemoveCreateThreadNotifyRoutine = .; . += 8;
_KeInsertQueueDpc = .; . += 8;
_ZwYieldExecution = .; . += 8;
_KeExpandKernelStackAndCalloutEx = .; . += 8;
_KeBugCheckEx = .; . += 8;
_DbgPrint = .; . += 8;
_MmGetVirtualForPhysical = .; . += 8;
_MmMapIoSpace = .; . += 8;
_MmGetPhysicalAddress = .; . += 8;
_HalGetInterruptVector = .; . += 8;
_IoConnectInterrupt = .; . += 8;
_IoDisconnectInterrupt = .; . += 8;
_PsActiveProcessHead = .; . += 8;
_PsInitialSystemProcess = .; . += 8;
_PsGetProcessId = .; . += 8;
_PsGetProcessImageFileName = .; . += 8;
_PsGetProcessPeb = .; . += 8;
_PsGetCurrentThread = .; . += 8;
_PsGetThreadProcess = .; . += 8;
_PsGetThreadId = .; . += 8;
_PsGetThreadTeb = .; . += 8;
_PsGetContextThread = .; . += 8;
_PsSetContextThread = .; . += 8;
_KeStackAttachProcess = .; . += 8;
_KeUnstackDetachProcess = .; . += 8;
_ZwCreateFile = .; . += 8;
_ZwOpenKey = .; . += 8;
_ZwEnumerateKey = .; . += 8;
_ZwQueryValueKey = .; . += 8;
_ZwReadFile = .; . += 8;
_ZwWriteFile = .; . += 8;
_ZwEnumerateValueKey = .; . += 8;
_ZwDeviceIoControlFile = .; . += 8;
_ZwClose = .; . += 8;
_ZwCreateEvent = .; . += 8;
_ExAllocatePool2 = .; . += 8;
_MmCopyVirtualMemory = .; . += 8;
_KeIpiGenericCall = .; . += 8;
_MmIsAddressValid = .; . += 8;
_ZwClearEvent = .; . += 8;
_ZwWaitForSingleObject = .; . += 8;
_ZwAllocateVirtualMemory = .; . += 8;
_ZwFreeVirtualMemory = .; . += 8;
_ZwCreateThread = .; . += 8;
_PsProcessType = .; . += 8;
_ObReferenceObjectByHandle = .; . += 8;
_ObOpenObjectByPointer = .; . += 8;
_ExEventObjectType = .; . += 8;
_IoAllocateMdl = .; . += 8;
_IoFreeMdl = .; . += 8;
_MmBuildMdlForNonPagedPool = .; . += 8;
_MmMapLockedPagesSpecifyCache = .; . += 8;
_MmUnmapLockedPages = .; . += 8;
}
. = ALIGN(4096);
_agent_private_start = .;
.data.rel.ro : {
*(.data.rel.ro*)
}
.got : {
*(.got*)
}
.init_array : {
_agent_init_start = .;
*(.init_array*)
_agent_init_end = .;
}
.data : {
*(.data*)
}
.bss : {
*(.bss*)
}
. = ALIGN(8);
_heap_start = .;
}
VERSION {
{
global:
_start;
frida_winnt_user_main;
frida_winnt_user_bootstrap;
local:
*;
};
}
+111
View File
@@ -0,0 +1,111 @@
SECTIONS {
.text : {
*(.text.entrypoint)
*(.text*)
}
.rel.dyn : {
_agent_relocs_start = .;
*(.rel.dyn)
_agent_relocs_end = .;
}
.rodata : {
*(.rodata*)
}
.kernel_addrs : ALIGN(4) {
. = ALIGN(4);
_ExAllocatePoolWithTag = .; . += 4;
_ExFreePoolWithTag = .; . += 4;
_PsCreateSystemThread = .; . += 4;
_PsGetCurrentThreadId = .; . += 4;
_KeInitializeEvent = .; . += 4;
_KeWaitForSingleObject = .; . += 4;
_KeSetEvent = .; . += 4;
_KeInitializeDpc = .; . += 4;
_PsSetCreateThreadNotifyRoutine = .; . += 4;
_PsRemoveCreateThreadNotifyRoutine = .; . += 4;
_KeInsertQueueDpc = .; . += 4;
_ZwYieldExecution = .; . += 4;
_KeExpandKernelStackAndCalloutEx = .; . += 4;
_KeBugCheckEx = .; . += 4;
_DbgPrint = .; . += 4;
_MmGetVirtualForPhysical = .; . += 4;
_MmMapIoSpace = .; . += 4;
_MmGetPhysicalAddress = .; . += 4;
_HalGetInterruptVector = .; . += 4;
_IoConnectInterrupt = .; . += 4;
_IoDisconnectInterrupt = .; . += 4;
_PsActiveProcessHead = .; . += 4;
_PsInitialSystemProcess = .; . += 4;
_PsGetProcessId = .; . += 4;
_PsGetProcessImageFileName = .; . += 4;
_PsGetProcessPeb = .; . += 4;
_PsGetCurrentThread = .; . += 4;
_PsGetThreadProcess = .; . += 4;
_PsGetThreadId = .; . += 4;
_PsGetThreadTeb = .; . += 4;
_PsGetContextThread = .; . += 4;
_PsSetContextThread = .; . += 4;
_KeStackAttachProcess = .; . += 4;
_KeUnstackDetachProcess = .; . += 4;
_ZwCreateFile = .; . += 4;
_ZwOpenKey = .; . += 4;
_ZwEnumerateKey = .; . += 4;
_ZwQueryValueKey = .; . += 4;
_ZwReadFile = .; . += 4;
_ZwWriteFile = .; . += 4;
_ZwEnumerateValueKey = .; . += 4;
_ZwDeviceIoControlFile = .; . += 4;
_ZwClose = .; . += 4;
_ZwCreateEvent = .; . += 4;
_ExAllocatePool2 = .; . += 4;
_MmCopyVirtualMemory = .; . += 4;
_KeIpiGenericCall = .; . += 4;
_MmIsAddressValid = .; . += 4;
_ZwClearEvent = .; . += 4;
_ZwWaitForSingleObject = .; . += 4;
_ZwAllocateVirtualMemory = .; . += 4;
_ZwFreeVirtualMemory = .; . += 4;
_ZwCreateThread = .; . += 4;
_PsProcessType = .; . += 4;
_ObReferenceObjectByHandle = .; . += 4;
_ObOpenObjectByPointer = .; . += 4;
_ExEventObjectType = .; . += 4;
_IoAllocateMdl = .; . += 4;
_IoFreeMdl = .; . += 4;
_MmBuildMdlForNonPagedPool = .; . += 4;
_MmMapLockedPagesSpecifyCache = .; . += 4;
_MmUnmapLockedPages = .; . += 4;
}
/* Everything above is only ever read, so a process being instrumented can be shown the very
same pages; everything below it each copy needs its own of. */
. = ALIGN(4096);
_agent_private_start = .;
.data.rel.ro : {
*(.data.rel.ro*)
}
.got : {
*(.got*)
}
.init_array : {
_agent_init_start = .;
*(.init_array*)
_agent_init_end = .;
}
.bss : {
*(.bss*)
}
. = ALIGN(4);
_heap_start = .;
}
VERSION {
{
global:
_start;
frida_winnt_user_main;
frida_winnt_user_bootstrap;
local:
*;
};
}
+121
View File
@@ -0,0 +1,121 @@
SECTIONS {
/* The large code model gives every section an l-prefixed name of its own, so that
the compiler can reach it with a 64-bit displacement rather than assume 2GB. */
.text : {
*(.text.entrypoint)
*(.ltext.entrypoint)
*(.text*)
*(.ltext*)
}
.rela.dyn : {
_agent_relocs_start = .;
*(.rela.dyn)
_agent_relocs_end = .;
}
.rodata : {
*(.rodata*)
*(.lrodata*)
}
.kernel_addrs : ALIGN(8) {
. = ALIGN(8);
_ExAllocatePoolWithTag = .; . += 8;
_ExFreePoolWithTag = .; . += 8;
_PsCreateSystemThread = .; . += 8;
_PsGetCurrentThreadId = .; . += 8;
_KeInitializeEvent = .; . += 8;
_KeWaitForSingleObject = .; . += 8;
_KeSetEvent = .; . += 8;
_KeInitializeDpc = .; . += 8;
_PsSetCreateThreadNotifyRoutine = .; . += 8;
_PsRemoveCreateThreadNotifyRoutine = .; . += 8;
_KeInsertQueueDpc = .; . += 8;
_ZwYieldExecution = .; . += 8;
_KeExpandKernelStackAndCalloutEx = .; . += 8;
_KeBugCheckEx = .; . += 8;
_DbgPrint = .; . += 8;
_MmGetVirtualForPhysical = .; . += 8;
_MmMapIoSpace = .; . += 8;
_MmGetPhysicalAddress = .; . += 8;
_HalGetInterruptVector = .; . += 8;
_IoConnectInterrupt = .; . += 8;
_IoDisconnectInterrupt = .; . += 8;
_PsActiveProcessHead = .; . += 8;
_PsInitialSystemProcess = .; . += 8;
_PsGetProcessId = .; . += 8;
_PsGetProcessImageFileName = .; . += 8;
_PsGetProcessPeb = .; . += 8;
_PsGetCurrentThread = .; . += 8;
_PsGetThreadProcess = .; . += 8;
_PsGetThreadId = .; . += 8;
_PsGetThreadTeb = .; . += 8;
_PsGetContextThread = .; . += 8;
_PsSetContextThread = .; . += 8;
_KeStackAttachProcess = .; . += 8;
_KeUnstackDetachProcess = .; . += 8;
_ZwCreateFile = .; . += 8;
_ZwOpenKey = .; . += 8;
_ZwEnumerateKey = .; . += 8;
_ZwQueryValueKey = .; . += 8;
_ZwReadFile = .; . += 8;
_ZwWriteFile = .; . += 8;
_ZwEnumerateValueKey = .; . += 8;
_ZwDeviceIoControlFile = .; . += 8;
_ZwClose = .; . += 8;
_ZwCreateEvent = .; . += 8;
_ExAllocatePool2 = .; . += 8;
_MmCopyVirtualMemory = .; . += 8;
_KeIpiGenericCall = .; . += 8;
_MmIsAddressValid = .; . += 8;
_ZwClearEvent = .; . += 8;
_ZwWaitForSingleObject = .; . += 8;
_ZwAllocateVirtualMemory = .; . += 8;
_ZwFreeVirtualMemory = .; . += 8;
_ZwCreateThread = .; . += 8;
_PsProcessType = .; . += 8;
_ObReferenceObjectByHandle = .; . += 8;
_ObOpenObjectByPointer = .; . += 8;
_ExEventObjectType = .; . += 8;
_IoAllocateMdl = .; . += 8;
_IoFreeMdl = .; . += 8;
_MmBuildMdlForNonPagedPool = .; . += 8;
_MmMapLockedPagesSpecifyCache = .; . += 8;
_MmUnmapLockedPages = .; . += 8;
}
/* Everything above is only ever read, so a process being instrumented can be shown the very
same pages; everything below it each copy needs its own of. */
. = ALIGN(4096);
_agent_private_start = .;
.data.rel.ro : {
*(.data.rel.ro*)
}
.got : {
*(.got*)
}
.init_array : {
_agent_init_start = .;
*(.init_array*)
_agent_init_end = .;
}
.data : {
*(.data*)
*(.ldata*)
}
.bss : {
*(.bss*)
*(.lbss*)
}
. = ALIGN(8);
_heap_start = .;
}
VERSION {
{
global:
_start;
frida_winnt_user_main;
frida_winnt_user_bootstrap;
local:
*;
};
}
+112
View File
@@ -0,0 +1,112 @@
SECTIONS {
.text : {
*(.text.entrypoint)
*(.text*)
}
.rela.dyn : {
_agent_relocs_start = .;
*(.rela.dyn)
_agent_relocs_end = .;
}
.rodata : {
*(.rodata*)
}
.kernel_addrs : ALIGN(8) {
. = ALIGN(8);
_panic = .; . += 8;
_IOLog = .; . += 8;
_kalloc = .; . += 8;
_proc_iterate = .; . += 8;
_proc_find = .; . += 8;
_proc_rele = .; . += 8;
_proc_pid = .; . += 8;
_proc_best_name = .; . += 8;
_proc_task = .; . += 8;
_current_task = .; . += 8;
_current_thread = .; . += 8;
_get_task_map = .; . += 8;
_mach_vm_allocate = .; . += 8;
_mach_vm_deallocate = .; . += 8;
_mach_vm_protect = .; . += 8;
_mach_vm_remap = .; . += 8;
_vm_map_copyin = .; . += 8;
_vm_map_copyout = .; . += 8;
_thread_create = .; . += 8;
_thread_set_state = .; . += 8;
_thread_resume = .; . += 8;
_thread_terminate = .; . += 8;
_task_suspend = .; . += 8;
_task_resume = .; . += 8;
_task_clear_return_wait = .; . += 8;
_get_bsdtask_info = .; . += 8;
_thread_suspend = .; . += 8;
_kfree = .; . += 8;
_kernel_thread_start = .; . += 8;
_assert_wait = .; . += 8;
_assert_wait_timeout = .; . += 8;
_thread_block = .; . += 8;
_thread_wakeup = .; . += 8;
_thread_wakeup_prim = .; . += 8;
_mach_absolute_time = .; . += 8;
_absolutetime_to_nanoseconds = .; . += 8;
_clock_get_calendar_microtime = .; . += 8;
_ml_io_map = .; . += 8;
_ml_vtophys = .; . += 8;
_ml_static_vtop = .; . += 8;
__ZN9IOService11getPlatformEv = .; . += 8;
__ZN9IOServiceC2Ev = .; . += 8;
__ZN8OSSymbol17withCStringNoCopyEPKc = .; . += 8;
__ZN6OSData9withBytesEPKvj = .; . += 8;
_kalloc_data = .; . += 8;
_kfree_data = .; . += 8;
_wakeup = .; . += 8;
_sock_socket = .; . += 8;
_sock_connect = .; . += 8;
_sock_send = .; . += 8;
_sock_receive = .; . += 8;
_sock_close = .; . += 8;
_sysent = .; . += 8;
_task_restartable_ranges_register = .; . += 8;
_task_threads = .; . += 8;
_mach_vm_region = .; . += 8;
_mach_vm_region_recurse = .; . += 8;
_copyin = .; . += 8;
_copyout = .; . += 8;
_ulock_wake = .; . += 8;
_task_info = .; . += 8;
_convert_port_to_thread = .; . += 8;
_convert_port_to_thread_read = .; . += 8;
_convert_port_to_thread_inspect = .; . += 8;
}
. = ALIGN(16384);
_agent_private_start = .;
.data.rel.ro : {
*(.data.rel.ro*)
}
.got : {
*(.got*)
}
.init_array : {
_agent_init_start = .;
*(.init_array*)
_agent_init_end = .;
}
.data : {
*(.data*)
}
.bss : {
*(.bss*)
}
. = ALIGN(8);
_heap_start = .;
}
VERSION {
{
global:
_start;
local:
*;
};
}
-59
View File
@@ -1,59 +0,0 @@
SECTIONS {
.text : {
*(.text.entrypoint)
*(.text*)
}
.rodata : {
*(.rodata*)
}
.kernel_addrs : ALIGN(8) {
. = ALIGN(8);
_panic = .; . += 8;
_IOLog = .; . += 8;
_kalloc = .; . += 8;
_kfree = .; . += 8;
_kernel_thread_start = .; . += 8;
_assert_wait = .; . += 8;
_assert_wait_timeout = .; . += 8;
_thread_block = .; . += 8;
_thread_wakeup = .; . += 8;
_mach_absolute_time = .; . += 8;
_absolutetime_to_nanoseconds = .; . += 8;
_clock_get_calendar_microtime = .; . += 8;
_ml_io_map = .; . += 8;
_ml_vtophys = .; . += 8;
__ZN9IOService11getPlatformEv = .; . += 8;
__ZN9IOServiceC2Ev = .; . += 8;
__ZN8OSSymbol17withCStringNoCopyEPKc = .; . += 8;
__ZN6OSData9withBytesEPKvj = .; . += 8;
_kalloc_data = .; . += 8;
_kfree_data = .; . += 8;
_wakeup = .; . += 8;
_sock_socket = .; . += 8;
_sock_connect = .; . += 8;
_sock_send = .; . += 8;
_sock_receive = .; . += 8;
_sock_close = .; . += 8;
}
.data.rel.ro : {
*(.data.rel.ro*)
}
.got : {
*(.got*)
}
.bss : {
*(.bss*)
}
. = ALIGN(8);
_heap_start = .;
}
VERSION {
{
global:
_start;
local:
*;
};
}
@@ -0,0 +1,17 @@
{
"abi": "eabi",
"arch": "arm",
"crt-objects-fallback": "false",
"data-layout": "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64",
"features": "+v7,+thumb2,+thumb-mode,+soft-float,+strict-align,-neon",
"linker": "rust-lld",
"linker-flavor": "gnu-lld",
"llvm-floatabi": "soft",
"llvm-target": "armv7a-none-eabi",
"max-atomic-width": 64,
"panic-strategy": "abort",
"plt-by-default": false,
"position-independent-executables": true,
"relocation-model": "pic",
"target-pointer-width": 32
}
+147 -22
View File
@@ -8,12 +8,17 @@ fn main() {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let devkit_dir = PathBuf::from(env::var("GUMJS_DEVKIT_DIR").unwrap());
let cc_str = env::var("CC_aarch64_unknown_none")
let target = env::var("TARGET").unwrap();
let cc_str = env::var(format!("CC_{}", target.replace('-', "_")))
.or_else(|_| env::var("CC"))
.unwrap_or_else(|_| "cc".to_string());
let cc = Path::new(&cc_str);
let cc_include_paths = detect_gcc_include_paths(cc);
let cc_library_paths = detect_gcc_library_paths(cc);
// Same convention as cc-rs: the variable may carry flags, and a bare-metal
// compiler needs them to find its libc at all.
let mut cc_argv = cc_str.split_whitespace();
let cc = Path::new(cc_argv.next().unwrap());
let cc_args: Vec<&str> = cc_argv.collect();
let cc_include_paths = detect_gcc_include_paths(cc, &cc_args);
let cc_library_paths = detect_gcc_library_paths(cc, &cc_args);
let cc_clang_args: Vec<String> = cc_include_paths
.iter()
@@ -25,9 +30,15 @@ fn main() {
.header(devkit_dir.join("frida-gumjs.h").to_str().unwrap())
.clang_arg("-nostdinc")
.clang_arg("-target")
.clang_arg("aarch64-none-elf")
.clang_arg(format!("{}-none-elf", target.split('-').next().unwrap()))
.clang_args(cc_clang_args)
.clang_arg(format!("-I{}", devkit_dir.to_str().unwrap()))
.clang_arg("-include")
.clang_arg("stddef.h")
.clang_arg("-include")
.clang_arg("stdint.h")
.blocklist_type("max_align_t")
.blocklist_function("memcpy|memmove|memset|memcmp|bcmp|strlen")
.merge_extern_blocks(true)
.generate()
.expect("Unable to generate bindings");
@@ -36,25 +47,131 @@ fn main() {
.write_to_file(out_dir.join("bindings.rs"))
.expect("Couldn't write bindings");
println!(
"cargo:rustc-link-search=native={}",
devkit_dir.to_str().unwrap()
);
println!("cargo:rustc-link-lib=static=frida-gumjs");
for path in cc_library_paths {
println!("cargo:rustc-link-search=native={}", path.to_string_lossy());
// The Linux flavour is a staticlib: kbuild owns the final link, so it — not
// us — decides where the devkit and libc archives come from.
if env::var("CARGO_FEATURE_BLOB").is_ok() {
println!(
"cargo:rustc-link-search=native={}",
devkit_dir.to_str().unwrap()
);
println!("cargo:rustc-link-lib=static=frida-gumjs");
for path in cc_library_paths {
println!("cargo:rustc-link-search=native={}", path.to_string_lossy());
}
for arg in &cc_args {
if let Some(path) = arg.strip_prefix("-L") {
println!("cargo:rustc-link-search=native={path}");
}
}
println!("cargo:rustc-link-lib=static=c");
println!("cargo:rustc-link-lib=static=m");
println!("cargo:rustc-link-arg=--export-dynamic");
println!("cargo:rustc-link-arg=--emit-relocs");
let flavor = if env::var("CARGO_FEATURE_WIN9X").is_ok() {
"win9x"
} else if env::var("CARGO_FEATURE_WINNT").is_ok() {
"winnt"
} else if env::var("CARGO_FEATURE_LINUX_INJECTED").is_ok() {
"linux"
} else {
"xnu"
};
let arch = match target.split('-').next().unwrap() {
"i686" => "x86",
"armv7a" => "arm",
"aarch64" => "arm64",
other => other,
};
let script = format!("agent-{flavor}-{arch}.lds");
println!("cargo:rustc-link-arg=--script={script}");
println!("cargo:rustc-link-arg=--gc-sections");
// All of the agent lives in the library crate, so nothing in the binary
// references the entrypoint the host calls; keep the linker from
// dropping it along with the rest of the unreferenced archive members.
println!("cargo:rustc-link-arg=--undefined=_start");
let note = compile_version_note(&out_dir, cc, &cc_args, arch);
println!("cargo:rustc-link-arg={}", note.to_string_lossy());
}
println!("cargo:rustc-link-lib=static=c");
println!("cargo:rustc-link-lib=static=m");
println!("cargo:rustc-link-arg=--export-dynamic");
println!("cargo:rustc-link-arg=--emit-relocs");
println!("cargo:rustc-link-arg=--script=agent.lds");
println!("cargo:rustc-link-arg=--gc-sections");
if target.starts_with("i686") && env::var("CARGO_FEATURE_LINUX_INJECTED").is_ok() {
let shim = out_dir.join("regparm.o");
let status = Command::new(cc)
.args(&cc_args)
.args(["-m32", "-c", "-O2", "-fPIC", "-ffreestanding", "-o"])
.arg(&shim)
.arg("src/linux/regparm.c")
.status()
.expect("Couldn't run the compiler");
assert!(status.success(), "Couldn't compile the kernel-call shim");
println!("cargo:rustc-link-arg={}", shim.to_string_lossy());
println!("cargo:rerun-if-changed=src/linux/regparm.c");
}
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-env-changed=GUMJS_DEVKIT_DIR");
println!("cargo:rerun-if-env-changed=FRIDA_VERSION");
println!("cargo:rerun-if-changed={}", devkit_dir.join("frida-gumjs.h").display());
}
pub fn detect_gcc_include_paths(gcc: &Path) -> Vec<PathBuf> {
fn compile_version_note(out_dir: &Path, cc: &Path, cc_args: &[&str], arch: &str) -> PathBuf {
let version = env::var("FRIDA_VERSION")
.ok()
.filter(|v| !v.is_empty())
.unwrap_or_else(|| "0.0.0".to_string());
let payload = format!(
"{{\"type\":\"frida\",\"name\":\"frida-barebone-agent\",\
\"version\":\"{version}\",\"architecture\":\"{arch}\"}}"
);
let source = out_dir.join("version-note.S");
fs::write(
&source,
format!(
".section .note.package,\"\",%note\n\
.balign 4\n\
.long 2f - 1f\n\
.long 4f - 3f\n\
.long 0xcafe1a7e\n\
1: .asciz \"FDO\"\n\
2: .balign 4\n\
3: .asciz {payload:?}\n\
4: .balign 4\n"
),
)
.expect("Couldn't write the version note");
let object = out_dir.join("version-note.o");
let status = Command::new(cc)
.args(cc_args)
.args(["-target", &llvm_target(), "-c", "-o"])
.arg(&object)
.arg(&source)
.status()
.expect("Couldn't run the compiler");
assert!(status.success(), "Couldn't assemble the version note");
object
}
fn llvm_target() -> String {
let target = env::var("TARGET").unwrap();
let spec = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).join(format!("{target}.json"));
let text = fs::read_to_string(&spec).expect("Couldn't read the target specification");
let key = "\"llvm-target\"";
let at = text.find(key).expect("Target specification names no LLVM target");
let rest = &text[at + key.len()..];
let open = rest.find('"').unwrap() + 1;
let close = open + rest[open..].find('"').unwrap();
rest[open..close].to_string()
}
pub fn detect_gcc_include_paths(gcc: &Path, args: &[&str]) -> Vec<PathBuf> {
let out = Command::new(gcc)
.args(args)
.args(["-xc", "-E", "-v", "-"])
.stdin(Stdio::null())
.output()
@@ -84,7 +201,7 @@ pub fn detect_gcc_include_paths(gcc: &Path) -> Vec<PathBuf> {
let p = Path::new(raw);
if p.exists() {
let canonical = fs::canonicalize(p).expect("Failed to canonicalize include path");
let canonical = without_extended_length_prefix(fs::canonicalize(p).expect("Failed to canonicalize include path"));
if !paths.contains(&canonical) {
paths.push(canonical);
}
@@ -94,8 +211,16 @@ pub fn detect_gcc_include_paths(gcc: &Path) -> Vec<PathBuf> {
paths
}
pub fn detect_gcc_library_paths(gcc: &Path) -> Vec<PathBuf> {
let out = Command::new(gcc).arg("-print-search-dirs").output()
fn without_extended_length_prefix(path: PathBuf) -> PathBuf {
let text = path.to_string_lossy();
match text.strip_prefix(r"\\?\") {
Some(rest) => PathBuf::from(rest),
None => path,
}
}
pub fn detect_gcc_library_paths(gcc: &Path, args: &[&str]) -> Vec<PathBuf> {
let out = Command::new(gcc).args(args).arg("-print-search-dirs").output()
.expect("Failed to execute GCC to detect library paths");
let stdout = String::from_utf8_lossy(&out.stdout);
+5 -5
View File
@@ -1,8 +1,8 @@
export FRIDA_BAREBONE_CONFIG=$PWD/etc/xnu.json
export PYTHONPATH=$HOME/src/frida-python
export PATH=$PWD/xpacks/.bin:$PATH
export CC_aarch64_unknown_none=aarch64-none-elf-gcc
export AR_aarch64_unknown_none=aarch64-none-elf-ar
export RANLIB_aarch64_unknown_none=aarch64-none-elf-ranlib
export GUMJS_DEVKIT_DIR=/home/oleavr/src/frida-gum/build/bindings/gumjs/devkit
sdk=$HOME/src/frida-core/deps/sdk-none-arm64-softfloat_pic
export CC="clang --target=aarch64-none-elf -ffixed-x18 -I$sdk/include -I$sdk/include/glib-2.0 -I$sdk/lib/glib-2.0/include \
-I$sdk/include/capstone -I$sdk/include/json-glib-1.0"
export RUSTFLAGS="-L native=$sdk/lib"
export GUMJS_DEVKIT_DIR=$HOME/src/frida-gum/build/none-arm64-softfloat_pic/bindings/gumjs/devkit
@@ -0,0 +1,8 @@
{
"agent": {
"transport": {
"type": "socket",
"path": "/tmp/frida-guest.sock"
}
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"agent": {
"transport": {
"type": "device",
"path": "/dev/frida"
}
}
}
+1
View File
@@ -20,6 +20,7 @@
}
},
"image": {
"type": "xnu-kernelcache",
"file": "/Users/oleavr/src/vphone-cli/vm/iPhone17,3_26.1_23B85_Restore/kernelcache.research.vresearch101",
"symbols": {
"panic": "0x12a2814",
+15
View File
@@ -0,0 +1,15 @@
{
"connection": {
"host": "127.0.0.1",
"port": 1234
},
"kernel": "win9x",
"agent": {
"path": "/home/oleavr/src/frida-core/src/barebone/agent/target-win9x/i686-unknown-none-pic/release/frida-barebone-agent",
"transport": {
"type": "hostlink",
"qmp": "unix:/tmp/claude-1000/-home-oleavr-src-frida-core/0f3e358c-8f66-4246-b793-deb2cd3727f3/scratchpad/qmp.sock",
"bus": "frida-vserial.0"
}
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"connection": {
"host": "127.0.0.1",
"port": 1234
},
"kernel": "winnt",
"agent": {
"path": "/home/oleavr/src/frida-core/src/barebone/agent/target-winnt/i686-unknown-none-pic/release/frida-barebone-agent",
"transport": {
"type": "hostlink",
"qmp": "unix:/tmp/qmp.sock",
"bus": "frida-vserial.0"
}
}
}
+1
View File
@@ -16,6 +16,7 @@
}
},
"image": {
"type": "xnu-kernelcache",
"file": "/Users/oleavr/src/frida-barebone-ios/iPhone11_8_iPhone12_1_14.0_18A5351d_Restore/kernelcache.research.iphone12b",
"base": "0xfffffff007004000",
"symbols": {
+1 -1
View File
@@ -5,6 +5,6 @@ const cm = new CModule(File.readAllBytes("./target/aarch64-unknown-none/release/
const start = new NativeFunction(cm._start, "pointer", []);
const bufferPhysicalAddress = start();
console.log("Ready! Buffer is at physical address:", bufferPhysicalAddress);
$gdb.continue();
$debugger.continue();
Object.assign(globalThis, { cm, start });
@@ -0,0 +1,20 @@
{
"arch": "x86",
"cpu": "pentium4",
"crt-objects-fallback": "false",
"data-layout": "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-i128:128-f64:32:64-f80:32-n8:16:32-S128",
"features": "-mmx,-sse,-sse2,-sse3,-ssse3,-sse4.1,-sse4.2,-avx,-avx2,+soft-float",
"linker": "rust-lld",
"linker-flavor": "gnu-lld",
"llvm-target": "i686-unknown-none-elf",
"max-atomic-width": 64,
"panic-strategy": "abort",
"plt-by-default": false,
"position-independent-executables": true,
"relocation-model": "pic",
"rustc-abi": "softfloat",
"stack-probes": {
"kind": "inline"
},
"target-pointer-width": 32
}
+20
View File
@@ -0,0 +1,20 @@
{
"arch": "x86",
"cpu": "pentium4",
"crt-objects-fallback": "false",
"data-layout": "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-i128:128-f64:32:64-f80:32-n8:16:32-S128",
"features": "-mmx,-sse,-sse2,-sse3,-ssse3,-sse4.1,-sse4.2,-avx,-avx2,+soft-float",
"linker": "rust-lld",
"linker-flavor": "gnu-lld",
"llvm-target": "i686-unknown-none-elf",
"max-atomic-width": 64,
"panic-strategy": "abort",
"plt-by-default": true,
"position-independent-executables": false,
"relocation-model": "static",
"rustc-abi": "softfloat",
"stack-probes": {
"kind": "inline"
},
"target-pointer-width": 32
}
+28
View File
@@ -0,0 +1,28 @@
MODULE_NAME ?= frida-agent
obj-m += $(MODULE_NAME).o
# frida-agent-prelinked.o is not built from source here: it is the Rust agent, the
# GumJS devkit and the C library collapsed into a single relocatable object by the prelink
# step in Makefile. Copy it in through if_changed rather than kbuild's _shipped rule,
# so the .cmd file modpost expects gets written.
$(MODULE_NAME)-y := frida-kmod.o frida-agent-prelinked.o
targets += frida-agent-prelinked.o
quiet_cmd_frida_agent_prelinked = COPY $@
cmd_frida_agent_prelinked = cp $< $@
$(obj)/frida-agent-prelinked.o: $(src)/frida-agent-prelinked.o.bin FORCE
$(call if_changed,frida_agent_prelinked)
# On x86 objtool validates every object going into a module and generates its ORC
# unwind tables. It was not built to read a JavaScript runtime linked against a
# freestanding libc: it gives up on the first Rust switch jump table it cannot
# follow, and the tables would only ever describe frames no kernel stack trace walks
# into. Both the blob and the module object it is linked into have to be excused,
# since objtool runs over each.
OBJECT_FILES_NON_STANDARD_frida-agent-prelinked.o := y
OBJECT_FILES_NON_STANDARD_$(MODULE_NAME).o := y
ccflags-y += $(FRIDA_KMOD_DEFS)
+206
View File
@@ -0,0 +1,206 @@
# Builds frida-agent.ko in two steps, because kbuild insists on owning the final
# link:
#
# 1. Collapse the Rust staticlib plus everything it needs — the GumJS devkit and
# the C library — into one relocatable object, and localize every symbol except
# the entry points frida-kmod.c calls. Localizing matters: Gum and the C library
# define names like memcpy and malloc that must not leak into the kernel's
# namespace.
# 2. Hand that object to kbuild as a shipped object, so vermagic, modinfo and
# CRCs all come from the target kernel's own build system.
#
# See README.md for usage.
# KDIR is the kernel to build against. When it is a source tree that was configured
# elsewhere — which is what Android's modules_prepare tarball gives — point KOUT at
# the prepared tree.
KDIR ?= /lib/modules/$(shell uname -r)/build
PROFILE ?= release
# The kernel's own name for the architecture, and the one Rust knows it by. Both
# follow from the machine this is built for, which defaults to the one building.
ARCH ?= $(shell uname -m \
| sed -e 's/^i[3-6]86$$/x86/' -e 's/^x86_64$$/x86/' -e 's/^aarch64$$/arm64/')
BITS ?= $(shell uname -m \
| sed -e 's/^i[3-6]86$$/32/' -e 's/^x86_64$$/64/' -e 's/^aarch64$$/64/')
ifeq ($(ARCH)-$(BITS),x86-32)
RUST_TARGET ?= i686-unknown-none
AGENT_TARGET_FLAGS ?= --target $(AGENT_DIR)/$(RUST_TARGET).json -Z json-target-spec
SDK_ARCH ?= x86
TOOLPREFIX ?=
AGENT_LD_EMULATION ?= elf_i386
else ifeq ($(ARCH)-$(BITS),x86-64)
RUST_TARGET ?= x86_64-unknown-none
SDK_ARCH ?= x86_64
TOOLPREFIX ?=
AGENT_LD_EMULATION ?= elf_x86_64
else ifeq ($(ARCH)-$(BITS),arm64-64)
RUST_TARGET ?= aarch64-unknown-none
SDK_ARCH ?= arm64
TOOLPREFIX ?= aarch64-none-elf-
else
$(error Unsupported ARCH "$(ARCH)" and BITS "$(BITS)"; this flavour covers \
x86/32, x86/64 and arm64/64)
endif
AGENT_TARGET_FLAGS ?= --target $(RUST_TARGET)
AGENT_DIR := $(CURDIR)/..
AGENT_TARGET_DIR := $(AGENT_DIR)/target-linux
AGENT_LIB := $(AGENT_TARGET_DIR)/$(RUST_TARGET)/$(PROFILE)/libfrida_barebone_agent.a
GUMJS_DEVKIT_DIR ?= $(error Set GUMJS_DEVKIT_DIR to the directory holding libfrida-gumjs.a)
# Any binutils that speak the target's ELF will do; nothing here is compiled with
# them. Building for the host's own architecture, that is the host's binutils.
AGENT_LD ?= $(TOOLPREFIX)ld
AGENT_AR ?= $(TOOLPREFIX)ar
AGENT_NM ?= $(TOOLPREFIX)nm
AGENT_OBJCOPY ?= $(TOOLPREFIX)objcopy
# The ABI the kernel imposes on anything linked into it. The flags only reach crates
# Cargo compiles, and the toolchain ships core, alloc and compiler_builtins prebuilt
# without them, so those three have to be rebuilt from source as well. That needs the
# rust-src component and an unstable flag, hence RUSTC_BOOTSTRAP.
#
# On x86_64 that is endbr64 on every address-taken function, since a kernel with
# CONFIG_X86_KERNEL_IBT faults an indirect call landing anywhere else, and switches
# compiled as comparisons rather than jump tables, which objtool cannot follow into.
# Soft-float, the kernel code model and the absent red zone come with the target
# itself. On arm64
# it is x18, which the kernel reserves for the shadow call stack, so nothing linked in
# may use it as a general-purpose register.
ifeq ($(ARCH)-$(BITS),x86-32)
AGENT_RUSTFLAGS ?=
AGENT_CFLAGS ?= --target=i386-none-elf \
-Xclang -target-feature -Xclang +soft-float -mno-mmx -mno-sse \
-mlong-double-64 -fno-pic --sysroot=$(FRIDA_SDK)
else ifeq ($(ARCH)-$(BITS),x86-64)
AGENT_RUSTFLAGS ?= -Zcf-protection=branch \
-Cllvm-args=--min-jump-table-entries=65536 \
-Crelocation-model=static -Zplt=yes
AGENT_CFLAGS ?= --target=x86_64-none-elf \
-Xclang -target-feature -Xclang +soft-float -mno-mmx -mno-sse -mno-red-zone \
-mlong-double-64 -mcmodel=kernel -fcf-protection=branch -fno-jump-tables \
-fno-pic --sysroot=$(FRIDA_SDK)
else
AGENT_RUSTFLAGS ?= -Zfixed-x18
AGENT_CFLAGS ?= --target=aarch64-none-elf -mabi=aapcs-soft -mgeneral-regs-only \
-ffixed-x18 -fno-pic --sysroot=$(FRIDA_SDK)
endif
AGENT_BUILD_STD ?= -Z build-std=core,alloc,compiler_builtins
# The devkit header reaches for GLib's and capstone's, which a devkit built against
# an SDK leaves in that SDK rather than beside itself. The sysroot's own include
# directory is spelled out because only clang's bare-metal driver, which it selects
# for arm64 but not for x86_64, searches there on its own.
AGENT_CFLAGS += -isystem $(FRIDA_SDK)/include \
-I$(FRIDA_SDK)/include/glib-2.0 \
-I$(FRIDA_SDK)/lib/glib-2.0/include \
-I$(FRIDA_SDK)/include/capstone \
-I$(FRIDA_SDK)/include/json-glib-1.0
# The soft-float SDK, whose libc is picolibc and whose compiler runtime is
# compiler-rt. GCC's own newlib and libgcc are hard-float and cannot be used here.
FRIDA_SDK ?= $(error Set FRIDA_SDK to a none-$(SDK_ARCH)-softfloat_nopic SDK prefix)
LIBM = $(FRIDA_SDK)/lib/libm.a
LIBBUILTINS = $(FRIDA_SDK)/lib/libclang_rt.builtins.a
# bindgen reads the devkit header through whatever this compiler reports as its
# include path, so it has to be one that knows where the SDK keeps its libc. It has
# to be clang either way: -mabi=aapcs-soft landed there in 19 and GCC has it at no
# version, and on x86_64 the soft-float ABI is reachable only as an -Xclang feature.
AGENT_CC ?= clang
PRELINKED := frida-agent-prelinked.o.bin
AGENT_LIB_SOFTFLOAT := $(AGENT_TARGET_DIR)/libfrida-barebone-agent-softfloat.a
# On arm64 the toolchain's own compiler_builtins is compiled for the ordinary AAPCS,
# where a double travels in d0: its __muldf3 opens with "fmov x14, d0". Everything
# else there is -mabi=aapcs-soft and passes that same double in x0. Whole-archiving
# the staticlib puts those definitions in front of the SDK's compiler-rt, which has
# the soft-float ones, and every double the C library computes then comes back
# garbage — quietly, since the arithmetic still runs. Drop them and let compiler-rt
# answer instead. x86_64 needs none of this: soft-float is part of the Rust target
# there, so the copy build-std compiles already passes doubles the way the C half
# expects.
ifeq ($(ARCH),arm64)
DROP_HARDFLOAT_BUILTINS = \
$(AGENT_AR) d $@ $$($(AGENT_NM) --print-armap $@ | sed -n 's/^__muldf3 in //p') && \
$(AGENT_AR) s $@
else
DROP_HARDFLOAT_BUILTINS = :
endif
# The agent puts malloc, the C library's locks and its process stubs on the kernel's
# own facilities, so picolibc's versions are duplicate definitions rather than
# fallbacks. Work out which members they live in instead of naming them: the set
# follows from what the agent defines, and picolibc moves things between releases.
LIBC := $(AGENT_TARGET_DIR)/libc-trimmed.a
ENTRY_POINTS := frida_agent_start frida_agent_stop \
gum_interceptor_obtain gum_interceptor_begin_transaction \
gum_interceptor_end_transaction gum_interceptor_replace \
gum_interceptor_revert
KEEP_GLOBAL := $(foreach s,$(ENTRY_POINTS),--keep-global-symbol=$(s))
# Two structures the reparenting path reaches into lost a member upstream, and
# neither change announces itself as anything frida-kmod.c could test for. The
# headers it is compiled against are the authority on both, so ask them.
FRIDA_KMOD_DEFS := \
$(if $(shell grep -l 'list_head[[:space:]]*thread_group;' \
$(KDIR)/include/linux/sched.h),-DFRIDA_HAVE_TASK_THREAD_GROUP) \
$(if $(shell grep -l 'spinlock_t[[:space:]]*lock;' \
$(KDIR)/include/linux/fs_struct.h),-DFRIDA_HAVE_FS_STRUCT_LOCK)
all: modules
# A module that dies in its init path is left wedged under its own name until the
# next boot, so bring-up wants a fresh one per attempt.
MODULE_NAME ?= frida-agent
modules: $(PRELINKED)
$(MAKE) -C $(KDIR) $(if $(KOUT),O=$(KOUT)) M=$(CURDIR) ARCH=$(ARCH) \
FRIDA_KMOD_DEFS="$(FRIDA_KMOD_DEFS)" MODULE_NAME=$(MODULE_NAME) modules
$(PRELINKED): $(AGENT_LIB_SOFTFLOAT) $(LIBC) prelink.lds
$(AGENT_LD) -r $(if $(AGENT_LD_EMULATION),-m $(AGENT_LD_EMULATION)) \
-T prelink.lds -o $@.tmp \
--whole-archive $(AGENT_LIB_SOFTFLOAT) --no-whole-archive \
--start-group \
$(GUMJS_DEVKIT_DIR)/libfrida-gumjs.a \
$(LIBC) \
$(LIBM) \
$(LIBBUILTINS) \
--end-group
$(AGENT_OBJCOPY) $(KEEP_GLOBAL) --strip-debug \
--remove-section=.llvmbc --remove-section=.llvmcmd $@.tmp $@
rm -f $@.tmp
$(AGENT_LIB_SOFTFLOAT): agent
cp $(AGENT_LIB) $@
$(DROP_HARDFLOAT_BUILTINS)
$(LIBC): export LC_ALL = C
$(LIBC): $(AGENT_LIB_SOFTFLOAT)
cp $(FRIDA_SDK)/lib/libc.a $@
$(AGENT_NM) --defined-only $(AGENT_LIB_SOFTFLOAT) \
| awk 'NF == 3 && $$2 ~ /^[TDBR]$$/ { print $$3 }' | sort -u > $@.agent-syms
$(AGENT_NM) --print-armap $@ | sed -n 's/^\([^ ]*\) in \(.*\)$$/\1 \2/p' | sort -u > $@.members
$(AGENT_AR) d $@ $$(join $@.agent-syms $@.members | awk '{ print $$2 }' | sort -u)
$(AGENT_AR) s $@
rm -f $@.agent-syms $@.members
agent:
cd $(AGENT_DIR) && RUSTC_BOOTSTRAP=1 RUSTFLAGS="$(AGENT_RUSTFLAGS)" \
CC_$(subst -,_,$(RUST_TARGET))="$(AGENT_CC) $(AGENT_CFLAGS)" \
cargo build --$(PROFILE) \
$(AGENT_TARGET_FLAGS) --features linux \
--target-dir $(AGENT_TARGET_DIR) $(AGENT_BUILD_STD)
clean:
$(MAKE) -C $(KDIR) $(if $(KOUT),O=$(KOUT)) M=$(CURDIR) ARCH=$(ARCH) clean
rm -f $(PRELINKED) $(PRELINKED).tmp $(AGENT_LIB_SOFTFLOAT) $(LIBC)
.PHONY: all modules agent clean
+291
View File
@@ -0,0 +1,291 @@
# Linux kernel module
The same agent that the Barebone backend injects into XNU from the outside, but
loaded from the inside as an ordinary kernel module.
## How it differs from the XNU flavor
| | XNU | Linux |
|-------------------|--------------------------------------------------|-------------------------------------------|
| Delivery | remote stub injects a freestanding ELF | `insmod frida-agent.ko` |
| Kernel primitives | addresses patched into `.kernel_addrs` by the host | direct calls into `frida-kmod.c` |
| Bootstrap config | GVariant blob written into guest memory | module parameters |
| Transport | virtio hostlink or vsock | `/dev/frida` character device |
| Page permissions | host rewrites descriptors over the hostlink | `set_memory_*()` |
| Writable alias | shadow copy, committed via physical-memory bridge | `vmap()` of the same pages |
| Symbols | table supplied by the host | kallsyms |
Because the Linux agent performs its own memory work, it never issues the
`RemapWritablePages`, `MemoryProtect` or `PatchCode` host RPCs. The wire protocol
is otherwise identical, so the host side sees the same `(yqv)` frames.
## Building
Four inputs: this repository, a GumJS devkit built for
`none-<arch>-softfloat_nopic`, the SDK it was built against, and the kernel the module is for. `Makefile`
covers x86_64 and arm64, and defaults to the architecture of the machine building.
Building for the running kernel is a good deal shorter than cross-building for a
device, so that walkthrough comes first; the arm64 one below it is the same steps
with the kernel obtained from elsewhere.
### Toolchain
rustup target add x86_64-unknown-none # or aarch64-unknown-none
rustup component add rust-src
sudo dnf install clang binutils kernel-devel-$(uname -r)
clang must be 19 or newer, since that is where `-mabi=aapcs-soft` landed; GCC does
not implement it at any version, nor does it reach x86_64's soft-float ABI. The
binutils are for the prelink, which reads the archive map that only GNU `nm` prints —
the LLVM one does not; cross-building wants the target's, natively the host's own
will do. The host compiler is for Cargo's own build scripts, which run on the machine
doing the building.
The commands below are run from the top of this repository, with `releng` checked
out — `git submodule update --init releng` if the clone did not recurse.
### For the running x86_64 kernel
No prebuilt bundles are published for `none-x86_64-softfloat_nopic`, so both inputs are
built locally, against the same SDK:
releng/deps.py build --bundle=sdk --host=none-x86_64-softfloat_nopic
mkdir -p ~/sdk-none-x86_64-softfloat_nopic
tar -C ~/sdk-none-x86_64-softfloat_nopic -xf deps/sdk-none-x86_64-softfloat_nopic.tar.xz
gum=subprojects/frida-gum
mkdir -p $gum/build/none-x86_64-softfloat_nopic
(cd $gum/build/none-x86_64-softfloat_nopic \
&& FRIDA_DEPS=$PWD/../../../../deps ../../configure \
--host=none-x86_64-softfloat_nopic \
--enable-gumjs \
--with-devkits=gumjs \
--with-devkit-symbol-scope=original \
&& make)
make -C src/barebone/agent/linux \
FRIDA_SDK=$HOME/sdk-none-x86_64-softfloat_nopic \
GUMJS_DEVKIT_DIR=$PWD/$gum/build/none-x86_64-softfloat_nopic/bindings/gumjs/devkit
Everything else — `KDIR`, the Rust target, the binutils, the kernel's own name for the
architecture — follows from the running kernel.
### For an arm64 device
Here there are published bundles to start from, and the kernel has to be assembled.
#### The devkit and the SDK
version=17.17.0
base=https://github.com/frida/frida/releases/download/$version
curl -LO $base/frida-gumjs-devkit-$version-none-arm64-softfloat_nopic.tar.xz
mkdir -p ~/gumjs-devkit
tar -C ~/gumjs-devkit -xf frida-gumjs-devkit-$version-none-arm64-softfloat_nopic.tar.xz
releng/deps.py sync sdk none-arm64-softfloat_nopic ~/sdk-none-arm64-softfloat_nopic
The SDK carries picolibc and the compiler-rt builtins the devkit expects to be linked
against, and `releng/deps.toml` pins which one. Mixing a devkit with an SDK built for
a different libc leaves undefined symbols at prelink time.
#### The kernel
Nothing here needs the device's own kernel to be built, but the config alone is not
enough either. `uname -r` ends in the GKI commit and the build number:
adb shell uname -r
6.1.145-android14-11-gc1de4747ac59-ab14219743
which is commit `c1de4747ac59` and build `14219743`. Three pieces are keyed to them:
commit=c1de4747ac59
build=14219743
ci=https://ci.android.com/builds/submitted/$build/kernel_aarch64/latest/raw
mkdir -p ~/kernel-prepared ~/kernel-source
curl -sSL $ci/modules_prepare_outdir.tar.gz | tar -xz -C ~/kernel-prepared
curl -sSL $ci/kernel_aarch64_Module.symvers -o ~/kernel-prepared/Module.symvers
curl -sSL https://android.googlesource.com/kernel/common/+archive/$commit.tar.gz \
| tar -xz -C ~/kernel-source
`modules_prepare_outdir.tar.gz` is the configured build tree with `scripts/` already
compiled as x86-64 binaries, so the kbuild half has to run on an x86-64 Linux host —
an arm64 one gets as far as `CC [M]` and then fails to run `fixdep`. `Module.symvers`
carries the CRCs `CONFIG_MODVERSIONS` checks. The top-level `Makefile` in that prepared tree is a stub
that includes the source's, hence both `KDIR` and `KOUT` below.
A different GKI build of the same branch does not substitute for these: vermagic is
compared verbatim, and it names the exact build.
#### Build
make -C src/barebone/agent/linux \
FRIDA_SDK=$HOME/sdk-none-arm64-softfloat_nopic \
GUMJS_DEVKIT_DIR=$HOME/gumjs-devkit \
AGENT_LD=aarch64-linux-gnu-ld \
AGENT_AR=aarch64-linux-gnu-ar \
AGENT_NM=aarch64-linux-gnu-nm \
AGENT_OBJCOPY=aarch64-linux-gnu-objcopy \
KDIR=$HOME/kernel-source \
KOUT=$HOME/kernel-prepared \
LLVM=1
`make` builds the Rust staticlib itself, prelinks it against the devkit and the SDK,
then hands the result to kbuild. The `AGENT_*` overrides are only needed because the
defaults name an `aarch64-none-elf-` GNU toolchain; drop them if that is what is
installed.
The prelink half runs anywhere; the kbuild half needs the x86-64 Linux host above.
Cross-building from macOS means running that step in a container holding the kernel
tree.
adb push src/barebone/agent/linux/frida-agent.ko /data/local/tmp/
## Loading
insmod frida-agent.ko
The module's init path returns as soon as the worker thread is running — watch
`dmesg` for `frida: listening on /dev/frida`. `rmmod frida-agent` unwinds in the
reverse order and does not return until the worker is off the module's text.
## Talking to it
The agent exposes `/dev/frida` and waits: one client at a time, `read`/`write` of the
same length-prefixed frames the other transports carry, with `poll` for readiness.
It never dials out, so there is no network namespace to get right and no ordering
requirement between loading the module and attaching a client.
On Android the node is created by devtmpfs without a policy-specific label, so an
SELinux-enforcing system needs the client's domain granted access to it before it can
open the device. A socket-based transport was the other option considered and is a
worse fit: `AF_VSOCK` only reaches a peer when a hypervisor is on the other side, and
having the kernel connect out to loopback needs something already listening before
the module loads.
### Reaching it from a host
Serve the Barebone device from a frida-server running on the target, so the `open()`
happens next to the device node and the host talks to an ordinary frida-server:
FRIDA_BAREBONE_CONFIG=/data/local/tmp/linux-kmod.json \
frida-server --device barebone -l 127.0.0.1:27042
`etc/linux-kmod.json` is that config. From the host, with `adb forward tcp:27042
tcp:27042` in place:
frida -H 127.0.0.1:27042 -p 0
## Injecting into userspace
Loaded on a live system the module doubles as a ptrace-free injector for ordinary
processes — how the Linux local backend reaches its targets when the module is
present, falling back to `ptrace` when it is not. There is no second device node;
the channel is `prctl()` with a magic option and a token in the fifth argument,
prctl(0x46524944, op, arg1, arg2, 0x1d5f9e6b2c7a4038)
and any call whose option or token does not match falls through to the real
prctl, so probing for the module looks like a stock kernel. `op` 32 pings and
returns the magic.
Allocate, free, read, write and spawn — the primitives the injector drives
against a target pid — require `CAP_SYS_ADMIN`. A spawned thread is a real
`CLONE_THREAD` sibling that adopts the target's mm, files, credentials,
namespaces, fs and SysV semaphore undo list, so it is a native member of the
group rather than a kernel thread wearing its address space.
Cloaking is Gum's own registry mirrored into the kernel so it holds against
`/proc` too: the module intercepts the `maps`, `smaps`, task and status readers
and drops cloaked threads, ranges and fds while keeping the counts consistent. A
process cloaks its own resources with the token alone — no capability — so an
injected agent self-cloaks by forwarding Gum's updates over the same channel,
and the loader adds the footprint the agent itself never sees. A cloaked thread
still sees through the cloak when it reads `/proc`, so the agent is not blind to
its own process.
## What the target kernel has to provide
- **x86_64 or arm64.** The register-level pieces — the page size, cache maintenance,
where the syscall ABI leaves prctl()'s arguments, and how a spawned thread's
registers are set up for its return to userspace — have a half for each. The XNU
flavor remains arm64-only.
- **`CONFIG_KPROBES` and `CONFIG_KALLSYMS_ALL`.** A good deal of what the shim
needs is compiled in but not exported — `kallsyms_lookup_name`,
`kallsyms_on_each_symbol` and `set_memory_*` all are, and GKI trims its export
table to the KMI symbol list on top of that. The shim resolves them by name
through a kprobe, which is the standard way back in. Without kallsyms the module
still loads but cannot look up symbols or change page permissions.
- **Unsigned modules.** `CONFIG_MODULE_SIG_FORCE` must be off. Android's
`CONFIG_MODULE_SIG_PROTECT` is fine: it only demands signatures for modules on
the GKI list.
## Kernel ABI constraints
A hardened kernel imposes an ABI on anything linked into it, and the FP constraint
below is why this flavor is built against a soft-float SDK
(`none-arm64-softfloat_nopic`, `none-x86_64-softfloat_nopic`) rather than an ordinary
bare-metal one. The remaining constraints are per-architecture.
- **FP/SIMD.** JavaScript numbers are doubles, so a hardfloat build would use FP
for as long as the runtime lives, and neither architecture lets a kernel thread
hold it for free — x86 wants every stretch bracketed by `kernel_fpu_begin()`, and
on arm64 kernels before 6.9 there is no correct way to hold it at all:
`kernel_neon_begin()` runs with preemption disabled so we cannot sleep under it,
and the scheduler does not save a kernel thread's FP registers — live values are
lost on preemption and the FP state of whichever user task was interrupted is
corrupted. Building everything soft-float removes the question rather than
answering it: `-mabi=aapcs-soft -mgeneral-regs-only` on arm64, and on x86_64
LLVM's `+soft-float`, which is also what Rust's own `x86_64-unknown-none` turns
on, so the two halves agree on where a double travels. `-mno-sse` alone does not
reach the ABI; and `long double` has to be narrowed to 64 bits with
`-mlong-double-64`, since soft-float has no lowering for x87's 80-bit format and
LLVM crashes rather than calling out to a builtin.
- **Indirect branch tracking (x86_64).** With `CONFIG_X86_KERNEL_IBT` an indirect
call landing on anything but an `endbr64` faults, so everything linked in is built
`-fcf-protection=branch` and the Rust half `-Zcf-protection=branch`.
- **Kernel code model and red zone (x86_64).** Modules are loaded into the top 2GB
of the address space and their relocations are resolved as if by `-mcmodel=kernel`,
and there is no red zone below the stack pointer for an interrupt to step on. Both
come with the Rust target; the C half is told explicitly.
- **objtool (x86_64).** It validates every object entering a module and builds its
ORC unwind tables, and was not written to read a JavaScript runtime linked against
a freestanding libc. `Kbuild` marks the prelinked blob `OBJECT_FILES_NON_STANDARD`.
- **Shadow call stack (arm64).** With `CONFIG_SHADOW_CALL_STACK` the kernel reserves x18, so
the SDK and Gum are built `-ffixed-x18` and the Rust half with `-Zfixed-x18`.
Anything linked in that uses x18 as a scratch register overwrites the kernel's SCS
pointer, and the thread then returns through whatever that address happens to hold.
The Rust flag only reaches crates Cargo compiles, so `core`, `alloc` and
`compiler_builtins` — which the toolchain ships prebuilt, and which between them
account for every x18 write that used to reach the module — are rebuilt from source
via `-Z build-std`. That needs `rustup component add rust-src`.
## The soft-float libc
The libc is picolibc and the compiler runtime is compiler-rt, both from the
soft-float SDK that `FRIDA_SDK` points at. `Makefile` trims a copy of its
`libc.a` before the prelink: the agent implements `malloc`, the C library's locks and
its process stubs over the kernel's own facilities, so picolibc's are duplicate
definitions rather than fallbacks. The set is derived from what the agent defines
rather than listed, since picolibc moves things between releases.
On arm64 those compiler-rt builtins are load-bearing in a way that is easy to miss.
Rust's toolchain carries its own `compiler_builtins`, compiled for the ordinary AAPCS
where a double travels in `d0`, and the prelink whole-archives the Rust staticlib — so
without the filtering step in `Makefile` those definitions answer the C library's calls
to `__muldf3` and friends, which pass the same double in `x0`. Nothing crashes. The
arithmetic simply returns garbage, and the first thing to notice is GLib deciding a
hash table need not grow. x86_64 needs none of that filtering: soft-float is part of
the Rust target there, so the copy `-Z build-std` compiles already agrees with the C
half.
## What a script may look like
The runtime compiles scripts on a kernel thread's stack, and QuickJS parses
expressions by recursion, so the ceiling is nesting rather than length: roughly
fourteen levels of parentheses or fifteen of object literals on arm64 Linux, whose
kernel stack is 16 KiB — as is x86_64's. A script that exceeds it fails to compile rather than taking
anything down, though the message is not always `stack overflow` — an over-deep
`if`/`else` chain reports `expecting ';'`. Long scripts are fine; deep ones are not.
File diff suppressed because it is too large. Load diff
+8
View File
@@ -0,0 +1,8 @@
SECTIONS
{
.init_array 0 : {
_agent_init_start = .;
*(.init_array*)
_agent_init_end = .;
}
}
+4
View File
@@ -0,0 +1,4 @@
/frida-agent
/frida-agent.kext/
/frida-kext.o
/frida-signing.o
+37
View File
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>frida-agent</string>
<key>CFBundleIdentifier</key>
<string>re.frida.agent</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>frida-agent</string>
<key>CFBundlePackageType</key>
<string>KEXT</string>
<key>CFBundleShortVersionString</key>
<string>@FRIDA_VERSION@</string>
<key>CFBundleVersion</key>
<string>@BUNDLE_VERSION@</string>
<key>OSBundleRequired</key>
<string>Root</string>
<key>OSBundleLibraries</key>
<dict>
<key>com.apple.kpi.bsd</key>
<string>12.0</string>
<key>com.apple.kpi.libkern</key>
<string>12.0</string>
<key>com.apple.kpi.iokit</key>
<string>12.0</string>
<key>com.apple.kpi.mach</key>
<string>12.0</string>
<key>com.apple.kpi.unsupported</key>
<string>12.0</string>
</dict>
</dict>
</plist>
+88
View File
@@ -0,0 +1,88 @@
PROFILE ?= release
ARCH ?= arm64e
ifeq ($(ARCH),arm64e)
RUST_TARGET ?= arm64e-apple-darwin
CLANG_TARGET ?= arm64e-apple-macos11
else ifeq ($(ARCH),arm64)
RUST_TARGET ?= aarch64-apple-darwin
CLANG_TARGET ?= arm64-apple-macos11
else
RUST_TARGET ?= x86_64-apple-darwin
CLANG_TARGET ?= x86_64-apple-macos11
endif
AGENT_DIR := $(CURDIR)/..
AGENT_TARGET_DIR := $(AGENT_DIR)/target-xnu-kext
AGENT_LIB := $(AGENT_TARGET_DIR)/$(RUST_TARGET)/$(PROFILE)/libfrida_barebone_agent.a
SDK ?= $(shell xcrun --sdk macosx --show-sdk-path)
KERNEL_HEADERS := $(SDK)/System/Library/Frameworks/Kernel.framework/Headers
FRIDA_VERSION := $(or $(FRIDA_VERSION),0.0.0)
BUNDLE_VERSION := $(firstword $(subst -, ,$(FRIDA_VERSION)))
FRIDA_SDK ?= $(error Set FRIDA_SDK to a macos-$(ARCH)-kernel SDK prefix)
GUMJS_DEVKIT_DIR ?= $(error Set GUMJS_DEVKIT_DIR to the directory holding libfrida-gumjs.a)
AGENT_RUSTFLAGS ?= -Cembed-bitcode=no
AGENT_BUILD_STD ?= -Z build-std=core,alloc,compiler_builtins
GLUE_CFLAGS ?= -target $(CLANG_TARGET) -isysroot $(SDK) -I$(KERNEL_HEADERS) \
-mkernel -fno-builtin -fno-common -nostdinc -std=gnu11 \
-DKERNEL -DKERNEL_PRIVATE -DAPPLE -D__MACHO__=1
AGENT_CC ?= clang -target $(CLANG_TARGET) -mkernel -nostdlibinc \
-isystem $(FRIDA_SDK)/include \
-I$(FRIDA_SDK)/include/glib-2.0 \
-I$(FRIDA_SDK)/lib/glib-2.0/include \
-I$(FRIDA_SDK)/include/capstone \
-I$(FRIDA_SDK)/include/json-glib-1.0
stamp_version = sed -e 's/@FRIDA_VERSION@/$(FRIDA_VERSION)/' \
-e 's/@BUNDLE_VERSION@/$(BUNDLE_VERSION)/' Info.plist
BUNDLE := frida-agent.kext
BINARY := $(BUNDLE)/Contents/MacOS/frida-agent
all: $(BUNDLE)
skeleton: frida-kext.o Info.plist
mkdir -p $(BUNDLE)/Contents/MacOS
$(stamp_version) > $(BUNDLE)/Contents/Info.plist
$(shell xcrun --find ld) -kext -arch $(ARCH) -o $(BINARY) frida-kext.o
$(BUNDLE): frida-agent Info.plist
mkdir -p $(BUNDLE)/Contents/MacOS
$(stamp_version) > $(BUNDLE)/Contents/Info.plist
cp frida-agent $(BINARY)
frida-agent: frida-kext.o frida-signing.o agent
$(shell xcrun --find ld) -kext -arch $(ARCH) -o $@ \
frida-kext.o \
frida-signing.o \
$(AGENT_LIB) \
$(GUMJS_DEVKIT_DIR)/libfrida-gumjs.a \
$(FRIDA_SDK)/lib/libc.a \
$(FRIDA_SDK)/lib/libm.a \
$(FRIDA_SDK)/lib/libclang_rt.builtins.a
frida-kext.o: frida-kext.c
$(shell xcrun --find clang) $(GLUE_CFLAGS) -c $< -o $@
frida-signing.o: frida-signing.c
$(AGENT_CC) -I$(GUMJS_DEVKIT_DIR) -c $< -o $@
agent:
cd $(AGENT_DIR) && RUSTC_BOOTSTRAP=1 RUSTFLAGS="$(AGENT_RUSTFLAGS)" \
CC_$(subst -,_,$(RUST_TARGET))="$(AGENT_CC)" \
GUMJS_DEVKIT_DIR="$(GUMJS_DEVKIT_DIR)" \
cargo build --$(PROFILE) \
--target $(RUST_TARGET) --features xnu-kext \
--target-dir $(AGENT_TARGET_DIR) $(AGENT_BUILD_STD)
clean:
rm -rf $(BUNDLE) frida-agent frida-kext.o frida-signing.o
.PHONY: all agent clean skeleton
+122
View File
@@ -0,0 +1,122 @@
# The agent as a kext
The XNU flavour of the barebone agent is ordinarily a blob: a host with a debugger writes it into
a running kernel, tells it where that kernel keeps the things it calls, and starts it. The Linux
flavour can also be built as a module the kernel loads (`../linux`), and this is the same thing for
XNU -- a kext macOS loads.
What the two packagings share is everything the agent does. What differs is how it is put there,
how it is told about the kernel around it, and how the host reaches it.
## Building
Three things, in order. First an SDK for this kernel -- picolibc, GLib, QuickJS, capstone and the
rest, built for `macos-arm64-kernel`:
cd frida-core
python3 -m releng.deps build --host macos-arm64-kernel
That fetches the same sources every other flavour is built from, six of them pinned to a commit
that says what a Darwin kernel is not; see below. Then a GumJS devkit against it:
cd frida-gum
./configure --host=macos-arm64-kernel --enable-gumjs --disable-v8 --with-devkits=gumjs
make
Then the kext itself:
make FRIDA_SDK=.../deps/sdk-macos-arm64-kernel \
GUMJS_DEVKIT_DIR=.../frida-gum/build/bindings/gumjs/devkit
which builds the agent as a static library for the kernel's ABI, `frida-kext.c` against the Kernel
framework's headers, and links the two with the devkit into `frida-agent.kext`.
`make skeleton` builds the glue alone, which is enough to check the pipeline without the rest.
## How it is told about the kernel
A kext may call what the kernel exports to kexts, and the agent calls a great deal more than that:
`proc_find` and `copyin` are exported, `task_threads` and `mach_vm_allocate` are not. So the
addresses arrive the way they do for the blob -- worked out by whoever knows this kernel -- and are
written in through `/dev/frida` before the agent is started:
ioctl(fd, FRIDA_IOC_SET_ADDR, &(FridaAddrRequest){ .name = "task_threads", .address = ... });
...
ioctl(fd, FRIDA_IOC_START);
A name nothing writes stays zero, and the agent already answers for that: every one of them is an
`Option` on the Rust side, and what needs it says so rather than calling through a null.
Once started, the same device carries the frames: read what the agent says, write what the host
says, framed with a four-byte little-endian length exactly as on every other transport.
## What the sources needed
An SDK for a Darwin kernel is a target nothing here had been built for, and six of the sources say
so. Each is a commit of its own on the fork, a few lines apiece:
- **picolibc** -- Mach-O has no `__attribute__((alias))`, so the aliasing macros fall through to
the assembler ones it already carries for Mach-O, and the names are spent before being spelled.
Its aarch64 assembly is written for ELF and its C stubs answer for it instead. The interrupt
vectors and the `exec` family are not built: one owns a machine this does not, the other crashes
the compiler.
- **compiler-rt** -- `apple_versioning.c` is stubs for macOS releases long past, written against a
header only userspace has.
- **libffi** -- the trampoline table is built out of what userspace has of Mach; whether that is
there is now asked of the header rather than of the system's name.
- **QuickJS**, **tinycc** and **libiconv** -- all three ask what to do on Apple by asking whether
they are on Apple. Where that means userspace -- a userspace malloc's header, a cache-control
header, a locale one -- they now ask whether they are freestanding as well.
## Which architecture, and where pointers are signed
Apple silicon runs an arm64e kernel and loads nothing else, so that is what `ARCH` defaults to,
and the SDK and the devkit are built for `macos-arm64e-kernel` to match.
Pointer authentication is the thing to get right there, and what makes it awkward is that Rust's
`arm64e-apple-darwin` is a tag rather than an ABI: it neither signs a function pointer nor
authenticates one.
_pick: adrp x0, 0 ; add x0, x0, #0x0 ; ret <- no signing
_call: mov x2, x0 ; mov x0, x1 ; br x2 <- no authenticating
So the line is drawn where the kernel is, and not inside:
- **Ourselves.** GLib, GumJS and the agent are built `-fno-ptrauth-calls`, so the C half calls a
Rust callback the way Rust hands it over -- plainly. Nothing there signs, so nothing there is
disappointed. `otool` says as much: not one `blraa` outside `__auth_stubs`.
- **The kernel calling us.** The glue keeps the kernel's own ABI, so the device table it registers
is written with `AUTH` relocations and the loader signs it as the kernel expects.
- **Us calling the kernel.** Through `__auth_stubs`, which the loader fills the same way.
- **Us handing the kernel a function to call.** A thread's entry is the one that matters, and the
agent signs it itself, as it already does for the kernel it is injected into. What it signs with
is no longer written down twice: the glue derives the discriminator from the kernel's own
headers with `ptrauth_type_discriminator`, and asserts that it is the number the blob flavour
carries -- 0xd507 for a `thread_continue_t`, which is what it was.
What this does not buy is pointer authentication *within* the kext, which is protection given up
rather than correctness lost. Getting it back wants rustc to grow the ABI, or every callback
wrapped in a C thunk.
None of this touches x86_64, which has no pointer authentication at all: an Intel kext wants only
its own SDK built the same way.
## Floating point
Every other kernel flavour is built soft-float, since a kernel does not save the floating-point
registers across a trap. This one cannot be: Darwin has no soft-float C ABI on either
architecture. arm64 refuses the type outright without one -- `ABI 'darwinpcs' does not support
it` -- and `-mabi=aapcs-soft` alongside it crashes both Apple's clang and upstream LLVM on the
first varargs function taking a double; x86_64 wants SSE to return one. A script engine is nothing
but doubles, so the floating-point registers stay in play, and what that costs at run time is a
thing to answer on the machine.
## Putting copies into processes
Not there yet. The blob's linker script marks out its own writable half and what a copy of it has
to relocate; a kext is laid out by the kernel's loader instead, so the glue says there is nothing
to copy from. Everything the agent does in the kernel itself is unaffected.
Carrying the blob's ELF and placing it the way the host does is not a way round that, either: on a
machine with SPTM, memory a kext allocates cannot be made executable, while a kext's own text is
mapped executable by the kernel's loader because it was signed.
+446
View File
@@ -0,0 +1,446 @@
#include <libkern/libkern.h>
#include <mach/kmod.h>
#include <mach/mach_types.h>
#include <miscfs/devfs/devfs.h>
#include <sys/conf.h>
#include <sys/errno.h>
#include <sys/ioccom.h>
#include <sys/malloc.h>
#include <sys/proc.h>
#include <sys/random.h>
#include <sys/uio.h>
#include <kern/locks.h>
#include <kern/thread.h>
#include <ptrauth.h>
extern int frida_agent_start (uint64_t own_base, uint64_t own_size);
extern void frida_agent_stop (void);
extern void frida_agent_wake (void);
#define FRIDA_KERNEL_ADDRS(X) \
X (panic) \
X (kalloc) X (kfree) X (kalloc_data) X (kfree_data) \
X (proc_iterate) X (proc_find) X (proc_rele) X (proc_pid) X (proc_best_name) X (proc_task) \
X (current_task) X (current_thread) X (get_task_map) X (get_bsdtask_info) \
X (mach_vm_allocate) X (mach_vm_deallocate) X (mach_vm_protect) X (mach_vm_remap) \
X (kernel_map) X (kernel_pmap) X (pmap_find_phys) X (ml_static_ptovirt) \
X (mach_vm_region) X (mach_vm_region_recurse) \
X (vm_map_copyin) X (vm_map_copyout) X (vm_map_wire_external) \
X (thread_create) X (thread_set_state) X (thread_resume) X (thread_suspend) \
X (thread_terminate) X (kernel_thread_start) \
X (task_suspend) X (task_resume) X (task_clear_return_wait) X (task_threads) X (task_info) \
X (task_restartable_ranges_register) \
X (assert_wait) X (assert_wait_timeout) X (thread_block) X (thread_wakeup) \
X (thread_wakeup_prim) X (wakeup) X (ulock_wake) \
X (mach_absolute_time) X (absolutetime_to_nanoseconds) X (clock_get_calendar_microtime) \
X (ml_io_map) X (ml_vtophys) X (ml_static_vtop) \
X (copyin) X (copyout) X (sysent) X (IOLog) \
X (convert_port_to_thread) X (convert_port_to_thread_read) \
X (convert_port_to_thread_inspect) \
X (sock_socket) X (sock_connect) X (sock_send) X (sock_receive) X (sock_close)
#define FRIDA_DEFINE_SLOT(name) void * _##name;
FRIDA_KERNEL_ADDRS (FRIDA_DEFINE_SLOT)
#undef FRIDA_DEFINE_SLOT
typedef struct _FridaKernelAddr FridaKernelAddr;
struct _FridaKernelAddr
{
const char * name;
void ** slot;
};
static const FridaKernelAddr frida_kernel_addrs[] = {
#define FRIDA_LIST_SLOT(name) { #name, &_##name },
FRIDA_KERNEL_ADDRS (FRIDA_LIST_SLOT)
#undef FRIDA_LIST_SLOT
};
extern void * frida_kext_init_start __asm ("section$start$__DATA$__mod_init_func");
extern void * frida_kext_init_end __asm ("section$end$__DATA$__mod_init_func");
const uintptr_t frida_agent_init_start = (uintptr_t) &frida_kext_init_start;
const uintptr_t frida_agent_init_end = (uintptr_t) &frida_kext_init_end;
const uintptr_t frida_agent_private_start = 0;
const uintptr_t frida_agent_heap_start = 0;
const uintptr_t frida_agent_relocs_start = 0;
const uintptr_t frida_agent_relocs_end = 0;
typedef int (* FridaProcCallout) (proc_t process, void * argument);
const unsigned frida_agent_disc_thread_continue =
ptrauth_type_discriminator (thread_continue_t);
const unsigned frida_agent_disc_proc_callout =
ptrauth_type_discriminator (FridaProcCallout);
_Static_assert (ptrauth_type_discriminator (thread_continue_t) == 0xd507,
"the agent signs a thread's entry with 0xd507");
int
getentropy (void * buffer, size_t size)
{
read_random (buffer, (u_int) size);
return 0;
}
void
sys_icache_invalidate (void * start, size_t size)
{
flush_dcache ((vm_offset_t) start, (unsigned) size, FALSE);
invalidate_icache ((vm_offset_t) start, (unsigned) size, FALSE);
}
#define FRIDA_IOC_SET_ADDR _IOW ('F', 1, FridaAddrRequest)
#define FRIDA_IOC_START _IO ('F', 2)
#define FRIDA_IOC_GET_IMAGE _IOR ('F', 3, FridaImageInfo)
typedef struct _FridaAddrRequest FridaAddrRequest;
typedef struct _FridaImageInfo FridaImageInfo;
struct _FridaAddrRequest
{
char name[64];
uint64_t address;
};
struct _FridaImageInfo
{
uint64_t base;
uint64_t size;
};
#define FRIDA_LINK_ROOM (1024 * 1024)
typedef struct _FridaLink FridaLink;
struct _FridaLink
{
lck_mtx_t * lock;
bool open;
uint8_t * to_host;
size_t to_host_len;
uint8_t * from_host;
size_t from_host_len;
};
static FridaLink frida_link;
static lck_grp_t * frida_lock_group;
static int frida_dev_open (dev_t dev, int flags, int devtype, struct proc * p);
static int frida_dev_close (dev_t dev, int flags, int devtype, struct proc * p);
static int frida_dev_read (dev_t dev, struct uio * uio, int ioflag);
static int frida_dev_write (dev_t dev, struct uio * uio, int ioflag);
static int frida_dev_ioctl (dev_t dev, u_long cmd, caddr_t data, int fflag, struct proc * p);
static struct cdevsw frida_cdevsw = {
.d_open = frida_dev_open,
.d_close = frida_dev_close,
.d_read = frida_dev_read,
.d_write = frida_dev_write,
.d_ioctl = frida_dev_ioctl,
.d_stop = eno_stop,
.d_reset = eno_reset,
.d_ttys = NULL,
.d_select = eno_select,
.d_mmap = eno_mmap,
.d_strategy = eno_strat,
.d_reserved_1 = eno_getc,
.d_reserved_2 = eno_putc,
.d_type = 0
};
static int frida_major = -1;
static void * frida_node = NULL;
static bool frida_running = false;
static uint64_t frida_own_base = 0;
static uint64_t frida_own_size = 0;
kern_return_t
frida_kext_start (kmod_info_t * ki, void * d)
{
frida_own_base = ki->address;
frida_own_size = ki->size;
frida_lock_group = lck_grp_alloc_init ("frida", LCK_GRP_ATTR_NULL);
if (frida_lock_group == NULL)
return KERN_FAILURE;
frida_link.lock = lck_mtx_alloc_init (frida_lock_group, LCK_ATTR_NULL);
frida_major = cdevsw_add (-1, &frida_cdevsw);
if (frida_major == -1)
return KERN_FAILURE;
frida_node = devfs_make_node (makedev (frida_major, 0), DEVFS_CHAR, UID_ROOT, GID_WHEEL, 0600,
"frida");
if (frida_node == NULL)
{
cdevsw_remove (frida_major, &frida_cdevsw);
frida_major = -1;
return KERN_FAILURE;
}
return KERN_SUCCESS;
}
kern_return_t
frida_kext_stop (kmod_info_t * ki, void * d)
{
if (frida_running)
{
frida_agent_stop ();
frida_running = false;
}
if (frida_node != NULL)
{
devfs_remove (frida_node);
frida_node = NULL;
}
if (frida_major != -1)
{
cdevsw_remove (frida_major, &frida_cdevsw);
frida_major = -1;
}
return KERN_SUCCESS;
}
KMOD_EXPLICIT_DECL (re.frida.agent, "1.0", frida_kext_start, frida_kext_stop)
static int
frida_dev_open (dev_t dev, int flags, int devtype, struct proc * p)
{
int result = 0;
lck_mtx_lock (frida_link.lock);
if (frida_link.open)
{
result = EBUSY;
}
else
{
frida_link.to_host = _MALLOC (FRIDA_LINK_ROOM, M_TEMP, M_WAITOK);
frida_link.from_host = _MALLOC (FRIDA_LINK_ROOM, M_TEMP, M_WAITOK);
if (frida_link.to_host != NULL && frida_link.from_host != NULL)
{
frida_link.to_host_len = 0;
frida_link.from_host_len = 0;
frida_link.open = true;
}
else
{
result = ENOMEM;
}
}
lck_mtx_unlock (frida_link.lock);
return result;
}
static int
frida_dev_close (dev_t dev, int flags, int devtype, struct proc * p)
{
lck_mtx_lock (frida_link.lock);
frida_link.open = false;
if (frida_link.to_host != NULL)
{
_FREE (frida_link.to_host, M_TEMP);
frida_link.to_host = NULL;
}
if (frida_link.from_host != NULL)
{
_FREE (frida_link.from_host, M_TEMP);
frida_link.from_host = NULL;
}
wakeup (&frida_link);
lck_mtx_unlock (frida_link.lock);
return 0;
}
static int
frida_dev_read (dev_t dev, struct uio * uio, int ioflag)
{
int result = 0;
lck_mtx_lock (frida_link.lock);
while (frida_link.open && frida_link.to_host_len == 0)
{
if (msleep (&frida_link, frida_link.lock, PCATCH, "frida", NULL) != 0)
{
lck_mtx_unlock (frida_link.lock);
return EINTR;
}
}
if (frida_link.to_host_len != 0)
{
size_t n = MIN ((size_t) uio_resid (uio), frida_link.to_host_len);
result = uiomove ((const char *) frida_link.to_host, (int) n, uio);
if (result == 0)
{
memmove (frida_link.to_host, frida_link.to_host + n, frida_link.to_host_len - n);
frida_link.to_host_len -= n;
}
}
lck_mtx_unlock (frida_link.lock);
return result;
}
static int
frida_dev_write (dev_t dev, struct uio * uio, int ioflag)
{
size_t n;
int result;
lck_mtx_lock (frida_link.lock);
n = MIN ((size_t) uio_resid (uio), FRIDA_LINK_ROOM - frida_link.from_host_len);
if (n == 0)
{
lck_mtx_unlock (frida_link.lock);
return ENOBUFS;
}
result = uiomove ((char *) frida_link.from_host + frida_link.from_host_len, (int) n, uio);
if (result == 0)
frida_link.from_host_len += n;
lck_mtx_unlock (frida_link.lock);
if (result == 0 && frida_running)
frida_agent_wake ();
return result;
}
int
frida_kmod_link_open (void)
{
return frida_link.open ? 0 : -1;
}
void
frida_kmod_link_close (void)
{
}
int
frida_kmod_link_send (const void * data, size_t size)
{
lck_mtx_lock (frida_link.lock);
if (!frida_link.open || frida_link.to_host_len + size > FRIDA_LINK_ROOM)
{
lck_mtx_unlock (frida_link.lock);
return -1;
}
memcpy (frida_link.to_host + frida_link.to_host_len, data, size);
frida_link.to_host_len += size;
wakeup (&frida_link);
lck_mtx_unlock (frida_link.lock);
return 0;
}
bool
frida_kmod_link_pending (void)
{
bool pending;
lck_mtx_lock (frida_link.lock);
pending = frida_link.from_host_len != 0;
lck_mtx_unlock (frida_link.lock);
return pending;
}
ssize_t
frida_kmod_link_recv (void * data, size_t size)
{
size_t n;
lck_mtx_lock (frida_link.lock);
n = MIN (size, frida_link.from_host_len);
if (n != 0)
{
memcpy (data, frida_link.from_host, n);
memmove (frida_link.from_host, frida_link.from_host + n, frida_link.from_host_len - n);
frida_link.from_host_len -= n;
}
lck_mtx_unlock (frida_link.lock);
return (ssize_t) n;
}
static int
frida_dev_ioctl (dev_t dev, u_long cmd, caddr_t data, int fflag, struct proc * p)
{
switch (cmd)
{
case FRIDA_IOC_SET_ADDR:
{
FridaAddrRequest * r = (FridaAddrRequest *) data;
size_t i;
r->name[sizeof (r->name) - 1] = '\0';
for (i = 0; i != sizeof (frida_kernel_addrs) / sizeof (frida_kernel_addrs[0]); i++)
{
if (strncmp (frida_kernel_addrs[i].name, r->name, sizeof (r->name)) == 0)
{
*frida_kernel_addrs[i].slot = (void *) (uintptr_t) r->address;
return 0;
}
}
return ENOENT;
}
case FRIDA_IOC_GET_IMAGE:
{
FridaImageInfo * info = (FridaImageInfo *) data;
info->base = frida_own_base;
info->size = frida_own_size;
return 0;
}
case FRIDA_IOC_START:
{
if (frida_running)
return EBUSY;
if (frida_agent_start (frida_own_base, frida_own_size) != 0)
return ENOMEM;
frida_running = true;
return 0;
}
default:
return ENOTTY;
}
}
+5
View File
@@ -0,0 +1,5 @@
#include <frida-gumjs.h>
#include <ptrauth.h>
_Static_assert (ptrauth_type_discriminator (__typeof__ (((GSourceFuncs *) 0)->prepare))
== 0xe4fe, "the kernel's own ABI still discriminates by type");
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""Make a kernel the agent can write to and run code in."""
import struct
import sys
NOP = 0xD503201F
FORBIDS_BELOW = 0x3800
ALLOWS_BELOW = 0x2000
def main(source, destination):
image = bytearray(open(source, "rb").read())
secs = sections(image)
print("pages the pmap has locked down:")
locked = stop_refusing(image, secs, strings_saying(bytes(image), secs, b"locked down"),
"may be mapped")
print("what a table forbids underneath it:")
below = stop_forbidding_execution_below(image, secs)
print("executable mappings in the kernel's own pmap:")
executable = stop_refusing(image, secs,
strings_saying(bytes(image), secs, b"executable mapping"),
"may be made")
if not (locked and below and executable):
sys.exit("kernel does not look like one this tool knows")
open(destination, "wb").write(bytes(image))
print(f"\n{locked + below + executable} changes -> {destination}")
def stop_refusing(image, secs, saying, what):
undone = 0
for segment, name, addr, size, foff in secs:
if name != "__text":
continue
code = bytes(image[foff:foff + size])
blocks = {addr + b
for b in (start_of_block(code, s) for s in complaints_at(code, addr, saying))
if b is not None}
if not blocks:
continue
for where, at, word in branches_into(code, addr, blocks):
struct.pack_into("<I", image, foff + at, NOP)
print(f" {segment} {where:#x} {word:#010x} -> nop ({what})")
undone += 1
return undone
def stop_forbidding_execution_below(image, secs):
undone = 0
for segment, name, addr, size, foff in secs:
if name != "__text":
continue
code = bytes(image[foff:foff + size])
for i in range(0, len(code) - 4, 4):
word = struct.unpack_from("<I", code, i)[0]
builds_high = (word & 0xFF800000) == 0xF2800000 and ((word >> 21) & 3) == 3
if not builds_high or ((word >> 5) & 0xFFFF) != FORBIDS_BELOW:
continue
into = word & 0x1F
struct.pack_into("<I", image, foff + i, 0xF2E00000 | (ALLOWS_BELOW << 5) | into)
print(f" {segment} {addr + i:#x} {word:#010x} -> "
f"movk x{into}, #{ALLOWS_BELOW:#06x}, lsl 48"
f" (a table no longer forbids what is under it)")
undone += 1
return undone
def strings_saying(image, secs, wanted):
where = set()
at = image.find(wanted)
while at != -1:
start = image.rfind(b"\0", 0, at) + 1
for _, name, addr, size, foff in secs:
if name == "__cstring" and foff <= start < foff + size:
where.add(addr + (start - foff))
at = image.find(wanted, at + 1)
return where
def complaints_at(code, base, about):
said = []
for i in range(0, len(code) - 4, 4):
word = struct.unpack_from("<I", code, i)[0]
if (word & 0x9F000000) != 0x90000000:
continue
into = word & 0x1F
page = (((word >> 5) & 0x7FFFF) << 2) | ((word >> 29) & 3)
if page & (1 << 20):
page -= 1 << 21
page = ((base + i) & ~0xFFF) + (page << 12)
for j in range(i + 4, min(i + 48, len(code) - 4), 4):
after = struct.unpack_from("<I", code, j)[0]
if (after & 0xFF800000) == 0x91000000 and ((after >> 5) & 0x1F) == into:
if page + ((after >> 10) & 0xFFF) in about:
said.append(i)
break
return said
def start_of_block(code, at):
i = at
while i >= 4:
word = struct.unpack_from("<I", code, i - 4)[0]
if (word & 0xFC000000) in (0x94000000, 0x14000000):
return i
if (word & 0xFFFFFC1F) == 0xD65F0000 or word == 0xD65F0FFF:
return i
i -= 4
return None
def branches_into(code, base, blocks):
found = []
for i in range(0, len(code) - 4, 4):
word = struct.unpack_from("<I", code, i)[0]
if (word & 0xFF000010) == 0x54000000 or (word & 0x7E000000) == 0x34000000:
far, sign = (word >> 5) & 0x7FFFF, 1 << 18
elif (word & 0x7E000000) == 0x36000000:
far, sign = (word >> 5) & 0x3FFF, 1 << 13
else:
continue
if far & sign:
far -= sign << 1
if base + i + far * 4 in blocks:
found.append((base + i, i, word))
return found
def sections(image):
count = struct.unpack_from("<I", image, 16)[0]
at, found = 32, []
for _ in range(count):
kind, size = struct.unpack_from("<II", image, at)
if kind == 0x19:
how_many = struct.unpack_from("<I", image, at + 64)[0]
so = at + 72
for _ in range(how_many):
found.append((
image[so + 16:so + 32].rstrip(b"\0").decode(),
image[so:so + 16].rstrip(b"\0").decode(),
*struct.unpack_from("<QQ", image, so + 32),
struct.unpack_from("<I", image, so + 48)[0],
))
so += 80
at += size
return found
if len(sys.argv) != 3:
sys.exit(f"usage: {sys.argv[0]} <kernel> <patched-kernel>")
main(sys.argv[1], sys.argv[2])
-19
View File
@@ -1,19 +0,0 @@
{
"name": "frida-barebone-agent",
"version": "1.0.0",
"description": "xPack configuration for frida-barebone-agent",
"xpack": {
"minimumXpmRequired": "0.20.8",
"dependencies": {},
"devDependencies": {
"@xpack-dev-tools/aarch64-none-elf-gcc": {
"specifier": "14.2.1-1.1.1",
"local": "link",
"platforms": "all"
}
},
"properties": {},
"actions": {},
"buildConfigurations": {}
}
}
+7 -9
View File
@@ -1,7 +1,7 @@
use core::ffi::c_void;
use crate::bindings::{gpointer, gsize, GumPageProtection};
use crate::{gum, libc, xnu};
use crate::bindings::{gpointer, gsize, gum_try_mprotect, GumPageProtection};
use crate::{kernel, libc};
const GUM_PAGE_READ: GumPageProtection = 1;
const GUM_PAGE_EXECUTE: GumPageProtection = 4;
@@ -10,7 +10,7 @@ const GUM_PAGE_EXECUTE: GumPageProtection = 4;
// static trampoline `text` made executable, followed by a writable parameter
// table `map_size` bytes later (the trampolines reach their parameters via that
// fixed offset). The two halves are whole pages, so flipping the first to RX
// over the hostlink leaves the parameter table writable.
// leaves the parameter table writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_tramp_embedder_map(
text: *const c_void,
@@ -19,7 +19,7 @@ pub unsafe extern "C" fn ffi_tramp_embedder_map(
parm_table: *mut *mut c_void,
) -> i32 {
unsafe {
let base = xnu::kalloc(map_size * 2);
let base = kernel::alloc_code(map_size * 2);
if base.is_null() {
return 0;
}
@@ -28,9 +28,9 @@ pub unsafe extern "C" fn ffi_tramp_embedder_map(
libc::__clear_cache(base, base.add(map_size));
let made_executable =
gum::gum_try_mprotect(base as gpointer, map_size as gsize, GUM_PAGE_READ | GUM_PAGE_EXECUTE);
gum_try_mprotect(base as gpointer, map_size as gsize, GUM_PAGE_READ | GUM_PAGE_EXECUTE);
if made_executable == 0 {
xnu::free(base, map_size * 2);
kernel::free_code(base, map_size * 2);
return 0;
}
@@ -46,7 +46,5 @@ pub unsafe extern "C" fn ffi_tramp_embedder_unmap(
_parm_table: *mut c_void,
map_size: usize,
) {
unsafe {
xnu::free(code_table as *mut u8, map_size * 2);
}
kernel::free_code(code_table as *mut u8, map_size * 2);
}
+47 -24
View File
@@ -1,46 +1,69 @@
use core::ptr;
use core::sync::atomic::{AtomicUsize, Ordering};
use crate::bindings::{g_wait_is_set, gint64, gpointer};
use crate::xnu;
use crate::kernel;
const G_WAIT_INFINITE: gint64 = -1;
pub static mut WAKEUP_TOKEN: u64 = 0;
pub fn wakeup_token() -> *const u8 {
ptr::addr_of!(WAKEUP_TOKEN) as *const u8
}
#[unsafe(no_mangle)]
pub extern "C" fn g_get_monotonic_time() -> gint64 {
let abstime = xnu::mach_absolute_time();
let time = xnu::absolutetime_to_nanoseconds(abstime);
(time / 1000) as gint64
kernel::monotonic_micros()
}
#[unsafe(no_mangle)]
pub extern "C" fn g_wait_sleep(token: gpointer, timeout_us: gint64) {
let wait_event = ptr::addr_of_mut!(WAKEUP_TOKEN) as *const u8;
let wait_result = if timeout_us == G_WAIT_INFINITE {
xnu::assert_wait(wait_event, xnu::THREAD_INTERRUPTIBLE)
let timeout = if timeout_us == G_WAIT_INFINITE {
None
} else {
xnu::assert_wait_timeout(
wait_event,
xnu::THREAD_INTERRUPTIBLE,
(timeout_us * 1000) as u32,
1, // TODO
)
Some(timeout_us as u64)
};
if wait_result != xnu::THREAD_WAITING {
panic!("assert_wait_timeout failed: {}", wait_result);
}
if unsafe { g_wait_is_set(token) != 0 } {
xnu::thread_wakeup(wait_event);
return;
}
let ours = is_loop_thread();
let event = if ours {
LOOP_TOKEN.store(token as usize, Ordering::Release);
ptr::addr_of_mut!(WAKEUP_TOKEN) as *const u8
} else {
token as *const u8
};
xnu::thread_block(None);
kernel::wait(event, timeout, &mut || unsafe { g_wait_is_set(token) != 0 });
if ours {
LOOP_TOKEN.store(0, Ordering::Release);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn g_wait_wake(_token: gpointer) {
xnu::thread_wakeup(ptr::addr_of_mut!(WAKEUP_TOKEN) as *const u8);
pub extern "C" fn g_wait_wake(token: gpointer) {
kernel::poke_loop_wakeup();
kernel::wake(ptr::addr_of_mut!(WAKEUP_TOKEN) as *const u8);
kernel::wake(token as *const u8);
}
pub fn is_loop_thread() -> bool {
kernel::current_thread_id() == LOOP_THREAD.load(Ordering::Acquire) as u64
}
pub fn own_the_loop() {
LOOP_THREAD.store(kernel::current_thread_id() as usize, Ordering::Release);
}
static LOOP_THREAD: AtomicUsize = AtomicUsize::new(0);
static LOOP_TOKEN: AtomicUsize = AtomicUsize::new(0);
#[unsafe(no_mangle)]
pub extern "C" fn g_io_channel_unix_new(_fd: i32) -> gpointer {
ptr::null_mut()
}
#[unsafe(no_mangle)]
pub extern "C" fn g_spawn_async_with_pipes() -> i32 {
0
}
+154 -76
View File
@@ -8,34 +8,39 @@ use crate::bindings::GThreadFunc;
use crate::bindings::{gpointer, gulong};
use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use core::arch::asm;
use core::ffi::c_void;
use core::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use core::sync::atomic::{AtomicU32, AtomicU64, AtomicUsize, Ordering};
// Our implementation structures that overlay the opaque GLib structures
#[repr(C, align(8))]
#[cfg_attr(target_pointer_width = "32", repr(C, align(4)))]
#[cfg_attr(target_pointer_width = "64", repr(C, align(8)))]
struct MutexImpl {
lock: AtomicU32,
_padding: [u8; core::mem::size_of::<GMutex>() - core::mem::size_of::<AtomicU32>()],
}
#[repr(C, align(8))]
#[cfg_attr(target_pointer_width = "32", repr(C, align(4)))]
#[cfg_attr(target_pointer_width = "64", repr(C, align(8)))]
struct RecMutexImpl {
owner: AtomicU64, // Thread ID of the owner (uses the 'p' field)
owner: AtomicUsize, // Thread ID of the owner (uses the 'p' field)
count: AtomicU32, // Recursion count (uses i[0])
_unused: u32, // Unused (uses i[1])
}
#[repr(C, align(8))]
#[cfg_attr(target_pointer_width = "32", repr(C, align(4)))]
#[cfg_attr(target_pointer_width = "64", repr(C, align(8)))]
struct RWLockImpl {
state: AtomicU32, // Lock state (readers count + writer bit)
_padding: [u8; core::mem::size_of::<GRWLock>() - core::mem::size_of::<AtomicU32>()],
}
#[repr(C, align(8))]
// A generation counter rather than a flag: a flag stays set once signalled, so every
// later wait returns at once and the caller's loop spins instead of blocking.
#[cfg_attr(target_pointer_width = "32", repr(C, align(4)))]
#[cfg_attr(target_pointer_width = "64", repr(C, align(8)))]
struct CondImpl {
signal: AtomicU32, // Signal state
generation: AtomicU32,
_padding: [u8; core::mem::size_of::<GCond>() - core::mem::size_of::<AtomicU32>()],
}
@@ -144,10 +149,9 @@ pub extern "C" fn g_rec_mutex_clear(_rec_mutex: *mut GRecMutex) {
pub extern "C" fn g_rec_mutex_lock(rec_mutex: *mut GRecMutex) {
unsafe {
let impl_ = rec_mutex_impl(rec_mutex);
let current_thread = get_current_thread_id();
loop {
if rec_mutex_try_acquire(impl_, current_thread) {
if rec_mutex_try_acquire(impl_) {
break;
}
rec_mutex_wait(impl_);
@@ -159,9 +163,8 @@ pub extern "C" fn g_rec_mutex_lock(rec_mutex: *mut GRecMutex) {
pub extern "C" fn g_rec_mutex_trylock(rec_mutex: *mut GRecMutex) -> u32 {
unsafe {
let impl_ = rec_mutex_impl(rec_mutex);
let current_thread = get_current_thread_id();
if rec_mutex_try_acquire(impl_, current_thread) {
if rec_mutex_try_acquire(impl_) {
1
} else {
0
@@ -183,7 +186,7 @@ pub extern "C" fn g_rec_mutex_unlock(rec_mutex: *mut GRecMutex) {
.is_ok()
{
impl_.owner.store(0, Ordering::Relaxed);
lock_notify_all();
wake_waiters(&impl_.count);
break;
}
} else {
@@ -198,7 +201,10 @@ pub extern "C" fn g_rec_mutex_unlock(rec_mutex: *mut GRecMutex) {
}
}
unsafe fn rec_mutex_try_acquire(impl_: &mut RecMutexImpl, current_thread: u64) -> bool {
// Asking the kernel who we are costs more than the uncontended acquisition itself, and
// every allocation takes this path, so the identity is only fetched once the lock turns
// out to be held.
unsafe fn rec_mutex_try_acquire(impl_: &mut RecMutexImpl) -> bool {
let current_count = impl_.count.load(Ordering::Relaxed);
if current_count == 0 {
@@ -206,12 +212,12 @@ unsafe fn rec_mutex_try_acquire(impl_: &mut RecMutexImpl, current_thread: u64) -
.compare_exchange_weak(0, 1, Ordering::Acquire, Ordering::Relaxed)
.is_ok()
{
impl_.owner.store(current_thread, Ordering::Relaxed);
impl_.owner.store(crate::kernel::current_thread_id() as usize, Ordering::Relaxed);
return true;
}
} else {
let current_owner = impl_.owner.load(Ordering::Relaxed);
if current_owner == current_thread {
if current_owner == crate::kernel::current_thread_id() as usize {
if impl_.count
.compare_exchange_weak(
current_count,
@@ -230,7 +236,7 @@ unsafe fn rec_mutex_try_acquire(impl_: &mut RecMutexImpl, current_thread: u64) -
unsafe fn rec_mutex_wait(impl_: &mut RecMutexImpl) {
while impl_.count.load(Ordering::Relaxed) != 0 {
unsafe { lock_wait_primitive() };
wait_on(&impl_.count, &mut || impl_.count.load(Ordering::Relaxed) == 0);
}
}
@@ -282,7 +288,7 @@ pub extern "C" fn g_rw_lock_writer_unlock(rw_lock: *mut GRWLock) {
unsafe {
let impl_ = rw_lock_impl(rw_lock);
impl_.state.store(0, Ordering::Release);
lock_notify_all();
wake_waiters(&impl_.state);
}
}
@@ -329,7 +335,7 @@ pub extern "C" fn g_rw_lock_reader_unlock(rw_lock: *mut GRWLock) {
.is_ok()
{
if current - 1 == 0 {
lock_notify_all();
wake_waiters(&impl_.state);
}
break;
}
@@ -355,13 +361,15 @@ unsafe fn rw_lock_reader_try_acquire(impl_: &mut RWLockImpl) -> bool {
unsafe fn rw_lock_wait_for_writers(impl_: &mut RWLockImpl) {
while impl_.state.load(Ordering::Relaxed) & RW_WRITER_LOCK_BIT != 0 {
unsafe { lock_wait_primitive() };
wait_on(&impl_.state, &mut || {
impl_.state.load(Ordering::Relaxed) & RW_WRITER_LOCK_BIT == 0
});
}
}
unsafe fn rw_lock_wait_for_all(impl_: &mut RWLockImpl) {
while impl_.state.load(Ordering::Relaxed) != 0 {
unsafe { lock_wait_primitive() };
wait_on(&impl_.state, &mut || impl_.state.load(Ordering::Relaxed) == 0);
}
}
@@ -369,7 +377,7 @@ unsafe fn rw_lock_wait_for_all(impl_: &mut RWLockImpl) {
pub extern "C" fn g_cond_init(cond: *mut GCond) {
unsafe {
let impl_ = cond_impl(cond);
impl_.signal.store(0, Ordering::Relaxed);
impl_.generation.store(0, Ordering::Relaxed);
}
}
@@ -379,11 +387,17 @@ pub extern "C" fn g_cond_clear(_cond: *mut GCond) {}
#[unsafe(no_mangle)]
pub extern "C" fn g_cond_wait(cond: *mut GCond, mutex: *mut GMutex) {
unsafe {
// Read the generation under the mutex, so a signal that lands after the
// unlock below is still observed as a change.
let seen = cond_impl(cond).generation.load(Ordering::Acquire);
g_mutex_unlock(mutex);
let impl_ = cond_impl(cond);
while impl_.signal.load(Ordering::Acquire) == 0 {
lock_wait_primitive();
while impl_.generation.load(Ordering::Acquire) == seen {
wait_on(&impl_.generation, &mut || {
impl_.generation.load(Ordering::Acquire) != seen
});
}
g_mutex_lock(mutex);
@@ -394,8 +408,8 @@ pub extern "C" fn g_cond_wait(cond: *mut GCond, mutex: *mut GMutex) {
pub extern "C" fn g_cond_signal(cond: *mut GCond) {
unsafe {
let impl_ = cond_impl(cond);
impl_.signal.store(1, Ordering::Release);
lock_notify_all();
impl_.generation.fetch_add(1, Ordering::Release);
wake_waiters(&impl_.generation);
}
}
@@ -403,22 +417,82 @@ pub extern "C" fn g_cond_signal(cond: *mut GCond) {
pub extern "C" fn g_cond_broadcast(cond: *mut GCond) {
unsafe {
let impl_ = cond_impl(cond);
impl_.signal.store(1, Ordering::Release);
lock_notify_all();
impl_.generation.fetch_add(1, Ordering::Release);
wake_waiters(&impl_.generation);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn g_cond_wait_until(cond: *mut GCond, mutex: *mut GMutex, _end_time: i64) -> u32 {
// TODO: Implement proper timeout handling
g_cond_wait(cond, mutex);
1
pub extern "C" fn g_cond_wait_until(cond: *mut GCond, mutex: *mut GMutex, end_time: i64) -> u32 {
unsafe {
let seen = cond_impl(cond).generation.load(Ordering::Acquire);
g_mutex_unlock(mutex);
let impl_ = cond_impl(cond);
let mut signalled = impl_.generation.load(Ordering::Acquire) != seen;
while !signalled && crate::kernel::monotonic_micros() < end_time {
wait_on(&impl_.generation, &mut || {
impl_.generation.load(Ordering::Acquire) != seen
});
signalled = impl_.generation.load(Ordering::Acquire) != seen;
}
g_mutex_lock(mutex);
if signalled { 1 } else { 0 }
}
}
// A half that runs on a system with slots of its own says so, and every GPrivate takes one. The
// map below is what the kernel halves use, and it costs a lock and a lookup per call, which is
// what code the Stalker writes does on every block it leaves.
pub struct ThreadSlots {
pub take: fn() -> u32,
pub get: fn(u32) -> *mut c_void,
pub set: fn(u32, *mut c_void),
}
#[cfg(any(feature = "win9x", feature = "winnt", feature = "xnu-core"))]
pub fn use_thread_slots(slots: &'static ThreadSlots) {
unsafe { SLOTS = Some(slots) };
}
fn slot_of(key: *mut GPrivate, slots: &ThreadSlots) -> u32 {
let taken = unsafe { (*key).p } as usize;
if taken != 0 {
return (taken - 1) as u32;
}
unsafe {
lock_acquire(&TLS_LOCK);
let mut taken = (*key).p as usize;
if taken == 0 {
taken = ((slots.take)() + 1) as usize;
(*key).p = taken as *mut c_void;
}
lock_release(&TLS_LOCK);
(taken - 1) as u32
}
}
fn slots() -> Option<&'static ThreadSlots> {
unsafe { SLOTS }
}
static mut SLOTS: Option<&'static ThreadSlots> = None;
#[unsafe(no_mangle)]
pub extern "C" fn g_private_get(key: *mut GPrivate) -> *mut c_void {
if let Some(slots) = slots() {
return (slots.get)(slot_of(key, slots));
}
unsafe {
let thread_id = get_current_thread_id();
let thread_id = crate::kernel::current_thread_id();
lock_acquire(&TLS_LOCK);
ensure_tls_storage();
@@ -437,8 +511,13 @@ pub extern "C" fn g_private_get(key: *mut GPrivate) -> *mut c_void {
#[unsafe(no_mangle)]
pub extern "C" fn g_private_set(key: *mut GPrivate, value: *mut c_void) {
if let Some(slots) = slots() {
(slots.set)(slot_of(key, slots), value);
return;
}
unsafe {
let thread_id = get_current_thread_id();
let thread_id = crate::kernel::current_thread_id();
lock_acquire(&TLS_LOCK);
ensure_tls_storage();
@@ -468,7 +547,9 @@ pub extern "C" fn _g_system_thread_create(
data: gpointer,
) -> *mut GSystemThread {
unsafe {
let system_thread = crate::xnu::kalloc(core::mem::size_of::<SystemThreadImpl>()) as *mut SystemThreadImpl;
crate::kernel::log("frida: glib is creating a thread\n\0");
let system_thread = crate::kernel::alloc(core::mem::size_of::<SystemThreadImpl>()) as *mut SystemThreadImpl;
g_mutex_init(&mut (*system_thread).mutex);
g_cond_init(&mut (*system_thread).cond);
@@ -478,12 +559,12 @@ pub extern "C" fn _g_system_thread_create(
(*system_thread).finished.store(0, Ordering::Relaxed);
(*system_thread).detached.store(0, Ordering::Relaxed);
let wrapper_data = crate::xnu::kalloc(core::mem::size_of::<ThreadWrapperData>()) as *mut ThreadWrapperData;
let wrapper_data = crate::kernel::alloc(core::mem::size_of::<ThreadWrapperData>()) as *mut ThreadWrapperData;
(*wrapper_data).func = func;
(*wrapper_data).data = data;
(*wrapper_data).system_thread = system_thread;
let _xnu_result = crate::xnu::kernel_thread_start(thread_wrapper, wrapper_data as *mut c_void);
let _xnu_result = crate::kernel::spawn_thread(thread_wrapper, wrapper_data as *mut c_void);
system_thread as *mut GSystemThread
}
@@ -496,8 +577,9 @@ extern "C" fn thread_wrapper(parameter: *mut c_void, _wait_result: i32) {
let data = (*wrapper_data).data;
let system_thread = (*wrapper_data).system_thread;
let current_thread_id = get_current_thread_id();
(*system_thread).thread_id.store(current_thread_id, Ordering::Relaxed);
let current_thread_id = crate::kernel::current_thread_id();
(*system_thread).thread_id.store(current_thread_id, Ordering::Release);
wake_waiters(&(*system_thread).thread_id);
func.unwrap()(data);
@@ -537,12 +619,10 @@ pub extern "C" fn _g_system_thread_wait(thread: *mut GSystemThread) {
unsafe {
let system_thread = thread as *mut SystemThreadImpl;
loop {
let thread_id = (*system_thread).thread_id.load(Ordering::Acquire);
if thread_id != 0 {
break;
}
core::hint::spin_loop();
while (*system_thread).thread_id.load(Ordering::Acquire) == 0 {
wait_on(&(*system_thread).thread_id, &mut || {
(*system_thread).thread_id.load(Ordering::Acquire) != 0
});
}
g_mutex_lock(&mut (*system_thread).mutex);
@@ -556,19 +636,16 @@ pub extern "C" fn _g_system_thread_wait(thread: *mut GSystemThread) {
}
unsafe fn cleanup_system_thread(system_thread: *mut SystemThreadImpl) {
crate::xnu::free(system_thread as *mut u8, core::mem::size_of::<SystemThreadImpl>());
crate::kernel::free(system_thread as *mut u8, core::mem::size_of::<SystemThreadImpl>());
}
unsafe fn cleanup_wrapper_data(wrapper_data: *mut ThreadWrapperData) {
crate::xnu::free(wrapper_data as *mut u8, core::mem::size_of::<ThreadWrapperData>());
crate::kernel::free(wrapper_data as *mut u8, core::mem::size_of::<ThreadWrapperData>());
}
unsafe fn lock_acquire(lock: &AtomicU32) {
loop {
if unsafe { lock_try_acquire(lock) } {
break;
}
unsafe { lock_wait(lock) };
while !unsafe { lock_try_acquire(lock) } {
wait_on(lock, &mut || lock.load(Ordering::Relaxed) == 0);
}
}
@@ -579,35 +656,36 @@ unsafe fn lock_try_acquire(lock: &AtomicU32) -> bool {
unsafe fn lock_release(lock: &AtomicU32) {
lock.store(0, Ordering::Release);
unsafe { lock_notify_all() };
wake_waiters(lock);
}
unsafe fn lock_wait(lock: &AtomicU32) {
while lock.load(Ordering::Relaxed) != 0 {
unsafe { lock_wait_primitive() };
}
// Sleeps until `ready` holds or the slice expires, whichever comes first.
//
// These used to spin on wfe/sev. A kernel thread that spins never reaches the
// scheduler, so a lost wakeup — or a genuine deadlock, which is how we found this —
// starves the CPU it is on until the hardware watchdog takes the machine down. The
// bounded sleep turns both into a stall that can be observed instead.
//
// The address of the word being waited on names the wait channel: XNU pairs
// assert_wait()/thread_wakeup() on it directly, while the Linux shim has a single
// channel and treats it as a hint. Every caller re-checks its own condition in a
// loop, so a spurious wakeup costs nothing either way.
fn wait_on<T>(word: &T, ready: &mut dyn FnMut() -> bool) {
crate::kernel::wait(token_of(word), Some(WAIT_SLICE_US), ready);
// The wait above returns without sleeping whenever `ready` already holds, so on
// its own it is not enough to keep a contended loop off the CPU: every caller
// here loops, and a loop in a kernel thread that never reaches the scheduler
// takes the machine down rather than merely running hot. Yield unconditionally.
crate::kernel::yield_now();
}
unsafe fn lock_wait_primitive() {
unsafe {
asm!("wfe", options(nomem, nostack));
}
fn wake_waiters<T>(word: &T) {
crate::kernel::wake(token_of(word));
}
unsafe fn lock_notify_all() {
unsafe {
asm!("sev", options(nomem, nostack));
}
fn token_of<T>(word: &T) -> *const u8 {
word as *const T as *const u8
}
// The thread pointer is exposed to JavaScript as a GumThreadId, so it must fit
// in a double without losing precision; the low bits keep it unique per thread.
const JS_SAFE_THREAD_ID_MASK: u64 = (1 << 48) - 1;
pub unsafe fn get_current_thread_id() -> u64 {
let thread_ptr: u64;
unsafe {
asm!("mrs {}, tpidr_el1", out(reg) thread_ptr, options(nomem, nostack));
}
thread_ptr & JS_SAFE_THREAD_ID_MASK
}
const WAIT_SLICE_US: u64 = 1000;
+419 -429
View File
@@ -1,296 +1,81 @@
// Gum's platform backend, without the parts that depend on the kernel. Memory protection,
// symbols and the module registry are in gum_injected.rs, gum_linux.rs and gum_windows.rs.
use crate::{
bindings::{
_GInterfaceInfo, _GTypeInfo, _GumPageProtection_GUM_PAGE_EXECUTE,
_GumPageProtection_GUM_PAGE_READ, _GumPageProtection_GUM_PAGE_WRITE,
_GumRwxSupport_GUM_RWX_NONE, GArray, GObject, GObjectClass, GPrivate, GType,
GumDebugSymbolDetails, GumExportDetails, GumExportType_GUM_EXPORT_FUNCTION,
GumFoundExportFunc, GumMemoryRange, GumModule, GumModuleInterface, GumModuleRegistry,
GumPageProtection, GumRwxSupport, GumThreadId, GumTlsKey, g_array_append_vals, g_array_new,
g_free, g_object_get_type, g_object_new, g_object_unref, g_once_init_enter,
g_once_init_leave, g_strdup, g_type_add_interface_static, g_type_class_peek_parent,
g_type_register_static, g_variant_get_boolean, g_variant_get_uint64, g_variant_new,
g_variant_new_fixed_array, g_variant_type_free, g_variant_type_new, gboolean, gchar,
gconstpointer, gpointer, gsize, guint,
gum_barebone_register_module,
gum_barebone_try_remap_writable_pages as _gum_barebone_try_remap_writable_pages,
g_variant_unref, gum_module_get_type, gum_mprotect, gum_query_page_size,
_GInterfaceInfo, _GTypeInfo, GObject, GObjectClass, GPrivate, GType, GumAddress,
GumExportDetails,
GumExportType_GUM_EXPORT_FUNCTION, GumFoundExportFunc, GumMemoryRange, GumModule,
GumModuleInterface, GumThreadId, GumTlsKey, g_free, g_object_get_type, g_object_new,
g_once_init_enter, g_once_init_leave, g_strdup, g_type_add_interface_static,
g_type_class_peek_parent, g_type_register_static, gchar, gpointer, gsize, guint,
},
gthread, host_rpc, libc, FridaCommand,
gthread, kernel, libc,
};
use alloc::boxed::Box;
use alloc::ffi::CString;
use alloc::format;
use alloc::vec::Vec;
use core::ffi::CStr;
use core::mem::size_of;
use alloc::vec::Vec;
use core::ptr;
use core::sync::atomic::{AtomicU32, Ordering};
// Code slabs the agent allocates are normal writable RAM, so gum can keep a real writable alias to
// them across the slab's lifetime. Pre-existing kernel/kext text is CTRR-locked and can only be
// modified through the host's physical-memory bridge, so we shadow it and commit on dispose. We
// distinguish the two by tracking the executable ranges we hand out.
static SLAB_LOCK: AtomicU32 = AtomicU32::new(0);
static mut SLABS: Vec<(u64, u64)> = Vec::new();
const SHADOW_MAGIC: u64 = 0x4644_4f48_5341_4853;
const SHADOW_HEADER: usize = 24;
const SHADOW_MIN_ADDRESS: u64 = 0xffff_f000_0000_0000;
fn slab_lock() {
while SLAB_LOCK
.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
core::hint::spin_loop();
}
}
fn slab_unlock() {
SLAB_LOCK.store(0, Ordering::Release);
}
fn register_slab(start: u64, size: usize) {
slab_lock();
unsafe {
(*core::ptr::addr_of_mut!(SLABS)).push((start, start + size as u64));
}
slab_unlock();
}
fn unregister_slab(start: u64) {
slab_lock();
unsafe {
(*core::ptr::addr_of_mut!(SLABS)).retain(|&(begin, _)| begin != start);
}
slab_unlock();
}
fn is_agent_slab(address: u64) -> bool {
slab_lock();
let found = unsafe {
(*core::ptr::addr_of!(SLABS))
.iter()
.any(|&(begin, end)| address >= begin && address < end)
};
slab_unlock();
found
}
#[cfg(feature = "linux")]
use crate::gum_linux::enumerate_exports_in_range;
#[cfg(any(feature = "linux", feature = "linux-injected"))]
use crate::bindings::{
GumFoundSymbolFunc, GumSymbolDetails, GumSymbolType_GUM_SYMBOL_FUNCTION,
GumSymbolType_GUM_SYMBOL_OBJECT, GumSymbolType_GUM_SYMBOL_UNKNOWN, gboolean, gssize,
};
#[cfg(any(feature = "linux", feature = "linux-injected"))]
use crate::gum_modules::enumerate_symbols_in_range;
#[cfg(any(feature = "win9x", feature = "winnt"))]
use crate::gum_windows::{enumerate_exports_in_range, enumerate_imports_in_module};
#[cfg(any(feature = "win9x", feature = "winnt"))]
use crate::bindings::{GumFoundImportFunc, GumImportDetails, GumImportType_GUM_IMPORT_FUNCTION};
#[cfg(any(feature = "xnu-core", feature = "linux-injected"))]
use crate::gum_injected::enumerate_exports_in_range;
#[unsafe(no_mangle)]
pub extern "C" fn gum_process_get_current_thread_id() -> GumThreadId {
unsafe { gthread::get_current_thread_id() }
kernel::current_thread_id() as GumThreadId
}
#[cfg(any(feature = "win9x", feature = "winnt", feature = "xnu-core"))]
#[unsafe(no_mangle)]
pub extern "C" fn gum_process_get_id() -> guint {
kernel::current_process_id() as guint
}
#[cfg(feature = "linux-injected")]
#[unsafe(no_mangle)]
pub extern "C" fn gum_process_get_id() -> guint {
crate::source_process_id() as guint
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_query_page_size() -> guint {
kernel::page_size() as guint
}
// Only the kernel is allowed to ask the translation control register what it was set up with,
// so a half that runs in a process is told instead.
#[cfg(target_arch = "aarch64")]
pub(crate) fn page_size_the_kernel_runs_with() -> usize {
let translation_control: u64;
unsafe {
let tcr_el1: u64;
core::arch::asm!("mrs {}, tcr_el1", out(reg) tcr_el1, options(nomem, nostack));
core::arch::asm!("mrs {}, tcr_el1", out(reg) translation_control, options(nomem, nostack));
}
let tg0 = (tcr_el1 >> 14) & 0x3;
match tg0 {
0b00 => 4096,
0b01 => 65536,
0b10 => 16384,
_ => 4096,
}
match (translation_control >> 14) & 0x3 {
0b01 => 65536,
0b10 => 16384,
_ => 4096,
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_query_rwx_support() -> GumRwxSupport {
_GumRwxSupport_GUM_RWX_NONE
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_can_remap_writable() -> gboolean {
1
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_try_remap_writable_pages(
first_page: gpointer,
n_pages: guint,
) -> gpointer {
if is_agent_slab(first_page as u64) {
return remap_agent_pages(first_page, n_pages);
}
shadow_kernel_pages(first_page, n_pages)
}
fn remap_agent_pages(first_page: gpointer, n_pages: guint) -> gpointer {
unsafe {
let page_size = gum_query_page_size() as usize;
let mut virtual_addrs = Vec::with_capacity(n_pages as usize);
let mut current_page = first_page as u64;
for _ in 0..n_pages {
virtual_addrs.push(current_page as gpointer);
current_page += page_size as u64;
}
_gum_barebone_try_remap_writable_pages(
virtual_addrs.as_ptr() as *mut *const core::ffi::c_void,
virtual_addrs.len() as guint,
)
}
}
fn shadow_kernel_pages(first_page: gpointer, n_pages: guint) -> gpointer {
unsafe {
let total = n_pages as usize * gum_query_page_size() as usize;
let buffer = crate::xnu::kalloc(SHADOW_HEADER + total);
*(buffer as *mut u64) = SHADOW_MAGIC;
*(buffer.add(8) as *mut u64) = first_page as u64;
*(buffer.add(16) as *mut u32) = n_pages;
let body = buffer.add(SHADOW_HEADER);
core::ptr::copy_nonoverlapping(first_page as *const u8, body, total);
body as gpointer
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_dispose_writable_pages(writable: gpointer, _n_pages: guint) {
if (writable as u64) < SHADOW_MIN_ADDRESS {
return;
}
unsafe {
let buffer = (writable as *mut u8).sub(SHADOW_HEADER);
if *(buffer as *const u64) != SHADOW_MAGIC {
return;
}
let first_page = *(buffer.add(8) as *const u64);
let n_pages = *(buffer.add(16) as *const u32);
let total = n_pages as usize * gum_query_page_size() as usize;
commit_kernel_patch(first_page, writable as *const u8, total);
libc::__clear_cache(first_page as *const u8, (first_page + total as u64) as *const u8);
crate::xnu::free(buffer, SHADOW_HEADER + total);
}
}
unsafe fn commit_kernel_patch(address: u64, data: *const u8, len: usize) {
unsafe {
let element_type = g_variant_type_new(c"y".as_ptr());
let bytes = g_variant_new_fixed_array(element_type, data as gconstpointer, len as gsize, 1);
g_variant_type_free(element_type);
let payload = g_variant_new(c"(t@ay)".as_ptr(), address, bytes);
let reply = host_rpc(FridaCommand::PatchCode, payload);
g_variant_unref(reply);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_try_remap_writable_pages(
addrs: *const gpointer,
n_addrs: guint,
) -> gpointer {
if !crate::transport_is_up() {
return ptr::null_mut();
}
unsafe {
let element_type = g_variant_type_new(c"t".as_ptr());
let payload = g_variant_new_fixed_array(
element_type,
addrs as gconstpointer,
n_addrs as gsize,
size_of::<u64>() as gsize,
);
g_variant_type_free(element_type);
let reply = host_rpc(FridaCommand::RemapWritablePages, payload);
let virtual_address = g_variant_get_uint64(reply);
g_variant_unref(reply);
virtual_address as gpointer
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_try_mprotect(
address: gpointer,
size: gsize,
prot: GumPageProtection,
) -> gboolean {
if !crate::transport_is_up() {
return 1;
}
unsafe {
let payload = g_variant_new(c"(ttu)".as_ptr(), address as u64, size as u64, prot as u32);
let reply = host_rpc(FridaCommand::MemoryProtect, payload);
let success = g_variant_get_boolean(reply);
g_variant_unref(reply);
if success != 0 {
flush_tlb_range(address as u64, size as u64);
}
success
}
}
// The host rewrites our page-table descriptors through the physical-memory
// bridge, which leaves this CPU's TLB holding the stale translation. Stalker
// flips a slab page RW then RX in place, so without this the freeze never takes
// effect and executing the page faults with a permission abort.
unsafe fn flush_tlb_range(address: u64, size: u64) {
unsafe {
let page_size = gum_query_page_size() as u64;
let start = address & !(page_size - 1);
let end = (address + size + page_size - 1) & !(page_size - 1);
core::arch::asm!("dsb ish", options(nostack, preserves_flags));
let mut va = start;
while va < end {
core::arch::asm!("tlbi vaae1is, {operand}", operand = in(reg) va >> 12,
options(nostack, preserves_flags));
va += page_size;
}
core::arch::asm!("dsb ish", "isb", options(nostack, preserves_flags));
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_allocate(
_address: gpointer,
size: gsize,
_alignment: gsize,
prot: GumPageProtection,
) -> gpointer {
let ptr = crate::xnu::kalloc(size as usize);
unsafe {
core::ptr::write_bytes(ptr, 0, size as usize);
if (prot & _GumPageProtection_GUM_PAGE_EXECUTE) != 0 {
register_slab(ptr as u64, size as usize);
gum_mprotect(ptr as gpointer, size, prot);
}
}
ptr as gpointer
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_free(address: gpointer, size: gsize) -> gboolean {
// Executable slabs were flipped to RX in the page tables; restore RW before returning them to
// the allocator, otherwise the reclaimed pages stay non-writable and the next consumer faults.
if is_agent_slab(address as u64) {
unsafe {
gum_mprotect(
address,
size,
(_GumPageProtection_GUM_PAGE_READ | _GumPageProtection_GUM_PAGE_WRITE)
as GumPageProtection,
);
}
unregister_slab(address as u64);
}
crate::xnu::free(address as *mut u8, size as usize);
1
#[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "arm"))]
pub(crate) fn page_size_the_kernel_runs_with() -> usize {
4096
}
#[unsafe(no_mangle)]
@@ -324,40 +109,6 @@ pub extern "C" fn gum_tls_key_set_value(key: GumTlsKey, value: gpointer) {
gthread::g_private_set(key as *mut GPrivate, value)
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_on_registry_activating(registry: *mut GumModuleRegistry) {
let kernel_base = crate::xnu::get_kernel_base();
unsafe {
let module_infos = core::ptr::addr_of!(crate::MODULE_INFO);
let module_infos = &*module_infos;
let mut i = 0;
for module_info in module_infos.iter() {
let module_base = kernel_base + module_info.offset as u64;
let module_path = if i == 0 {
"/System/Library/Kernels/kernel"
} else {
&format!(
"/System/Library/Extensions/{}.kext/{}",
module_info.name, module_info.name
)
};
let module_range = GumMemoryRange {
base_address: module_base,
size: module_info.size as u64,
};
let module = gum_native_module_new(&module_path, &module_info.version, &module_range);
gum_barebone_register_module(registry, module);
g_object_unref(module as gpointer);
i += 1;
}
}
}
#[repr(C)]
pub struct GumNativeModule {
parent: GObject,
@@ -388,7 +139,7 @@ fn gum_native_module_get_type() -> GType {
class_size: core::mem::size_of::<GObjectClass>() as u16,
base_init: None,
base_finalize: None,
class_init: Some(gum_native_module_class_init),
class_init: Some(crate::signed_to_be_called_back(gum_native_module_class_init, 0)),
class_finalize: None,
class_data: core::ptr::null(),
instance_size: core::mem::size_of::<GumNativeModule>() as u16,
@@ -400,12 +151,12 @@ fn gum_native_module_get_type() -> GType {
let new_type = g_type_register_static(g_object_get_type(), type_name, &type_info, 0);
let interface_info = _GInterfaceInfo {
interface_init: Some(gum_native_module_iface_init),
interface_init: Some(crate::signed_to_be_called_back(gum_native_module_iface_init, 0)),
interface_finalize: None,
interface_data: core::ptr::null_mut(),
};
g_type_add_interface_static(new_type, gum_module_get_type(), &interface_info);
g_type_add_interface_static(new_type, crate::bindings::gum_module_get_type(), &interface_info);
g_once_init_leave(
core::ptr::addr_of_mut!(GUM_NATIVE_MODULE_TYPE) as *mut ::core::ffi::c_void,
@@ -423,18 +174,29 @@ unsafe extern "C" fn gum_native_module_class_init(klass: gpointer, _class_data:
GUM_NATIVE_MODULE_PARENT_CLASS = g_type_class_peek_parent(klass) as *mut GObjectClass;
(*object_class).finalize = Some(gum_native_module_finalize);
(*object_class).finalize = Some(crate::signed_to_be_called_back(gum_native_module_finalize, 0));
}
}
extern "C" fn gum_native_module_iface_init(g_iface: gpointer, _iface_data: gpointer) {
unsafe {
let iface = g_iface as *mut GumModuleInterface;
(*iface).get_name = Some(gum_native_module_get_name);
(*iface).get_version = Some(gum_native_module_get_version);
(*iface).get_path = Some(gum_native_module_get_path);
(*iface).get_range = Some(gum_native_module_get_range);
(*iface).enumerate_exports = Some(gum_native_module_enumerate_exports);
(*iface).get_name = Some(crate::signed_to_be_called_back(gum_native_module_get_name, 0));
(*iface).get_version = Some(crate::signed_to_be_called_back(gum_native_module_get_version, 0));
(*iface).get_path = Some(crate::signed_to_be_called_back(gum_native_module_get_path, 0));
(*iface).get_range = Some(crate::signed_to_be_called_back(gum_native_module_get_range, 0));
(*iface).enumerate_exports = Some(crate::signed_to_be_called_back(gum_native_module_enumerate_exports, 0));
#[cfg(any(feature = "win9x", feature = "winnt"))]
{
(*iface).enumerate_imports =
Some(crate::signed_to_be_called_back(gum_native_module_enumerate_imports, 0));
}
(*iface).find_export_by_name = Some(crate::signed_to_be_called_back(gum_native_module_find_export_by_name, 0));
#[cfg(any(feature = "linux", feature = "linux-injected"))]
{
(*iface).enumerate_symbols =
Some(crate::signed_to_be_called_back(gum_native_module_enumerate_symbols, 0));
}
}
}
@@ -448,22 +210,36 @@ unsafe extern "C" fn gum_native_module_finalize(object: *mut GObject) {
}
}
fn gum_native_module_new(path: &str, version: &str, range: &GumMemoryRange) -> *mut GumModule {
pub(crate) fn gum_native_module_new(
path: &str,
version: &str,
range: &GumMemoryRange,
) -> *mut GumModule {
unsafe {
let path_cstr = CString::new(path).unwrap();
let version_cstr = CString::new(version).unwrap();
let module =
g_object_new(gum_native_module_get_type(), ptr::null()) as *mut GumNativeModule;
(*module).path = g_strdup(path_cstr.as_ptr());
(*module).name = (*module).path.add(path.rfind('/').unwrap() + 1);
(*module).version = g_strdup(version_cstr.as_ptr());
let name_offset = path.rfind(['/', '\\']).map_or(0, |at| at + 1);
(*module).name = (*module).path.add(name_offset);
(*module).version = version_copied(version);
(*module).range = *range;
module as *mut GumModule
}
}
fn version_copied(version: &str) -> *mut gchar {
if version.is_empty() {
return ptr::null_mut();
}
let copy = CString::new(version).unwrap();
unsafe { g_strdup(copy.as_ptr()) }
}
extern "C" fn gum_native_module_get_name(module: *mut GumModule) -> *const gchar {
unsafe {
let native_module = module as *mut GumNativeModule;
@@ -492,6 +268,34 @@ extern "C" fn gum_native_module_get_range(module: *mut GumModule) -> *const GumM
}
}
unsafe extern "C" fn gum_native_module_find_export_by_name(
self_: *mut GumModule,
symbol_name: *const gchar,
) -> GumAddress {
unsafe {
let module = self_ as *mut GumNativeModule;
let range = (*module).range;
let wanted = CStr::from_ptr(symbol_name);
let mut found = 0;
let mut on_export = |name: *const gchar, address: u64| {
if CStr::from_ptr(name) == wanted {
found = address;
}
found == 0
};
enumerate_exports_in_range(
range.base_address,
range.base_address + range.size as u64,
&mut on_export,
);
found
}
}
unsafe extern "C" fn gum_native_module_enumerate_exports(
self_: *mut GumModule,
func: GumFoundExportFunc,
@@ -499,134 +303,320 @@ unsafe extern "C" fn gum_native_module_enumerate_exports(
) {
unsafe {
let module = self_ as *mut GumNativeModule;
let module_range = &(*module).range;
let symbol_table = core::ptr::addr_of!(crate::SYMBOL_TABLE);
let symbol_table = &*symbol_table;
const N_EXT: u8 = 0x01; // External symbol flag
const N_TYPE: u8 = 0x0e; // Type mask
const N_SECT: u8 = 0x0e; // Defined in section
let start_address = module_range.base_address;
let end_address = module_range.base_address + module_range.size;
for symbol_ref in symbol_table.iter_symbols_in_range(start_address, end_address) {
let is_external = (symbol_ref.symbol_type() & N_EXT) != 0;
let is_defined = (symbol_ref.symbol_type() & N_TYPE) == N_SECT;
if !is_external || !is_defined {
continue;
}
let export_type = GumExportType_GUM_EXPORT_FUNCTION;
let range = (*module).range;
let mut on_export = |name: *const gchar, address: u64| {
let export_details = GumExportDetails {
type_: export_type,
name: g_strdup(symbol_ref.name_ptr()),
address: symbol_ref.address(),
type_: GumExportType_GUM_EXPORT_FUNCTION,
name,
address,
size: -1,
};
let should_continue =
func.unwrap()(&export_details as *const GumExportDetails, user_data);
if should_continue == 0 {
func.unwrap()(&export_details as *const GumExportDetails, user_data) != 0
};
enumerate_exports_in_range(
range.base_address,
range.base_address + range.size as u64,
&mut on_export,
);
}
}
#[cfg(any(feature = "win9x", feature = "winnt"))]
unsafe extern "C" fn gum_native_module_enumerate_imports(
self_: *mut GumModule,
func: GumFoundImportFunc,
user_data: gpointer,
) {
unsafe {
let module = self_ as *mut GumNativeModule;
let base = (*module).range.base_address;
let mut on_import = |library: *const gchar, name: *const gchar, slot: u64, address: u64| {
let details = GumImportDetails {
type_: GumImportType_GUM_IMPORT_FUNCTION,
name,
module: library,
address,
slot,
};
func.unwrap()(&details as *const GumImportDetails, user_data) != 0
};
enumerate_imports_in_module(base, &mut on_import);
}
}
#[cfg(any(feature = "linux", feature = "linux-injected"))]
unsafe extern "C" fn gum_native_module_enumerate_symbols(
self_: *mut GumModule,
func: GumFoundSymbolFunc,
user_data: gpointer,
) {
unsafe {
let module = self_ as *mut GumNativeModule;
let range = (*module).range;
let mut on_symbol = |name: *const gchar, address: u64, size: u64, kind: u8, global: bool| {
let details = GumSymbolDetails {
is_global: global as gboolean,
type_: match kind {
SYMBOL_IS_FUNCTION => GumSymbolType_GUM_SYMBOL_FUNCTION,
SYMBOL_IS_OBJECT => GumSymbolType_GUM_SYMBOL_OBJECT,
_ => GumSymbolType_GUM_SYMBOL_UNKNOWN,
},
section: ptr::null(),
name,
address,
size: size as gssize,
};
func.unwrap()(&details as *const GumSymbolDetails, user_data) != 0
};
enumerate_symbols_in_range(
range.base_address,
range.base_address + range.size as u64,
&mut on_symbol,
);
}
}
/// What the backends implement for `gum_native_module_enumerate_symbols`: call
/// `callback` for every symbol in `[start, end)`, stopping early when it returns
/// false.
#[cfg(any(feature = "linux", feature = "linux-injected"))]
pub(crate) type FoundSymbolCallback<'a> = dyn FnMut(*const gchar, u64, u64, u8, bool) -> bool + 'a;
#[cfg(any(feature = "linux", feature = "linux-injected"))]
const SYMBOL_IS_OBJECT: u8 = 1;
#[cfg(any(feature = "linux", feature = "linux-injected"))]
const SYMBOL_IS_FUNCTION: u8 = 2;
/// What the backends implement for `gum_native_module_enumerate_exports`: call
/// `callback` for every exported function in `[start, end)`, stopping early when
/// it returns false.
pub(crate) type FoundExportCallback<'a> = dyn FnMut(*const gchar, u64) -> bool + 'a;
#[cfg(any(feature = "win9x", feature = "winnt"))]
pub(crate) type FoundImportCallback<'a> = dyn FnMut(*const gchar, *const gchar, u64, u64) -> bool + 'a;
// Code slabs the agent allocates are normal writable RAM, so Gum can keep a real writable alias to
// them across the slab's lifetime. Pre-existing kernel text is write-protected — CTRR-locked on
// Apple silicon, STRICT_KERNEL_RWX on Linux — and can only be modified through a separate path, so
// we treat it differently. We distinguish the two by tracking the executable ranges we hand out.
static SLAB_LOCK: AtomicU32 = AtomicU32::new(0);
static mut SLABS: Vec<(u64, u64)> = Vec::new();
fn slab_lock() {
while SLAB_LOCK
.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
kernel::yield_now();
}
}
fn slab_unlock() {
SLAB_LOCK.store(0, Ordering::Release);
}
pub(crate) fn register_slab(start: u64, size: usize) {
slab_lock();
unsafe {
(*core::ptr::addr_of_mut!(SLABS)).push((start, start + size as u64));
}
slab_unlock();
}
pub(crate) fn unregister_slab(start: u64) {
slab_lock();
unsafe {
(*core::ptr::addr_of_mut!(SLABS)).retain(|&(begin, _)| begin != start);
}
slab_unlock();
}
pub(crate) fn is_agent_slab(address: u64) -> bool {
slab_lock();
let found = unsafe {
(*core::ptr::addr_of!(SLABS))
.iter()
.any(|&(begin, end)| address >= begin && address < end)
};
slab_unlock();
found
}
// The host gives the symbol table, thus this makes a name from an address.
#[cfg(feature = "blob")]
mod symbolication {
use super::*;
use crate::bindings::{
GArray, GumAddress, GumDebugSymbolDetails, g_array_append_vals, g_array_new, gboolean,
gconstpointer,
};
use core::ffi::CStr;
#[unsafe(no_mangle)]
pub extern "C" fn gum_symbol_name_from_address(address: gpointer) -> *mut gchar {
match closest_to(address) {
Some(symbol) => unsafe { g_strdup(symbol.name_ptr()) },
None => ptr::null_mut(),
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_symbol_details_from_address(
address: gpointer,
details: *mut GumDebugSymbolDetails,
) -> gboolean {
let Some(symbol) = closest_to(address) else {
return 0;
};
let details = unsafe { &mut *details };
*details = unsafe { core::mem::zeroed() };
details.address = address as u64;
put_text(symbol.name(), &mut details.symbol_name);
if let Some(module) = module_containing(address as u64) {
put_text(&module.name, &mut details.module_name);
}
1
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_module_find_global_export_by_name(name: *const gchar) -> GumAddress {
let Some(name) = text_of(name) else {
return 0;
};
#[cfg(any(feature = "win9x", feature = "winnt"))]
if crate::running_in_a_process() {
return export_of_a_process_module(name);
}
#[cfg(feature = "xnu-core")]
if crate::xnu::in_copy() {
return crate::xnu_mapped::export_named(name);
}
#[cfg(feature = "linux-injected")]
if crate::kernel::in_copy() {
return crate::kernel::export_named(name);
}
let table = unsafe { &*ptr::addr_of!(crate::SYMBOL_TABLE) };
match table.find_symbol_by_name(name) {
Some(symbol) => symbol.address(),
None => 0,
}
}
// A copy runs in a process, thus a name without a module means one of the modules of that
// process.
#[cfg(any(feature = "win9x", feature = "winnt"))]
fn export_of_a_process_module(wanted: &str) -> GumAddress {
let mut found = 0;
for module in crate::kernel::enumerate_modules() {
crate::kernel::enumerate_exports(module.base as _, &mut |name, address| {
if text_of(name as *const gchar) == Some(wanted) {
found = address;
}
found == 0
});
if found != 0 {
break;
}
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_symbol_details_from_address(
address: gpointer,
details: *mut GumDebugSymbolDetails,
) -> gboolean {
unsafe {
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
if let Some(symbol) = table.find_symbol_by_address(address as u64) {
let name_bytes = symbol.name().as_bytes();
let copy_len = core::cmp::min(name_bytes.len(), 2048);
core::ptr::copy_nonoverlapping(
name_bytes.as_ptr(),
(*details).symbol_name.as_mut_ptr() as *mut u8,
copy_len,
found
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_find_function(name: *const gchar) -> gpointer {
let Some(name) = text_of(name) else {
return ptr::null_mut();
};
let table = unsafe { &*ptr::addr_of!(crate::SYMBOL_TABLE) };
match table.find_symbol_by_name(name) {
Some(symbol) => symbol.address() as gpointer,
None => ptr::null_mut(),
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_find_functions_named(name: *const gchar) -> *mut GArray {
let table = unsafe { &*ptr::addr_of!(crate::SYMBOL_TABLE) };
let found = text_of(name)
.map(|name| table.find_symbols_by_name(name).map(|s| s.address()).collect())
.unwrap_or_else(Vec::new);
pointer_array_of(&found)
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_find_functions_matching(pattern: *const gchar) -> *mut GArray {
let table = unsafe { &*ptr::addr_of!(crate::SYMBOL_TABLE) };
let found = text_of(pattern)
.map(|pattern| {
table
.find_symbols_matching_glob(pattern)
.map(|s| s.address())
.collect()
})
.unwrap_or_else(Vec::new);
pointer_array_of(&found)
}
fn closest_to(address: gpointer) -> Option<crate::symbols::SymbolRef<'static>> {
let table = unsafe { &*ptr::addr_of!(crate::SYMBOL_TABLE) };
table.find_closest_symbol_by_address(address as u64)
}
fn module_containing(address: u64) -> Option<&'static crate::ModuleInfo> {
let modules = unsafe { &*ptr::addr_of!(crate::MODULE_INFO) };
modules.iter().find(|m| {
address >= m.offset as u64 && address < m.offset as u64 + m.size as u64
})
}
fn pointer_array_of(addresses: &[u64]) -> *mut GArray {
let pointers: Vec<gpointer> = addresses.iter().map(|a| *a as gpointer).collect();
unsafe {
let array = g_array_new(0, 0, core::mem::size_of::<gpointer>() as guint);
g_array_append_vals(
array,
pointers.as_ptr() as gconstpointer,
pointers.len() as guint,
);
(*details).symbol_name[copy_len] = 0;
(*details).address = symbol.address();
(*details).module_name[0] = 0;
(*details).file_name[0] = 0;
(*details).line_number = 0;
(*details).column = 0;
return 1;
array
}
0
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_symbol_name_from_address(address: gpointer) -> *mut gchar {
unsafe {
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
let name_ptr = table.find_symbol_name_ptr_by_address(address as u64);
g_strdup(name_ptr as *const gchar)
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_find_function(name: *const gchar) -> gpointer {
unsafe {
let target_name = CStr::from_ptr(name).to_string_lossy();
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
if let Some(symbol) = table.find_symbol_by_name(&target_name) {
return symbol.address() as gpointer;
fn text_of(value: *const gchar) -> Option<&'static str> {
if value.is_null() {
return None;
}
core::ptr::null_mut()
unsafe { CStr::from_ptr(value) }.to_str().ok()
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_find_functions_named(name: *const gchar) -> *mut GArray {
unsafe {
let array = g_array_new(0, 0, core::mem::size_of::<gpointer>() as guint);
let target_name = CStr::from_ptr(name).to_string_lossy();
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
let symbols = table.find_symbols_by_name(&target_name);
for symbol in symbols {
let addr = symbol.address() as gpointer;
g_array_append_vals(array, &addr as *const gpointer as gconstpointer, 1);
fn put_text(text: &str, out: &mut [gchar]) {
let limit = out.len() - 1;
for (i, byte) in text.bytes().take(limit).enumerate() {
out[i] = byte as gchar;
}
array
out[text.len().min(limit)] = 0;
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_find_functions_matching(pattern: *const gchar) -> *mut GArray {
unsafe {
let array = g_array_new(0, 0, core::mem::size_of::<gpointer>() as guint);
let glob_pattern = CStr::from_ptr(pattern).to_string_lossy();
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
let symbols = table.find_symbols_matching_glob(&glob_pattern);
for symbol in symbols {
let addr = symbol.address() as gpointer;
g_array_append_vals(array, &addr as *const gpointer as gconstpointer, 1);
}
array
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_load_symbols(_path: *const gchar) -> gboolean {
0
}
+393
View File
@@ -0,0 +1,393 @@
use alloc::ffi::CString;
use alloc::string::String;
use core::ffi::{CStr, c_char};
use crate::bindings::{
GError, GumScriptApiValue, _GumScriptApiType_GUM_SCRIPT_API_BOOLEAN,
_GumScriptApiType_GUM_SCRIPT_API_STRING, _GumScriptApiType_GUM_SCRIPT_API_UINT, gboolean,
gpointer, gum_script_api_add_function, gum_script_api_new, gum_script_api_registry_add,
gum_script_api_registry_obtain, gum_script_api_set_prelude, gum_script_api_unref,
};
use crate::linux::layout::{
enumerate_constants_in, enumerate_fields_in, enumerate_parameters_in, field_offset_in,
find_constant, name_of_type, resolve_struct, size_of_struct, size_of_type, types_are_described,
};
pub fn publish() {
unsafe {
let api = gum_script_api_new(c"Btf".as_ptr());
gum_script_api_add_function(
api,
c"_findStruct".as_ptr(),
c"s".as_ptr(),
_GumScriptApiType_GUM_SCRIPT_API_UINT,
Some(crate::signed_to_be_called_back(on_find_struct, 0)),
core::ptr::null_mut(),
);
gum_script_api_add_function(
api,
c"_sizeOf".as_ptr(),
c"u".as_ptr(),
_GumScriptApiType_GUM_SCRIPT_API_UINT,
Some(crate::signed_to_be_called_back(on_size_of, 0)),
core::ptr::null_mut(),
);
gum_script_api_add_function(
api,
c"_findOffsetOf".as_ptr(),
c"us".as_ptr(),
_GumScriptApiType_GUM_SCRIPT_API_UINT,
Some(crate::signed_to_be_called_back(on_find_offset, 0)),
core::ptr::null_mut(),
);
gum_script_api_add_function(
api,
c"_fieldsOf".as_ptr(),
c"u".as_ptr(),
_GumScriptApiType_GUM_SCRIPT_API_STRING,
Some(crate::signed_to_be_called_back(on_fields_of, 0)),
core::ptr::null_mut(),
);
gum_script_api_add_function(
api,
c"_findConstant".as_ptr(),
c"s".as_ptr(),
_GumScriptApiType_GUM_SCRIPT_API_STRING,
Some(crate::signed_to_be_called_back(on_find_constant, 0)),
core::ptr::null_mut(),
);
gum_script_api_add_function(
api,
c"_constantsOf".as_ptr(),
c"s".as_ptr(),
_GumScriptApiType_GUM_SCRIPT_API_STRING,
Some(crate::signed_to_be_called_back(on_constants_of, 0)),
core::ptr::null_mut(),
);
gum_script_api_add_function(
api,
c"_signatureOf".as_ptr(),
c"s".as_ptr(),
_GumScriptApiType_GUM_SCRIPT_API_STRING,
Some(crate::signed_to_be_called_back(on_signature_of, 0)),
core::ptr::null_mut(),
);
gum_script_api_add_function(
api,
c"_isAvailable".as_ptr(),
c"".as_ptr(),
_GumScriptApiType_GUM_SCRIPT_API_BOOLEAN,
Some(crate::signed_to_be_called_back(on_is_available, 0)),
core::ptr::null_mut(),
);
gum_script_api_set_prelude(api, PRELUDE.as_ptr());
gum_script_api_registry_add(gum_script_api_registry_obtain(), api);
gum_script_api_unref(api);
}
}
unsafe extern "C" fn on_find_struct(
args: *const GumScriptApiValue,
retval: *mut GumScriptApiValue,
_user_data: gpointer,
_error: *mut *mut GError,
) -> gboolean {
let container = unsafe { text_of(args, 0) };
let handle = resolve_struct(container).map_or(NOTHING, |handle| (handle + 1) as u32);
unsafe { (*retval).__bindgen_anon_1.u = handle };
1
}
unsafe extern "C" fn on_size_of(
args: *const GumScriptApiValue,
retval: *mut GumScriptApiValue,
_user_data: gpointer,
_error: *mut *mut GError,
) -> gboolean {
let handle = unsafe { handle_of(args, 0) };
unsafe { (*retval).__bindgen_anon_1.u = size_of_struct(handle) as u32 };
1
}
unsafe extern "C" fn on_find_offset(
args: *const GumScriptApiValue,
retval: *mut GumScriptApiValue,
_user_data: gpointer,
_error: *mut *mut GError,
) -> gboolean {
let handle = unsafe { handle_of(args, 0) };
let field = unsafe { text_of(args, 1) };
let offset = field_offset_in(handle, field).map_or(NOWHERE, |offset| offset as u32);
unsafe { (*retval).__bindgen_anon_1.u = offset };
1
}
unsafe extern "C" fn on_is_available(
_args: *const GumScriptApiValue,
retval: *mut GumScriptApiValue,
_user_data: gpointer,
_error: *mut *mut GError,
) -> gboolean {
unsafe { (*retval).__bindgen_anon_1.b = types_are_described() as gboolean };
1
}
unsafe extern "C" fn on_fields_of(
args: *const GumScriptApiValue,
retval: *mut GumScriptApiValue,
_user_data: gpointer,
_error: *mut *mut GError,
) -> gboolean {
let handle = unsafe { handle_of(args, 0) };
let mut described = String::from("{");
let mut separator = "";
enumerate_fields_in(handle, &mut |name, field| {
described.push_str(&alloc::format!(
"{separator}\"{name}\":{{\"offset\":{},\"size\":{},\"type\":\"{}\"",
field.offset,
size_of_type(field.id),
name_of_type(field.id)
));
if field.bit_size != 0 {
described.push_str(&alloc::format!(
",\"bitOffset\":{},\"bitSize\":{}",
field.bit_offset, field.bit_size
));
}
described.push('}');
separator = ",";
true
});
described.push('}');
unsafe { answer_with(retval, described) };
1
}
unsafe extern "C" fn on_find_constant(
args: *const GumScriptApiValue,
retval: *mut GumScriptApiValue,
_user_data: gpointer,
_error: *mut *mut GError,
) -> gboolean {
let name = unsafe { text_of(args, 0) };
let described = find_constant(name).map_or_else(String::new, |value| alloc::format!("{value}"));
unsafe { answer_with(retval, described) };
1
}
unsafe extern "C" fn on_constants_of(
args: *const GumScriptApiValue,
retval: *mut GumScriptApiValue,
_user_data: gpointer,
_error: *mut *mut GError,
) -> gboolean {
let container = unsafe { text_of(args, 0) };
let mut described = String::from("{");
let mut separator = "";
let found = enumerate_constants_in(container, &mut |name, value| {
described.push_str(&alloc::format!("{separator}\"{name}\":{value}"));
separator = ",";
});
described.push('}');
unsafe { answer_with(retval, if found { described } else { String::new() }) };
1
}
unsafe extern "C" fn on_signature_of(
args: *const GumScriptApiValue,
retval: *mut GumScriptApiValue,
_user_data: gpointer,
_error: *mut *mut GError,
) -> gboolean {
let name = unsafe { text_of(args, 0) };
let mut parameters = String::from("[");
let mut separator = "";
let returned = enumerate_parameters_in(name, &mut |name, kind| {
parameters.push_str(&alloc::format!(
"{separator}{{\"name\":\"{name}\",\"type\":\"{kind}\"}}"
));
separator = ",";
});
parameters.push(']');
let described = match returned {
Some(returned) => {
alloc::format!("{{\"returns\":\"{returned}\",\"parameters\":{parameters}}}")
}
None => String::new(),
};
unsafe { answer_with(retval, described) };
1
}
unsafe fn answer_with(retval: *mut GumScriptApiValue, described: String) {
unsafe {
let holder = (&raw mut LAST_ANSWER).as_mut().unwrap();
*holder = Some(CString::new(described).unwrap());
(*retval).__bindgen_anon_1.s = holder.as_ref().unwrap().as_ptr();
}
}
static mut LAST_ANSWER: Option<CString> = None;
const NOTHING: u32 = 0;
const NOWHERE: u32 = u32::MAX;
unsafe fn handle_of(args: *const GumScriptApiValue, index: usize) -> usize {
unsafe { (*args.add(index)).__bindgen_anon_1.u as usize - 1 }
}
unsafe fn text_of(args: *const GumScriptApiValue, index: usize) -> &'static str {
unsafe {
CStr::from_ptr((*args.add(index)).__bindgen_anon_1.s as *const c_char)
.to_str()
.unwrap_or("")
}
}
const PRELUDE: &CStr = c"
(() => {
const { _findStruct, _sizeOf, _findOffsetOf, _fieldsOf, _findConstant, _constantsOf,
_signatureOf, _isAvailable } = Btf;
const NOWHERE = 0xffffffff;
class BtfStruct {
constructor(name, handle) {
this.name = name;
this.handle = handle;
this.size = _sizeOf(handle);
this.known = new Map();
}
getOffsetOf(field) {
const offset = this.findOffsetOf(field);
if (offset === null)
throw new Error(`${this.name}: unable to find field '${field}'`);
return offset;
}
findOffsetOf(field) {
let offset = this.known.get(field);
if (offset === undefined) {
offset = _findOffsetOf(this.handle, field);
if (offset === NOWHERE)
return null;
this.known.set(field, offset);
}
return offset;
}
get fields() {
const fields = JSON.parse(_fieldsOf(this.handle));
Object.defineProperty(this, 'fields', { value: fields });
return fields;
}
toString() {
return `BtfStruct(${this.name}, size: ${this.size})`;
}
}
const resolved = new Map();
const functions = new Map();
Btf.available = _isAvailable();
Btf.getStruct = name => {
const described = Btf.findStruct(name);
if (described === null)
throw new Error(`unable to find struct '${name}'`);
return described;
};
Btf.findStruct = name => {
let described = resolved.get(name);
if (described === undefined) {
const handle = _findStruct(name);
if (handle === 0)
return null;
described = new BtfStruct(name, handle);
resolved.set(name, described);
}
return described;
};
Btf.getConstant = name => {
const value = Btf.findConstant(name);
if (value === null)
throw new Error(`unable to find constant '${name}'`);
return value;
};
Btf.findConstant = name => {
const value = _findConstant(name);
if (value === '')
return null;
return numberFrom(value);
};
Btf.getEnum = name => {
const constants = Btf.findEnum(name);
if (constants === null)
throw new Error(`unable to find enum '${name}'`);
return constants;
};
Btf.findEnum = name => {
const constants = _constantsOf(name);
if (constants === '')
return null;
return JSON.parse(constants);
};
Btf.getFunction = name => {
const signature = Btf.findFunction(name);
if (signature === null)
throw new Error(`unable to find function '${name}'`);
return signature;
};
Btf.findFunction = name => {
let signature = functions.get(name);
if (signature === undefined) {
const described = _signatureOf(name);
if (described === '')
return null;
signature = JSON.parse(described);
functions.set(name, signature);
}
return signature;
};
function numberFrom(text) {
const value = parseInt(text);
return Number.isSafeInteger(value) ? value : int64(text);
}
})();
";
File diff suppressed because it is too large. Load diff
+216
View File
@@ -0,0 +1,216 @@
// Linux half of Gum's platform backend. Everything is done locally: we are the
// kernel, so page permissions go through set_memory_*(), writable aliases of
// write-protected text come from vmap(), and symbols come from kallsyms.
use crate::{
bindings::{
_GumPageProtection_GUM_PAGE_EXECUTE, _GumPageProtection_GUM_PAGE_READ,
_GumPageProtection_GUM_PAGE_WRITE, _GumRwxSupport_GUM_RWX_NONE, GArray,
GumDebugSymbolDetails, GumModuleRegistry, GumPageProtection, GumRwxSupport,
g_array_append_vals, g_array_new, g_pattern_spec_free, g_pattern_spec_match_string,
g_pattern_spec_new, g_strdup, gboolean, gchar, gconstpointer, gpointer, gsize, guint,
gum_mprotect,
},
gum::{self, FoundExportCallback},
kernel,
};
use core::ffi::CStr;
use core::ptr;
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_query_platform() -> *const crate::bindings::gchar {
c"linux".as_ptr() as *const crate::bindings::gchar
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_query_stack_size() -> gsize {
crate::linux::stack_headroom() as gsize
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_query_rwx_support() -> GumRwxSupport {
_GumRwxSupport_GUM_RWX_NONE
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_can_remap_writable() -> gboolean {
1
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_try_remap_writable_pages(
first_page: gpointer,
n_pages: guint,
) -> gpointer {
kernel::remap_writable(first_page as u64, n_pages) as gpointer
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_dispose_writable_pages(writable: gpointer, _n_pages: guint) {
kernel::unmap_writable(writable as *mut core::ffi::c_void);
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_try_mprotect(
address: gpointer,
size: gsize,
prot: GumPageProtection,
) -> gboolean {
if kernel::protect(address as u64, size as usize, prot as u32) {
1
} else {
0
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_allocate(
_address: gpointer,
size: gsize,
_alignment: gsize,
prot: GumPageProtection,
) -> gpointer {
let ptr = kernel::alloc_code(size as usize);
unsafe {
core::ptr::write_bytes(ptr, 0, size as usize);
if (prot & _GumPageProtection_GUM_PAGE_EXECUTE) != 0 {
gum::register_slab(ptr as u64, size as usize);
gum_mprotect(ptr as gpointer, size, prot);
}
}
ptr as gpointer
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_free(address: gpointer, size: gsize) -> gboolean {
// vfree() wants the range writable again: the pages go back to the vmalloc
// allocator, and leaving them RX would fault the next consumer.
if gum::is_agent_slab(address as u64) {
unsafe {
gum_mprotect(
address,
size,
(_GumPageProtection_GUM_PAGE_READ | _GumPageProtection_GUM_PAGE_WRITE)
as GumPageProtection,
);
}
gum::unregister_slab(address as u64);
}
kernel::free_code(address as *mut u8, size as usize);
1
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_on_registry_activating(registry: *mut GumModuleRegistry) {
crate::gum_modules::publish(registry);
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_on_registry_deactivating(_registry: *mut GumModuleRegistry) {
crate::gum_modules::unpublish();
}
pub(crate) unsafe fn enumerate_exports_in_range(
start_address: u64,
end_address: u64,
callback: &mut FoundExportCallback<'_>,
) {
if crate::gum_modules::enumerate_exports_in_module(start_address, callback) {
return;
}
kernel::enumerate_symbols(&mut |name, address| {
if address < start_address || address >= end_address {
return true;
}
callback(name.as_ptr() as *const gchar, address)
});
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_symbol_details_from_address(
address: gpointer,
details: *mut GumDebugSymbolDetails,
) -> gboolean {
let Some((name, symbol_address)) = kernel::symbol_from_address(address as u64) else {
return 0;
};
unsafe {
let name_bytes = name.as_bytes();
let copy_len = core::cmp::min(name_bytes.len(), (*details).symbol_name.len() - 1);
core::ptr::copy_nonoverlapping(
name_bytes.as_ptr(),
(*details).symbol_name.as_mut_ptr() as *mut u8,
copy_len,
);
(*details).symbol_name[copy_len] = 0;
(*details).address = symbol_address;
(*details).module_name[0] = 0;
(*details).file_name[0] = 0;
(*details).line_number = 0;
(*details).column = 0;
}
1
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_symbol_name_from_address(address: gpointer) -> *mut gchar {
let Some((name, _)) = kernel::symbol_from_address(address as u64) else {
return ptr::null_mut();
};
let mut name = name;
name.push('\0');
unsafe { g_strdup(name.as_ptr() as *const gchar) }
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_find_function(name: *const gchar) -> gpointer {
let address = kernel::find_function(unsafe { CStr::from_ptr(name) });
address as gpointer
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_find_functions_named(name: *const gchar) -> *mut GArray {
let wanted = unsafe { CStr::from_ptr(name) };
collect_addresses(&mut |candidate, _address| candidate == wanted)
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_find_functions_matching(pattern: *const gchar) -> *mut GArray {
unsafe {
let spec = g_pattern_spec_new(pattern);
let array = collect_addresses(&mut |candidate, _address| {
g_pattern_spec_match_string(spec, candidate.as_ptr() as *const gchar) != 0
});
g_pattern_spec_free(spec);
array
}
}
fn collect_addresses(predicate: &mut dyn FnMut(&CStr, u64) -> bool) -> *mut GArray {
unsafe {
let array = g_array_new(0, 0, core::mem::size_of::<gpointer>() as guint);
kernel::enumerate_symbols(&mut |name, address| {
if predicate(name, address) {
let addr = address as gpointer;
g_array_append_vals(array, &addr as *const gpointer as gconstpointer, 1);
}
true
});
array
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_load_symbols(_path: *const gchar) -> gboolean {
0
}
+98
View File
@@ -0,0 +1,98 @@
use alloc::format;
use crate::bindings::{
GumMemoryRange, GumModuleRegistry, g_object_unref, gpointer, gsize,
gum_barebone_register_module, gum_barebone_unregister_module,
};
use crate::gum::{self, FoundExportCallback, FoundSymbolCallback};
use crate::kernel;
use crate::linux::{LoadedModule, ModuleEvent};
pub fn publish(registry: *mut GumModuleRegistry) {
unsafe { REGISTRY = registry };
let base = kernel::get_kernel_base();
if base != 0 {
register(registry, KERNEL_PATH, "", base, kernel::get_kernel_size());
}
for m in kernel::enumerate_modules() {
register(registry, &path_of(&m.name), &m.version, m.base, m.size);
}
kernel::watch_modules(on_module_event);
}
pub fn enumerate_symbols_in_range(
start_address: u64,
end_address: u64,
callback: &mut FoundSymbolCallback<'_>,
) {
#[cfg(feature = "linux-injected")]
if kernel::in_copy() {
kernel::enumerate_symbols_in_range(start_address, end_address, callback);
return;
}
kernel::enumerate_module_symbols(start_address, &mut |name, address, size, kind, global| {
if address < start_address || address >= end_address {
return true;
}
callback(name as *const _, address, size, kind, global)
});
}
pub fn enumerate_exports_in_module(
start_address: u64,
callback: &mut FoundExportCallback<'_>,
) -> bool {
kernel::enumerate_module_exports(start_address, &mut |name, address| {
callback(name as *const _, address)
})
}
pub fn unpublish() {
kernel::unwatch_modules();
unsafe { REGISTRY = core::ptr::null_mut() };
}
fn on_module_event(kind: ModuleEvent, m: LoadedModule) {
let registry = unsafe { REGISTRY };
if registry.is_null() {
return;
}
match kind {
ModuleEvent::Loaded => register(registry, &path_of(&m.name), &m.version, m.base, m.size),
ModuleEvent::Unloaded => unsafe { gum_barebone_unregister_module(registry, m.base) },
}
}
fn register(
registry: *mut GumModuleRegistry,
path: &str,
version: &str,
base: u64,
size: u64,
) {
unsafe {
let range = GumMemoryRange {
base_address: base,
size: size as gsize,
};
let module = gum::gum_native_module_new(path, version, &range);
gum_barebone_register_module(registry, module);
g_object_unref(module as gpointer);
}
}
fn path_of(name: &str) -> alloc::string::String {
format!("{}{}{}", MODULE_DIRECTORY, name, MODULE_SUFFIX)
}
static mut REGISTRY: *mut GumModuleRegistry = core::ptr::null_mut();
const KERNEL_PATH: &str = "/boot/vmlinux";
const MODULE_DIRECTORY: &str = "/lib/modules/";
const MODULE_SUFFIX: &str = ".ko";
+900
View File
@@ -0,0 +1,900 @@
// The Windows part of Gum's platform backend, for Win9x and NT. Kernel code is writable on
// both, thus this backend needs no alias. Only the position of the modules is different.
use crate::{
bindings::{
_GumPageProtection_GUM_PAGE_EXECUTE, _GumRwxSupport_GUM_RWX_FULL, GError,
GIOErrorEnum_G_IO_ERROR_BROKEN_PIPE, GIOErrorEnum_G_IO_ERROR_FAILED,
GIOErrorEnum_G_IO_ERROR_INVALID_ARGUMENT, GIOErrorEnum_G_IO_ERROR_NOT_FOUND,
GIOErrorEnum_G_IO_ERROR_PERMISSION_DENIED, GumBareboneStreamOps, GumMemoryRange,
GumModuleRegistry, GumPageProtection, GumRwxSupport, g_free, g_io_error_quark, g_malloc,
g_object_unref, g_set_error_literal, g_utf8_to_utf16, g_utf16_to_utf8, gboolean, gchar,
gint, gpointer, gsize, gssize, guint, gum_barebone_register_module, gum_mprotect,
GumFoundRangeFunc, GumFoundThreadFunc, GumModifyThreadFlags, GumModifyThreadFunc,
GumRangeDetails, GumThreadDetails, GumThreadFlags, GumThreadId, GumThreadRegistry,
gconstpointer, gum_thread_details_copy,
},
gum::{self, FoundExportCallback, FoundImportCallback},
kernel,
};
use crate::bindings::{GumCpuContext, GumThreadFlags_GUM_THREAD_FLAGS_CPU_CONTEXT};
use alloc::ffi::CString;
#[cfg(not(feature = "winnt"))]
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use core::ptr;
#[cfg(target_arch = "x86")]
macro_rules! windows_fn {
($($argument:ty),* $(,)?) => { unsafe extern "stdcall" fn($($argument),*) };
($($argument:ty),* $(,)? => $result:ty) => {
unsafe extern "stdcall" fn($($argument),*) -> $result
};
}
#[cfg(target_arch = "x86_64")]
macro_rules! windows_fn {
($($argument:ty),* $(,)?) => { unsafe extern "win64" fn($($argument),*) };
($($argument:ty),* $(,)? => $result:ty) => {
unsafe extern "win64" fn($($argument),*) -> $result
};
}
#[cfg(target_arch = "aarch64")]
macro_rules! windows_fn {
($($argument:ty),* $(,)?) => { unsafe extern "C" fn($($argument),*) };
($($argument:ty),* $(,)? => $result:ty) => {
unsafe extern "C" fn($($argument),*) -> $result
};
}
#[allow(unused_imports)]
pub(crate) use windows_fn;
pub struct FileApi {
pub read_file: windows_fn!(usize, *mut u8, u32, *mut u32, usize => i32),
pub write_file: windows_fn!(usize, *const u8, u32, *mut u32, usize => i32),
pub close_handle: windows_fn!(usize => i32),
pub format_message: windows_fn!(u32, usize, u32, u32, *mut u8, u32, usize => u32),
}
pub struct TextApi {
pub multi_byte_to_wide_char: windows_fn!(u32, u32, *const u8, i32, *mut u16, i32 => i32),
pub wide_char_to_multi_byte:
windows_fn!(u32, u32, *const u16, i32, *mut u8, i32, usize, usize => i32),
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_query_platform() -> *const crate::bindings::gchar {
c"windows".as_ptr() as *const crate::bindings::gchar
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_thread_get_system_error() -> crate::bindings::gint {
kernel::thread_error() as crate::bindings::gint
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_thread_set_system_error(value: crate::bindings::gint) {
kernel::set_thread_error(value as u32)
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_ansi_string_to_utf8(str_ansi: *const gchar, length: gint) -> *mut gchar {
let ansi = unsafe { bytes_of(str_ansi as *const u8, length) };
match kernel::text_api() {
Some(api) => ansi_to_utf8(api, ansi),
None => handed_to_glib(ansi.iter().map(|&byte| byte as char).collect::<String>().as_bytes()),
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_ansi_string_from_utf8(str_utf8: *const gchar) -> *mut gchar {
match kernel::text_api() {
Some(api) => ansi_from_utf8(api, str_utf8),
None => {
let utf8 = unsafe { core::ffi::CStr::from_ptr(str_utf8) }.to_str().unwrap();
let latin1: Vec<u8> =
utf8.chars().map(|c| if (c as u32) <= 0xff { c as u8 } else { b'?' }).collect();
handed_to_glib(&latin1)
}
}
}
unsafe fn bytes_of<'a>(str: *const u8, length: gint) -> &'a [u8] {
let length = if length < 0 {
unsafe { core::ffi::CStr::from_ptr(str as *const core::ffi::c_char) }.to_bytes().len()
} else {
length as usize
};
unsafe { core::slice::from_raw_parts(str, length) }
}
fn ansi_to_utf8(api: &TextApi, ansi: &[u8]) -> *mut gchar {
let count = unsafe {
(api.multi_byte_to_wide_char)(CP_ACP, 0, ansi.as_ptr(), ansi.len() as i32,
ptr::null_mut(), 0)
};
let mut wide = alloc::vec![0u16; count as usize];
unsafe {
(api.multi_byte_to_wide_char)(CP_ACP, 0, ansi.as_ptr(), ansi.len() as i32,
wide.as_mut_ptr(), count);
g_utf16_to_utf8(wide.as_ptr(), count as _, ptr::null_mut(), ptr::null_mut(),
ptr::null_mut())
}
}
fn ansi_from_utf8(api: &TextApi, utf8: *const gchar) -> *mut gchar {
unsafe {
let wide = g_utf8_to_utf16(utf8, -1, ptr::null_mut(), ptr::null_mut(), ptr::null_mut());
let size = (api.wide_char_to_multi_byte)(CP_ACP, 0, wide, -1, ptr::null_mut(), 0, 0, 0);
let ansi = g_malloc(size as gsize) as *mut u8;
(api.wide_char_to_multi_byte)(CP_ACP, 0, wide, -1, ansi, size, 0, 0);
g_free(wide as gpointer);
ansi as *mut gchar
}
}
fn handed_to_glib(bytes: &[u8]) -> *mut gchar {
unsafe {
let copy = g_malloc((bytes.len() + 1) as gsize) as *mut u8;
ptr::copy_nonoverlapping(bytes.as_ptr(), copy, bytes.len());
*copy.add(bytes.len()) = 0;
copy as *mut gchar
}
}
const CP_ACP: u32 = 0;
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_query_stream_ops() -> *const GumBareboneStreamOps {
if kernel::in_copy() { &STREAM_OPS } else { ptr::null() }
}
static STREAM_OPS: GumBareboneStreamOps = GumBareboneStreamOps {
poll: Some(kernel::poll_handles),
read: Some(read_handle),
write: Some(write_handle),
close: Some(close_handle),
};
unsafe extern "C" fn read_handle(handle: gpointer, buffer: gpointer, count: gsize,
error: *mut *mut GError) -> gssize {
let mut transferred = 0u32;
let succeeded = unsafe {
(kernel::file_api().read_file)(handle as usize, buffer as *mut u8, count as u32,
&mut transferred, 0)
};
if succeeded == 0 {
return failed(error);
}
transferred as gssize
}
unsafe extern "C" fn write_handle(handle: gpointer, buffer: gconstpointer, count: gsize,
error: *mut *mut GError) -> gssize {
let mut transferred = 0u32;
let succeeded = unsafe {
(kernel::file_api().write_file)(handle as usize, buffer as *const u8, count as u32,
&mut transferred, 0)
};
if succeeded == 0 {
return failed(error);
}
transferred as gssize
}
unsafe extern "C" fn close_handle(handle: gpointer, error: *mut *mut GError) -> gboolean {
let succeeded = unsafe { (kernel::file_api().close_handle)(handle as usize) };
if succeeded == 0 {
failed(error);
return 0;
}
1
}
fn failed(error: *mut *mut GError) -> gssize {
let code = kernel::thread_error();
let message = CString::new(describe_error(code)).unwrap();
unsafe {
g_set_error_literal(error, g_io_error_quark(), io_error_from_win32(code) as gint,
message.as_ptr())
};
-1
}
fn describe_error(code: u32) -> String {
let mut ansi = [0u8; 512];
let length = unsafe {
(kernel::file_api().format_message)(
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, 0, code, 0,
ansi.as_mut_ptr(), ansi.len() as u32, 0)
};
let utf8 = ansi_to_utf8(kernel::text_api().unwrap(), &ansi[..length as usize]);
let message = unsafe { core::ffi::CStr::from_ptr(utf8) }.to_str().unwrap().trim_end();
let described = String::from(message);
unsafe { g_free(utf8 as gpointer) };
described
}
fn io_error_from_win32(code: u32) -> u32 {
match code {
ERROR_FILE_NOT_FOUND | ERROR_PATH_NOT_FOUND => GIOErrorEnum_G_IO_ERROR_NOT_FOUND,
ERROR_ACCESS_DENIED => GIOErrorEnum_G_IO_ERROR_PERMISSION_DENIED,
ERROR_INVALID_HANDLE | ERROR_INVALID_PARAMETER => GIOErrorEnum_G_IO_ERROR_INVALID_ARGUMENT,
ERROR_BROKEN_PIPE => GIOErrorEnum_G_IO_ERROR_BROKEN_PIPE,
_ => GIOErrorEnum_G_IO_ERROR_FAILED,
}
}
const FORMAT_MESSAGE_IGNORE_INSERTS: u32 = 0x200;
const FORMAT_MESSAGE_FROM_SYSTEM: u32 = 0x1000;
const ERROR_FILE_NOT_FOUND: u32 = 2;
const ERROR_PATH_NOT_FOUND: u32 = 3;
const ERROR_ACCESS_DENIED: u32 = 5;
const ERROR_INVALID_HANDLE: u32 = 6;
const ERROR_INVALID_PARAMETER: u32 = 87;
const ERROR_BROKEN_PIPE: u32 = 109;
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_query_stack_size() -> crate::bindings::gsize {
if crate::on_js_thread() {
kernel::THREAD_STACK_SIZE as crate::bindings::gsize
} else {
0
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_query_rwx_support() -> GumRwxSupport {
_GumRwxSupport_GUM_RWX_FULL
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_can_remap_writable() -> gboolean {
#[cfg(feature = "win9x")]
{
crate::win9x::in_copy() as gboolean
}
#[cfg(not(feature = "win9x"))]
{
0
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_try_remap_writable_pages(
first_page: gpointer,
_n_pages: guint,
) -> gpointer {
#[cfg(feature = "win9x")]
if crate::win9x::in_copy() {
return crate::win9x_user::take_writes_on(first_page as *mut u8, _n_pages) as gpointer;
}
first_page
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_dispose_writable_pages(_writable: gpointer, _n_pages: guint) {
#[cfg(feature = "win9x")]
if crate::win9x::in_copy() {
crate::win9x_user::make_the_writes(_writable as *mut u8, _n_pages);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_query_region(
address: gconstpointer,
range: *mut GumMemoryRange,
prot: *mut GumPageProtection,
) -> gboolean {
let Some(region) = kernel::region_at(address as usize).filter(|region| region.protection != 0)
else {
return 0;
};
unsafe {
*range = GumMemoryRange {
base_address: region.base,
size: region.size as gsize,
};
*prot = region.protection as GumPageProtection;
}
1
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_try_mprotect(
address: gpointer,
size: gsize,
prot: GumPageProtection,
) -> gboolean {
if kernel::protect(address as u64, size as usize, prot as u32) {
1
} else {
0
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_allocate(
_address: gpointer,
size: gsize,
_alignment: gsize,
prot: GumPageProtection,
) -> gpointer {
let ptr = kernel::alloc_code(size as usize);
if ptr.is_null() {
return ptr::null_mut();
}
unsafe {
core::ptr::write_bytes(ptr, 0, size as usize);
if (prot & _GumPageProtection_GUM_PAGE_EXECUTE) != 0 {
gum::register_slab(ptr as u64, size as usize);
gum_mprotect(ptr as gpointer, size, prot);
}
}
ptr as gpointer
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_free(address: gpointer, size: gsize) -> gboolean {
if gum::is_agent_slab(address as u64) {
gum::unregister_slab(address as u64);
}
kernel::free_code(address as *mut u8, size as usize);
1
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_on_registry_activating(registry: *mut GumModuleRegistry) {
if crate::running_in_a_process() {
register_the_process_modules(registry);
} else {
register_the_kernel_modules(registry);
}
}
// A script can hook a library the moment it arrives, thus stand in front of the loader and say
// what came and what went.
#[cfg(any(feature = "win9x", feature = "winnt"))]
pub(crate) fn watch_the_loader() {
let Some(entries) = kernel::loader_entry_points() else {
return;
};
unsafe {
let interceptor = crate::bindings::gum_interceptor_obtain();
crate::bindings::gum_interceptor_begin_transaction(interceptor);
crate::bindings::gum_interceptor_replace(interceptor, entries.load as gpointer,
kernel::on_module_load as gpointer, &raw mut LOADER_LOAD, ptr::null());
if entries.load_with_flags != 0 {
crate::bindings::gum_interceptor_replace(interceptor,
entries.load_with_flags as gpointer, kernel::on_module_load_with_flags as gpointer,
&raw mut LOADER_LOAD_WITH_FLAGS, ptr::null());
}
crate::bindings::gum_interceptor_replace(interceptor, entries.unload as gpointer,
kernel::on_module_unload as gpointer, &raw mut LOADER_UNLOAD, ptr::null());
crate::bindings::gum_interceptor_end_transaction(interceptor);
}
}
#[cfg(feature = "winnt")]
pub(crate) fn forget_the_loader() {
let Some(entries) = kernel::loader_entry_points() else {
return;
};
unsafe {
let interceptor = crate::bindings::gum_interceptor_obtain();
crate::bindings::gum_interceptor_begin_transaction(interceptor);
crate::bindings::gum_interceptor_revert(interceptor, entries.load as gpointer);
if entries.load_with_flags != 0 {
crate::bindings::gum_interceptor_revert(interceptor,
entries.load_with_flags as gpointer);
}
crate::bindings::gum_interceptor_revert(interceptor, entries.unload as gpointer);
crate::bindings::gum_interceptor_end_transaction(interceptor);
}
}
#[cfg(any(feature = "win9x", feature = "winnt"))]
pub(crate) fn loader_load() -> gpointer {
unsafe { LOADER_LOAD }
}
#[cfg(feature = "win9x")]
pub(crate) fn loader_load_with_flags() -> gpointer {
unsafe { LOADER_LOAD_WITH_FLAGS }
}
#[cfg(any(feature = "win9x", feature = "winnt"))]
pub(crate) fn loader_unload() -> gpointer {
unsafe { LOADER_UNLOAD }
}
#[cfg(feature = "win9x")]
pub(crate) fn module_arrived(base: u64, path: &str) {
if !known_mut().insert(base) {
return;
}
let range = GumMemoryRange {
base_address: base,
size: kernel::image_size(base as u32) as gsize,
};
unsafe {
let registry = crate::bindings::gum_module_registry_obtain();
crate::bindings::gum_module_registry_lock(registry);
let native = gum::gum_native_module_new(path, "", &range);
gum_barebone_register_module(registry, native);
g_object_unref(native as gpointer);
crate::bindings::gum_module_registry_unlock(registry);
}
}
// A module arrived, thus tell the registry where it is.
#[cfg(feature = "winnt")]
pub(crate) fn module_arrived(base: u64) {
if !known_mut().insert(base) {
return;
}
let Some(module) = kernel::describe_module(base) else {
known_mut().remove(&base);
return;
};
let range = GumMemoryRange {
base_address: module.base,
size: module.size as gsize,
};
unsafe {
let registry = crate::bindings::gum_module_registry_obtain();
crate::bindings::gum_module_registry_lock(registry);
let native = gum::gum_native_module_new(&module.path, "", &range);
gum_barebone_register_module(registry, native);
g_object_unref(native as gpointer);
crate::bindings::gum_module_registry_unlock(registry);
}
}
// A module went away, thus take it out of the registry while its name is still known.
#[cfg(any(feature = "win9x", feature = "winnt"))]
pub(crate) fn module_left(base: u64) {
if !known_mut().remove(&base) {
return;
}
unsafe {
let registry = crate::bindings::gum_module_registry_obtain();
crate::bindings::gum_module_registry_lock(registry);
crate::bindings::gum_barebone_unregister_module(registry, base);
crate::bindings::gum_module_registry_unlock(registry);
}
}
#[cfg(any(feature = "win9x", feature = "winnt"))]
fn known_mut() -> &'static mut alloc::collections::BTreeSet<u64> {
unsafe { (&raw mut KNOWN).as_mut().unwrap() }
}
#[cfg(any(feature = "win9x", feature = "winnt"))]
static mut KNOWN: alloc::collections::BTreeSet<u64> = alloc::collections::BTreeSet::new();
#[cfg(any(feature = "win9x", feature = "winnt"))]
static mut LOADER_LOAD: gpointer = ptr::null_mut();
#[cfg(any(feature = "win9x", feature = "winnt"))]
static mut LOADER_LOAD_WITH_FLAGS: gpointer = ptr::null_mut();
#[cfg(any(feature = "win9x", feature = "winnt"))]
static mut LOADER_UNLOAD: gpointer = ptr::null_mut();
fn register_the_process_modules(registry: *mut GumModuleRegistry) {
for module in kernel::enumerate_modules() {
known_mut().insert(module.base);
register_module(registry, &module.path, "", module.base, module.size);
}
}
fn register_the_kernel_modules(registry: *mut GumModuleRegistry) {
#[cfg(feature = "winnt")]
for module in crate::winnt::enumerate_kernel_modules() {
register_module(registry, &module.path, "", module.base, module.size);
}
#[cfg(not(feature = "winnt"))]
for module_info in unsafe { &*core::ptr::addr_of!(crate::MODULE_INFO) }.iter() {
let path = format!("{}{}", kernel::MODULE_DIRECTORY, module_info.name);
register_module(
registry,
&path,
&module_info.version,
module_info.offset as u64,
module_info.size as u64,
);
}
}
fn register_module(registry: *mut GumModuleRegistry, path: &str, version: &str, base: u64, size: u64) {
let range = GumMemoryRange {
base_address: base,
size: size as gsize,
};
unsafe {
let module = gum::gum_native_module_new(path, version, &range);
gum_barebone_register_module(registry, module);
g_object_unref(module as gpointer);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn _gum_process_enumerate_ranges(
prot: GumPageProtection,
func: GumFoundRangeFunc,
user_data: gpointer,
) {
let Some(emit) = func else {
return;
};
kernel::enumerate_ranges(&mut |base, size, protection| {
if (protection & prot as u32) != prot as u32 {
return;
}
let range = GumMemoryRange {
base_address: base,
size: size as gsize,
};
let details = GumRangeDetails {
range: &range,
protection: protection as GumPageProtection,
file: ptr::null(),
};
unsafe { emit(&details, user_data) };
});
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_on_thread_registry_activating(registry: *mut GumThreadRegistry) {
unsafe { THREAD_REGISTRY = registry };
kernel::enumerate_threads(&mut |thread| announce_thread(thread.id), false);
if kernel::watches_threads() {
kernel::watch_threads(announce_thread, announce_thread_is_gone);
} else {
watch_the_threads();
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_on_thread_registry_deactivating(_registry: *mut GumThreadRegistry) {
if kernel::watches_threads() {
kernel::forget_threads();
} else {
forget_the_threads();
}
unsafe { THREAD_REGISTRY = ptr::null_mut() };
}
fn watch_the_threads() {
let Some(entries) = kernel::thread_entry_points() else {
return;
};
unsafe {
let interceptor = crate::bindings::gum_interceptor_obtain();
crate::bindings::gum_interceptor_begin_transaction(interceptor);
crate::bindings::gum_interceptor_replace(interceptor, entries.start as gpointer,
kernel::on_thread_start as gpointer, kernel::thread_start_slot(), ptr::null());
crate::bindings::gum_interceptor_replace(interceptor, entries.exit as gpointer,
kernel::on_thread_exit as gpointer, kernel::thread_exit_slot(), ptr::null());
crate::bindings::gum_interceptor_end_transaction(interceptor);
}
}
fn forget_the_threads() {
let Some(entries) = kernel::thread_entry_points() else {
return;
};
unsafe {
let interceptor = crate::bindings::gum_interceptor_obtain();
crate::bindings::gum_interceptor_begin_transaction(interceptor);
crate::bindings::gum_interceptor_revert(interceptor, entries.start as gpointer);
crate::bindings::gum_interceptor_revert(interceptor, entries.exit as gpointer);
crate::bindings::gum_interceptor_end_transaction(interceptor);
}
}
pub(crate) fn thread_appeared_at(id: u32, routine: usize, parameter: usize) {
let registry = unsafe { THREAD_REGISTRY };
if registry.is_null() {
return;
}
let mut details: GumThreadDetails = unsafe { core::mem::zeroed() };
details.id = id as GumThreadId;
details.entrypoint.routine = routine as u64;
details.entrypoint.parameter = parameter as u64;
details.flags = crate::bindings::GumThreadFlags_GUM_THREAD_FLAGS_ENTRYPOINT_ROUTINE
| crate::bindings::GumThreadFlags_GUM_THREAD_FLAGS_ENTRYPOINT_PARAMETER;
unsafe { crate::bindings::gum_barebone_register_thread(registry, &details) };
}
pub(crate) fn thread_vanished(id: u32) {
announce_thread_is_gone(id);
}
fn announce_thread(id: u32) {
let registry = unsafe { THREAD_REGISTRY };
if registry.is_null() {
return;
}
let mut details: GumThreadDetails = unsafe { core::mem::zeroed() };
details.id = id as GumThreadId;
unsafe { crate::bindings::gum_barebone_register_thread(registry, &details) };
}
fn announce_thread_is_gone(id: u32) {
let registry = unsafe { THREAD_REGISTRY };
if registry.is_null() {
return;
}
unsafe { crate::bindings::gum_barebone_unregister_thread(registry, id as GumThreadId) };
}
static mut THREAD_REGISTRY: *mut GumThreadRegistry = ptr::null_mut();
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_enumerate_threads(func: GumFoundThreadFunc, user_data: gpointer,
flags: GumThreadFlags) {
let Some(emit) = func else {
return;
};
let wanted = (flags & GumThreadFlags_GUM_THREAD_FLAGS_CPU_CONTEXT) != 0;
kernel::enumerate_threads(&mut |thread| {
let mut details: GumThreadDetails = unsafe { core::mem::zeroed() };
details.flags = 0;
details.id = thread.id as GumThreadId;
if let Some(state) = thread.cpu_state {
details.flags = GumThreadFlags_GUM_THREAD_FLAGS_CPU_CONTEXT;
details.cpu_context = cpu_context_from(&state);
}
unsafe { emit(&details, user_data) };
}, wanted);
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_find_thread_by_id(thread_id: GumThreadId, flags: GumThreadFlags)
-> *mut GumThreadDetails {
let wanted = (flags & GumThreadFlags_GUM_THREAD_FLAGS_CPU_CONTEXT) != 0;
let Some(thread) = kernel::find_thread(thread_id as u32, wanted) else {
return ptr::null_mut();
};
let mut details: GumThreadDetails = unsafe { core::mem::zeroed() };
details.id = thread.id as GumThreadId;
if let Some(state) = thread.cpu_state {
details.flags = GumThreadFlags_GUM_THREAD_FLAGS_CPU_CONTEXT;
details.cpu_context = cpu_context_from(&state);
}
unsafe { gum_thread_details_copy(&details) }
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_modify_thread(thread_id: GumThreadId, func: GumModifyThreadFunc,
user_data: gpointer, _flags: GumModifyThreadFlags) -> gboolean {
let Some(modify) = func else {
return 0;
};
let changed = kernel::modify_thread(thread_id as u32, &mut |state| {
let mut context = cpu_context_from(state);
unsafe { modify(thread_id, &mut context, user_data) };
*state = cpu_state_from(&context);
});
changed as gboolean
}
#[cfg(target_arch = "x86")]
pub(crate) fn cpu_context_from(state: &kernel::CpuState) -> GumCpuContext {
GumCpuContext {
eip: state.eip,
edi: state.edi,
esi: state.esi,
ebp: state.ebp,
esp: state.esp,
ebx: state.ebx,
edx: state.edx,
ecx: state.ecx,
eax: state.eax,
xmm: ptr::null_mut(),
}
}
#[cfg(target_arch = "x86")]
pub(crate) fn cpu_state_from(context: &GumCpuContext) -> kernel::CpuState {
kernel::CpuState {
eip: context.eip,
edi: context.edi,
esi: context.esi,
ebp: context.ebp,
esp: context.esp,
ebx: context.ebx,
edx: context.edx,
ecx: context.ecx,
eax: context.eax,
}
}
#[cfg(target_arch = "x86_64")]
pub(crate) fn cpu_context_from(state: &kernel::CpuState) -> GumCpuContext {
GumCpuContext {
rip: state.rip,
r15: state.r15,
r14: state.r14,
r13: state.r13,
r12: state.r12,
r11: state.r11,
r10: state.r10,
r9: state.r9,
r8: state.r8,
rdi: state.rdi,
rsi: state.rsi,
rbp: state.rbp,
rsp: state.rsp,
rbx: state.rbx,
rdx: state.rdx,
rcx: state.rcx,
rax: state.rax,
xmm: ptr::null_mut(),
}
}
#[cfg(target_arch = "x86_64")]
pub(crate) fn cpu_state_from(context: &GumCpuContext) -> kernel::CpuState {
kernel::CpuState {
rip: context.rip,
r15: context.r15,
r14: context.r14,
r13: context.r13,
r12: context.r12,
r11: context.r11,
r10: context.r10,
r9: context.r9,
r8: context.r8,
rdi: context.rdi,
rsi: context.rsi,
rbp: context.rbp,
rsp: context.rsp,
rbx: context.rbx,
rdx: context.rdx,
rcx: context.rcx,
rax: context.rax,
}
}
#[cfg(target_arch = "aarch64")]
pub(crate) fn cpu_context_from(state: &kernel::CpuState) -> GumCpuContext {
GumCpuContext {
pc: state.pc,
sp: state.sp,
nzcv: state.nzcv,
x: state.x,
fp: state.fp,
lr: state.lr,
}
}
#[cfg(target_arch = "aarch64")]
pub(crate) fn cpu_state_from(context: &GumCpuContext) -> kernel::CpuState {
kernel::CpuState {
pc: context.pc,
sp: context.sp,
nzcv: context.nzcv,
x: context.x,
fp: context.fp,
lr: context.lr,
}
}
pub(crate) fn fault_type_of(code: u32) -> crate::bindings::GumExceptionType {
use crate::bindings::*;
match code {
ACCESS_VIOLATION | MISALIGNED_DATA | ARRAY_BOUNDS_EXCEEDED =>
_GumExceptionType_GUM_EXCEPTION_ACCESS_VIOLATION,
GUARD_PAGE => _GumExceptionType_GUM_EXCEPTION_GUARD_PAGE,
ILLEGAL_INSTRUCTION | PRIVILEGED_INSTRUCTION =>
_GumExceptionType_GUM_EXCEPTION_ILLEGAL_INSTRUCTION,
STACK_OVERFLOW => _GumExceptionType_GUM_EXCEPTION_STACK_OVERFLOW,
FLOAT_DENORMAL_OPERAND | FLOAT_DIVIDE_BY_ZERO | FLOAT_INEXACT_RESULT
| FLOAT_INVALID_OPERATION | FLOAT_OVERFLOW | FLOAT_STACK_CHECK | FLOAT_UNDERFLOW
| INTEGER_DIVIDE_BY_ZERO | INTEGER_OVERFLOW =>
_GumExceptionType_GUM_EXCEPTION_ARITHMETIC,
BREAKPOINT => _GumExceptionType_GUM_EXCEPTION_BREAKPOINT,
SINGLE_STEP => _GumExceptionType_GUM_EXCEPTION_SINGLE_STEP,
_ => _GumExceptionType_GUM_EXCEPTION_SYSTEM,
}
}
const ACCESS_VIOLATION: u32 = 0xc000_0005;
const MISALIGNED_DATA: u32 = 0x8000_0002;
const ARRAY_BOUNDS_EXCEEDED: u32 = 0xc000_008c;
const GUARD_PAGE: u32 = 0x8000_0001;
const ILLEGAL_INSTRUCTION: u32 = 0xc000_001d;
const PRIVILEGED_INSTRUCTION: u32 = 0xc000_0096;
const STACK_OVERFLOW: u32 = 0xc000_00fd;
const FLOAT_DENORMAL_OPERAND: u32 = 0xc000_008d;
const FLOAT_DIVIDE_BY_ZERO: u32 = 0xc000_008e;
const FLOAT_INEXACT_RESULT: u32 = 0xc000_008f;
const FLOAT_INVALID_OPERATION: u32 = 0xc000_0090;
const FLOAT_OVERFLOW: u32 = 0xc000_0091;
const FLOAT_STACK_CHECK: u32 = 0xc000_0092;
const FLOAT_UNDERFLOW: u32 = 0xc000_0093;
const INTEGER_DIVIDE_BY_ZERO: u32 = 0xc000_0094;
const INTEGER_OVERFLOW: u32 = 0xc000_0095;
const BREAKPOINT: u32 = 0x8000_0003;
const SINGLE_STEP: u32 = 0x8000_0004;
pub(crate) unsafe fn enumerate_imports_in_module(base: u64, callback: &mut FoundImportCallback) {
if crate::running_in_a_process() {
kernel::enumerate_imports(base as _, &mut |library, name, slot, address| {
callback(library as *const crate::bindings::gchar, name as *const crate::bindings::gchar, slot, address)
});
}
}
pub(crate) unsafe fn enumerate_exports_in_range(
start_address: u64,
end_address: u64,
callback: &mut FoundExportCallback,
) {
#[cfg(any(feature = "win9x", feature = "winnt"))]
if crate::running_in_a_process() {
kernel::enumerate_exports(start_address as _, &mut |name, address| {
callback(name as *const crate::bindings::gchar, address)
});
return;
}
unsafe {
let symbol_table = &*core::ptr::addr_of!(crate::SYMBOL_TABLE);
for symbol_ref in symbol_table.iter_symbols_in_range(start_address, end_address) {
if !callback(symbol_ref.name_ptr(), symbol_ref.address()) {
break;
}
}
}
}
+116
View File
@@ -0,0 +1,116 @@
use core::sync::atomic::{AtomicBool, Ordering};
// The copy's own memory, which is not the C library's: that one asks for memory of its own
// while it is taking its lock, and a lock that allocates through the allocator it guards never
// finishes the first time it is asked.
pub fn take(size: usize) -> *mut u8 {
let wanted = wanted_for(size);
let class = class_of(wanted);
held(|| {
if let Some(piece) = pop(class) {
return piece;
}
carve(class)
})
}
pub fn give_back(piece: *mut u8) {
if piece.is_null() {
return;
}
let class = unsafe { *(piece.sub(HEADER) as *const usize) };
held(|| push(class, piece));
}
fn pages_for(size: usize) -> *mut u8 {
#[cfg(feature = "linux-injected")]
{
crate::linux::user::map_writable(size)
}
#[cfg(feature = "xnu-core")]
{
crate::xnu_user_calls::map_writable(size)
}
}
fn pop(class: usize) -> Option<*mut u8> {
let free = unsafe { (&raw mut FREE).as_mut().unwrap() };
let piece = free[class];
if piece.is_null() {
return None;
}
free[class] = unsafe { *(piece as *const *mut u8) };
Some(piece)
}
fn push(class: usize, piece: *mut u8) {
let free = unsafe { (&raw mut FREE).as_mut().unwrap() };
unsafe { *(piece as *mut *mut u8) = free[class] };
free[class] = piece;
}
fn carve(class: usize) -> *mut u8 {
let wanted = HEADER + (SMALLEST << class);
let mut edge = unsafe { EDGE };
let mut left = unsafe { LEFT };
if left < wanted {
let asked = if wanted > CHUNK { wanted } else { CHUNK };
edge = pages_for(asked) as usize;
if edge == 0 {
return core::ptr::null_mut();
}
left = asked;
}
unsafe {
EDGE = edge + wanted;
LEFT = left - wanted;
*(edge as *mut usize) = class;
}
(edge + HEADER) as *mut u8
}
fn held<T>(work: impl FnOnce() -> T) -> T {
while TAKEN.swap(true, Ordering::Acquire) {
core::hint::spin_loop();
}
let answer = work();
TAKEN.store(false, Ordering::Release);
answer
}
fn wanted_for(size: usize) -> usize {
if size < SMALLEST { SMALLEST } else { size }
}
fn class_of(size: usize) -> usize {
let mut class = 0;
while (SMALLEST << class) < size {
class += 1;
}
class
}
static mut FREE: [*mut u8; CLASSES] = [core::ptr::null_mut(); CLASSES];
static mut EDGE: usize = 0;
static mut LEFT: usize = 0;
static TAKEN: AtomicBool = AtomicBool::new(false);
const HEADER: usize = 16;
const SMALLEST: usize = 16;
const CLASSES: usize = 32;
const CHUNK: usize = 4 * 1024 * 1024;
+121
View File
@@ -0,0 +1,121 @@
// Linux transport: /dev/frida, framed the same way as the XNU hostlinks (4-byte
// little-endian length prefix) so the host-side (yqv) protocol is identical across
// every transport.
//
// The character device itself lives in linux/frida-kmod.c and hands us a byte
// stream; whoever opens the device decides how it reaches the host.
use core::cell::UnsafeCell;
use core::ffi::{c_int, c_void};
use alloc::vec::Vec;
struct Inner {
rx_lenbuf: [u8; 4],
rx_lenhave: usize,
rx_buf: Vec<u8>,
rx_have: usize,
rx_need: usize,
}
pub struct Hostlink {
state: UnsafeCell<Inner>,
on_rx: Option<fn(&[u8])>,
}
unsafe impl Send for Hostlink {}
impl Hostlink {
pub fn init(on_rx: Option<fn(&[u8])>) -> Result<Self, ()> {
if unsafe { frida_kmod_link_open() } != 0 {
return Err(());
}
Ok(Hostlink {
state: UnsafeCell::new(Inner {
rx_lenbuf: [0; 4],
rx_lenhave: 0,
rx_buf: Vec::new(),
rx_have: 0,
rx_need: 0,
}),
on_rx,
})
}
pub fn send(&self, payload: &[u8]) {
let header = (payload.len() as u32).to_le_bytes();
unsafe {
frida_kmod_link_send(header.as_ptr() as *const c_void, header.len());
frida_kmod_link_send(payload.as_ptr() as *const c_void, payload.len());
}
}
pub fn process(&self) {
let s = unsafe { &mut *self.state.get() };
loop {
if s.rx_lenhave < 4 {
let lo = s.rx_lenhave;
let n = recv_nonblocking(&mut s.rx_lenbuf[lo..4]);
if n == 0 {
return;
}
s.rx_lenhave += n;
if s.rx_lenhave < 4 {
continue;
}
s.rx_need = u32::from_le_bytes(s.rx_lenbuf) as usize;
s.rx_have = 0;
s.rx_buf.resize(s.rx_need, 0);
}
while s.rx_have < s.rx_need {
let lo = s.rx_have;
let hi = s.rx_need;
let n = recv_nonblocking(&mut s.rx_buf[lo..hi]);
if n == 0 {
return;
}
s.rx_have += n;
}
// Detach the frame and reset receive state before dispatching: the callback may
// re-enter process() (a synchronous host RPC issued while handling a command), and
// it must start from a clean state instead of re-dispatching this same frame.
let frame = core::mem::take(&mut s.rx_buf);
let need = s.rx_need;
s.rx_lenhave = 0;
s.rx_have = 0;
s.rx_need = 0;
if let Some(cb) = self.on_rx {
cb(&frame[..need]);
}
}
}
}
impl Drop for Hostlink {
fn drop(&mut self) {
unsafe { frida_kmod_link_close() };
}
}
#[cfg(feature = "xnu-kext")]
pub fn a_turn_is_wanted() -> bool {
unsafe { frida_kmod_link_pending() }
}
fn recv_nonblocking(dst: &mut [u8]) -> usize {
let n = unsafe { frida_kmod_link_recv(dst.as_mut_ptr() as *mut c_void, dst.len()) };
if n <= 0 { 0 } else { n as usize }
}
unsafe extern "C" {
fn frida_kmod_link_open() -> c_int;
fn frida_kmod_link_close();
fn frida_kmod_link_send(data: *const c_void, size: usize) -> c_int;
fn frida_kmod_link_recv(data: *mut c_void, size: usize) -> isize;
#[cfg(feature = "xnu-kext")]
fn frida_kmod_link_pending() -> bool;
}
+441
View File
@@ -0,0 +1,441 @@
use core::cell::UnsafeCell;
use core::ffi::c_void;
use core::ptr;
use alloc::collections::VecDeque;
use alloc::vec::Vec;
use crate::kernel;
use crate::winnt::windows_fn;
pub struct Hostlink {
state: UnsafeCell<Inner>,
on_rx: Option<fn(&[u8])>,
}
struct Inner {
port: *mut c_void,
event: *mut c_void,
}
fn issue<F>(event: *mut c_void, request: F) -> Option<usize>
where
F: FnOnce(*mut c_void, *mut c_void) -> i32,
{
let mut status_block = [0usize; STATUS_BLOCK_WORDS];
unsafe {
(_ZwClearEvent)(event);
}
let status = request(event, status_block.as_mut_ptr() as *mut c_void);
if status == STATUS_PENDING {
unsafe {
(_ZwWaitForSingleObject)(event, 0, ptr::null());
}
}
if status < 0 || (status_block[STATUS_BLOCK_STATUS] as i32) < 0 {
return None;
}
Some(status_block[STATUS_BLOCK_COUNT])
}
fn create_event() -> Option<*mut c_void> {
let mut event: *mut c_void = ptr::null_mut();
let status = unsafe {
(_ZwCreateEvent)(&mut event, EVENT_ALL_ACCESS, ptr::null_mut(), SYNCHRONIZATION_EVENT, 0)
};
if status < 0 {
return None;
}
Some(event)
}
impl Hostlink {
pub fn init(on_rx: Option<fn(&[u8])>, wake_token: *const u8) -> Result<Self, ()> {
let port = open_first_serial_port()?;
let event = create_event().ok_or(())?;
let reader_event = create_event().ok_or(())?;
unsafe {
WAKE_TOKEN = wake_token;
READER_PORT = port;
READER_EVENT = reader_event;
}
kernel::spawn_thread(read_from_host, ptr::null_mut());
Ok(Self {
state: UnsafeCell::new(Inner { port, event }),
on_rx,
})
}
pub fn send(&self, payload: &[u8]) {
let s = unsafe { &*self.state.get() };
write_all(s.port, s.event, &(payload.len() as u32).to_le_bytes());
write_all(s.port, s.event, payload);
}
pub fn process(&self) {
loop {
let Some(frame) = take_frame() else {
return;
};
if let Some(on_rx) = self.on_rx {
on_rx(&frame);
}
}
}
pub fn shutdown(&self) {
let s = unsafe { &*self.state.get() };
unsafe {
(_ZwClose)(s.port);
(_ZwClose)(s.event);
}
}
}
unsafe extern "C" fn read_from_host(_parameter: *mut c_void, _wait_result: i32) {
let port = unsafe { READER_PORT };
let event = unsafe { READER_EVENT };
let mut length = [0u8; FRAME_LENGTH_SIZE];
loop {
if !read_exactly(port, event, &mut length) {
return;
}
let mut frame = alloc::vec![0u8; u32::from_le_bytes(length) as usize];
if !read_exactly(port, event, &mut frame) {
return;
}
put_frame(frame);
kernel::wake(unsafe { WAKE_TOKEN });
}
}
fn read_exactly(port: *mut c_void, event: *mut c_void, buffer: &mut [u8]) -> bool {
let mut read = 0;
while read != buffer.len() {
let mut offset = 0i64;
let moved = issue(event, |event, status_block| unsafe {
(_ZwReadFile)(
port,
event,
ptr::null_mut(),
ptr::null_mut(),
status_block,
buffer.as_mut_ptr().add(read),
(buffer.len() - read) as u32,
&mut offset,
ptr::null_mut(),
)
});
let Some(moved) = moved else {
return false;
};
if moved == 0 {
return false;
}
read += moved;
}
true
}
fn write_all(port: *mut c_void, event: *mut c_void, buffer: &[u8]) {
let mut written = 0;
while written != buffer.len() {
let mut offset = 0i64;
let moved = issue(event, |event, status_block| unsafe {
(_ZwWriteFile)(
port,
event,
ptr::null_mut(),
ptr::null_mut(),
status_block,
buffer.as_ptr().add(written),
(buffer.len() - written) as u32,
&mut offset,
ptr::null_mut(),
)
});
let Some(moved) = moved else {
return;
};
if moved == 0 {
return;
}
written += moved;
}
}
pub fn a_turn_is_wanted() -> bool {
A_TURN_IS_WANTED.load(Ordering::Acquire)
}
static A_TURN_IS_WANTED: AtomicBool = AtomicBool::new(false);
fn put_frame(frame: Vec<u8>) {
lock_frames();
unsafe {
(*ptr::addr_of_mut!(FRAMES)).push_back(frame);
}
unlock_frames();
A_TURN_IS_WANTED.store(true, Ordering::Release);
}
fn take_frame() -> Option<Vec<u8>> {
lock_frames();
let frame = unsafe { (*ptr::addr_of_mut!(FRAMES)).pop_front() };
let drained = unsafe { (*ptr::addr_of!(FRAMES)).is_empty() };
unlock_frames();
if drained {
A_TURN_IS_WANTED.store(false, Ordering::Release);
}
frame
}
fn lock_frames() {
while FRAMES_LOCK
.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
kernel::yield_now();
}
}
fn unlock_frames() {
FRAMES_LOCK.store(0, Ordering::Release);
}
static mut FRAMES: VecDeque<Vec<u8>> = VecDeque::new();
static FRAMES_LOCK: AtomicU32 = AtomicU32::new(0);
static mut WAKE_TOKEN: *const u8 = ptr::null();
static mut READER_PORT: *mut c_void = ptr::null_mut();
static mut READER_EVENT: *mut c_void = ptr::null_mut();
fn open_first_serial_port() -> Result<*mut c_void, ()> {
let ports = open_key(SERIAL_PORT_KEY)?;
let mut name = [0u8; VALUE_NAME_SIZE];
let mut size = 0u32;
let status = unsafe {
(_ZwEnumerateValueKey)(ports, 0, VALUE_NAME_INFORMATION, name.as_mut_ptr(),
name.len() as u32, &mut size)
};
unsafe {
(_ZwClose)(ports);
}
if status < 0 {
return Err(());
}
let length = u32::from_le_bytes(name[VALUE_NAME_LENGTH..VALUE_NAME_LENGTH + 4]
.try_into()
.unwrap()) as usize;
open_device(&name[VALUE_NAME_OFFSET..VALUE_NAME_OFFSET + length])
}
fn open_key(path: &[u16]) -> Result<*mut c_void, ()> {
let mut handle: *mut c_void = ptr::null_mut();
let mut name = unicode_string(path);
let mut attributes = object_attributes(&mut name);
let status =
unsafe { (_ZwOpenKey)(&mut handle, KEY_READ, attributes.as_mut_ptr() as *mut c_void) };
if status < 0 {
return Err(());
}
Ok(handle)
}
fn open_device(path: &[u8]) -> Result<*mut c_void, ()> {
let mut handle: *mut c_void = ptr::null_mut();
let mut name = UnicodeString {
length: path.len() as u16,
maximum_length: path.len() as u16,
buffer: path.as_ptr() as *const u16,
};
let mut attributes = object_attributes(&mut name);
let mut status_block = [0usize; STATUS_BLOCK_WORDS];
let status = unsafe {
(_ZwCreateFile)(
&mut handle,
GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE,
attributes.as_mut_ptr() as *mut c_void,
status_block.as_mut_ptr() as *mut c_void,
ptr::null_mut(),
0,
0,
FILE_OPEN,
0,
ptr::null_mut(),
0,
)
};
if status < 0 {
return Err(());
}
configure_port(handle);
Ok(handle)
}
fn configure_port(port: *mut c_void) {
let mut baud_rate = [0u32; 1];
baud_rate[0] = BAUD_RATE;
control_port(port, SET_BAUD_RATE, baud_rate.as_ptr() as *const u8,
core::mem::size_of_val(&baud_rate) as u32);
let line_control = [ONE_STOP_BIT, NO_PARITY, DATA_BITS];
control_port(port, SET_LINE_CONTROL, line_control.as_ptr(),
core::mem::size_of_val(&line_control) as u32);
let hand_flow = [0u32; HAND_FLOW_WORDS];
control_port(port, SET_HAND_FLOW, hand_flow.as_ptr() as *const u8,
core::mem::size_of_val(&hand_flow) as u32);
control_port(port, SET_DTR, core::ptr::null(), 0);
control_port(port, SET_RTS, core::ptr::null(), 0);
let mut timeouts = [0u32; TIMEOUT_WORDS];
timeouts[READ_INTERVAL_TIMEOUT] = FOREVER;
timeouts[READ_TOTAL_TIMEOUT_MULTIPLIER] = FOREVER;
timeouts[READ_TOTAL_TIMEOUT_CONSTANT] = ALMOST_FOREVER;
control_port(port, SET_TIMEOUTS, timeouts.as_ptr() as *const u8,
core::mem::size_of_val(&timeouts) as u32);
}
fn control_port(port: *mut c_void, code: u32, input: *const u8, length: u32) {
let Some(event) = create_event() else {
return;
};
issue(event, |event, status_block| unsafe {
(_ZwDeviceIoControlFile)(
port,
event,
ptr::null_mut(),
ptr::null_mut(),
status_block,
code,
input,
length,
ptr::null_mut(),
0,
)
});
unsafe {
(_ZwClose)(event);
}
}
#[repr(C)]
struct UnicodeString {
length: u16,
maximum_length: u16,
buffer: *const u16,
}
fn unicode_string(text: &[u16]) -> UnicodeString {
let bytes = (text.len() * 2) as u16;
UnicodeString {
length: bytes,
maximum_length: bytes,
buffer: text.as_ptr(),
}
}
fn object_attributes(name: &mut UnicodeString) -> [usize; OBJECT_ATTRIBUTES_WORDS] {
let mut attributes = [0usize; OBJECT_ATTRIBUTES_WORDS];
attributes[0] = core::mem::size_of::<[usize; OBJECT_ATTRIBUTES_WORDS]>();
attributes[2] = name as *mut UnicodeString as usize;
attributes[3] = OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE;
attributes
}
const FRAME_LENGTH_SIZE: usize = 4;
const STATUS_BLOCK_WORDS: usize = 2;
const STATUS_BLOCK_COUNT: usize = 1;
const STATUS_BLOCK_STATUS: usize = 0;
const OBJECT_ATTRIBUTES_WORDS: usize = 6;
const SERIAL_PORT_KEY: &[u16] = &[
0x5c, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x52, 0x59, 0x5c, 0x4d, 0x41, 0x43, 0x48, 0x49, 0x4e,
0x45, 0x5c, 0x48, 0x41, 0x52, 0x44, 0x57, 0x41, 0x52, 0x45, 0x5c, 0x44, 0x45, 0x56, 0x49, 0x43,
0x45, 0x4d, 0x41, 0x50, 0x5c, 0x53, 0x45, 0x52, 0x49, 0x41, 0x4c, 0x43, 0x4f, 0x4d, 0x4d,
];
const VALUE_NAME_INFORMATION: u32 = 0;
const VALUE_NAME_SIZE: usize = 512;
const VALUE_NAME_LENGTH: usize = 0x08;
const VALUE_NAME_OFFSET: usize = 0x0c;
const SET_BAUD_RATE: u32 = 0x001b_0004;
const SET_LINE_CONTROL: u32 = 0x001b_000c;
const SET_TIMEOUTS: u32 = 0x001b_001c;
const SET_DTR: u32 = 0x001b_0024;
const SET_RTS: u32 = 0x001b_0030;
const SET_HAND_FLOW: u32 = 0x001b_0064;
const BAUD_RATE: u32 = 921_600;
const DATA_BITS: u8 = 8;
const NO_PARITY: u8 = 0;
const ONE_STOP_BIT: u8 = 0;
const HAND_FLOW_WORDS: usize = 4;
const TIMEOUT_WORDS: usize = 5;
const READ_INTERVAL_TIMEOUT: usize = 0;
const READ_TOTAL_TIMEOUT_MULTIPLIER: usize = 1;
const READ_TOTAL_TIMEOUT_CONSTANT: usize = 2;
const FOREVER: u32 = u32::MAX;
const ALMOST_FOREVER: u32 = u32::MAX - 1;
const STATUS_PENDING: i32 = 0x103;
const SYNCHRONIZATION_EVENT: u32 = 1;
const EVENT_ALL_ACCESS: u32 = 0x1f_0003;
const KEY_READ: u32 = 0x2_0019;
const GENERIC_READ: u32 = 0x8000_0000;
const GENERIC_WRITE: u32 = 0x4000_0000;
const SYNCHRONIZE: u32 = 0x0010_0000;
const FILE_OPEN: u32 = 1;
const OBJ_CASE_INSENSITIVE: usize = 0x40;
const OBJ_KERNEL_HANDLE: usize = 0x200;
use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
unsafe extern "C" {
static _ZwOpenKey: windows_fn!(*mut *mut c_void, u32, *mut c_void => i32);
static _ZwEnumerateValueKey: windows_fn!(
*mut c_void, u32, u32, *mut u8, u32, *mut u32 => i32);
static _ZwCreateFile: windows_fn!(
*mut *mut c_void, u32, *mut c_void, *mut c_void, *mut i64, u32, u32, u32, u32,
*mut c_void, u32 => i32);
static _ZwReadFile: windows_fn!(
*mut c_void, *mut c_void, *mut c_void, *mut c_void, *mut c_void, *mut u8, u32, *mut i64,
*mut u32 => i32);
static _ZwWriteFile: windows_fn!(
*mut c_void, *mut c_void, *mut c_void, *mut c_void, *mut c_void, *const u8, u32, *mut i64,
*mut u32 => i32);
static _ZwDeviceIoControlFile: windows_fn!(
*mut c_void, *mut c_void, *mut c_void, *mut c_void, *mut c_void, u32, *const u8, u32,
*mut u8, u32 => i32);
static _ZwClose: windows_fn!(*mut c_void => i32);
static _ZwCreateEvent: windows_fn!(*mut *mut c_void, u32, *mut c_void, u32, u8 => i32);
static _ZwClearEvent: windows_fn!(*mut c_void => i32);
static _ZwWaitForSingleObject: windows_fn!(*mut c_void, u8, *const i64 => i32);
static _ObReferenceObjectByHandle: windows_fn!(
*mut c_void, u32, *mut c_void, u8, *mut *mut c_void, *mut c_void => i32);
static _KeWaitForSingleObject: windows_fn!(*mut c_void, u32, u32, u8, *const i64 => i32);
}
File diff suppressed because it is too large. Load diff
+37 -34
View File
@@ -8,8 +8,6 @@ use core::sync::atomic::{AtomicU32, Ordering};
use alloc::vec::Vec;
use crate::xnu;
const AF_VSOCK: c_int = 40;
const SOCK_STREAM: c_int = 1;
const VMADDR_CID_HOST: u32 = 2;
@@ -79,6 +77,10 @@ unsafe extern "C" {
static UPCALL_PENDING: AtomicU32 = AtomicU32::new(0);
pub fn a_turn_is_wanted() -> bool {
UPCALL_PENDING.load(Ordering::Acquire) != 0
}
struct Inner {
so: SocketT,
rx_lenbuf: [u8; 4],
@@ -153,39 +155,17 @@ impl Hostlink {
pub fn send(&self, payload: &[u8]) {
let s = unsafe { &*self.state.get() };
let length = (payload.len() as u32).to_le_bytes();
unsafe {
let len = payload.len() as u32;
let mut hdr = [
(len & 0xff) as u8,
((len >> 8) & 0xff) as u8,
((len >> 16) & 0xff) as u8,
((len >> 24) & 0xff) as u8,
];
let mut iov = [
Iovec {
iov_base: hdr.as_mut_ptr() as *mut c_void,
iov_len: 4,
},
Iovec {
iov_base: payload.as_ptr() as *mut c_void,
iov_len: payload.len(),
},
];
let msg = Msghdr {
msg_name: ptr::null_mut(),
msg_namelen: 0,
msg_iov: iov.as_mut_ptr(),
msg_iovlen: 2,
msg_control: ptr::null_mut(),
msg_controllen: 0,
msg_flags: 0,
};
let mut sent: usize = 0;
let _ = _sock_send(s.so, &msg, 0, &mut sent);
send_all(s.so, &length);
send_all(s.so, payload);
}
}
pub fn process(&self) {
UPCALL_PENDING.store(0, Ordering::Release);
let s = unsafe { &mut *self.state.get() };
loop {
if s.rx_lenhave < 4 {
@@ -259,9 +239,32 @@ unsafe fn recv_nonblocking(so: SocketT, dst: &mut [u8]) -> usize {
}
}
unsafe extern "C" fn upcall(_so: SocketT, cookie: *mut c_void, _waitf: c_int) {
UPCALL_PENDING.fetch_add(1, Ordering::Release);
if !cookie.is_null() {
xnu::thread_wakeup(cookie as *const u8);
unsafe fn send_all(so: SocketT, bytes: &[u8]) {
let mut gone = 0;
while gone < bytes.len() {
let mut iov = [Iovec {
iov_base: unsafe { bytes.as_ptr().add(gone) } as *mut c_void,
iov_len: bytes.len() - gone,
}];
let msg = Msghdr {
msg_name: ptr::null_mut(),
msg_namelen: 0,
msg_iov: iov.as_mut_ptr(),
msg_iovlen: 1,
msg_control: ptr::null_mut(),
msg_controllen: 0,
msg_flags: 0,
};
let mut sent: usize = 0;
if unsafe { _sock_send(so, &msg, 0, &mut sent) } != 0 || sent == 0 {
return;
}
gone += sent;
}
}
unsafe extern "C" fn upcall(_so: SocketT, cookie: *mut c_void, _waitf: c_int) {
UPCALL_PENDING.fetch_add(1, Ordering::Release);
crate::nudge_the_loop(cookie as *const u8);
}
+377
View File
@@ -0,0 +1,377 @@
pub trait Image {
fn read_at(&self, position: u32, buffer: &mut [u8]) -> u32;
}
pub fn enumerate(image: &dyn Image, found: &mut dyn FnMut(&[u8])) {
let Some(resources) = resource_directory(image) else {
return;
};
let Some(group) = resources.first_leaf(RT_GROUP_ICON) else {
return;
};
// A group contains each size one time for each color depth. Keep only the best of each size.
let count = read_u16(image, group.offset + GROUP_COUNT_OFFSET) as u32;
for index in 0..count {
if !is_best_of_its_size(image, group.offset, count, index) {
continue;
}
let entry = group.offset + GROUP_ENTRIES_OFFSET + index * GROUP_ENTRY_SIZE;
let id = read_u16(image, entry + GROUP_ENTRY_ID_OFFSET);
if let Some(icon) = resources.leaf(RT_ICON, id) {
if let Some(bytes) = read_blob(image, icon.offset, icon.size) {
found(bytes);
}
}
}
}
pub fn describe(image: &dyn Image, into: &mut [u8]) -> usize {
if read_u16(image, read_u32(image, DOS_HEADERS_OFFSET)) == NE_SIGNATURE {
return describe_ne(image, into);
}
string_in_version(image, FILE_DESCRIPTION, into)
}
pub fn identify(image: &dyn Image, into: &mut [u8]) -> usize {
if read_u16(image, read_u32(image, DOS_HEADERS_OFFSET)) == NE_SIGNATURE {
let mut description = [0u8; 128];
let taken = describe_ne(image, &mut description);
let described = &description[..taken];
let Some(space) = described.iter().position(|b| *b == b' ') else {
return 0;
};
let program = &described[space + 1..];
if program.iter().filter(|b| b.is_ascii_alphanumeric()).count() > MAX_PROGRAM_WORD {
return 0;
}
return write_identity(&described[..space], program, into);
}
let mut company = [0u8; 64];
let mut taken_company = string_in_version(image, COMPANY_NAME, &mut company);
if taken_company == 0 {
taken_company = string_in_version(image, PRODUCT_NAME, &mut company);
}
let mut internal = [0u8; 64];
let mut taken_internal = string_in_version(image, INTERNAL_NAME, &mut internal);
if taken_internal == 0 {
taken_internal = string_in_version(image, ORIGINAL_FILENAME, &mut internal);
}
if taken_company == 0 || taken_internal == 0 {
return 0;
}
write_identity(&company[..taken_company], without_extension(&internal[..taken_internal]), into)
}
fn without_extension(name: &[u8]) -> &[u8] {
match name.iter().rposition(|b| *b == b'.') {
Some(dot) => &name[..dot],
None => name,
}
}
fn write_identity(company: &[u8], program: &[u8], into: &mut [u8]) -> usize {
let maker = match company.iter().position(|b| *b == b' ') {
Some(space) => &company[..space],
None => company,
};
let mut written = 0;
let put = |byte: u8, into: &mut [u8], written: &mut usize| {
if *written != into.len() {
into[*written] = byte;
*written += 1;
}
};
let put_word = |word: &[u8], into: &mut [u8], written: &mut usize| {
for byte in word {
if byte.is_ascii_alphanumeric() {
put(byte.to_ascii_lowercase(), into, written);
}
}
};
put_word(b"com", into, &mut written);
put(b'.', into, &mut written);
put_word(maker, into, &mut written);
put(b'.', into, &mut written);
put_word(program, into, &mut written);
written
}
fn string_in_version(image: &dyn Image, key: &[u8], into: &mut [u8]) -> usize {
let Some(resources) = resource_directory(image) else {
return 0;
};
let Some(version) = resources.first_leaf(RT_VERSION) else {
return 0;
};
let Some(blob) = read_blob(image, version.offset, version.size) else {
return 0;
};
let Some(at) = find(blob, key) else {
return 0;
};
// Each string in the block says how long its value is, in the six bytes before its key. A
// value of nothing leaves the next key where the value would be.
if at < 6 {
return 0;
}
let value_length = u16::from_le_bytes([blob[at - 4], blob[at - 3]]) as usize;
if value_length == 0 {
return 0;
}
let mut at = at + key.len();
while at % 4 != 0 {
at += 1;
}
let mut written = 0;
while at + 1 < blob.len() && written < into.len() && written < value_length {
let unit = blob[at] as u16 | ((blob[at + 1] as u16) << 8);
if unit == 0 {
break;
}
into[written] = if unit < 0x80 { unit as u8 } else { b'?' };
written += 1;
at += 2;
}
written
}
fn describe_ne(image: &dyn Image, into: &mut [u8]) -> usize {
let headers = read_u32(image, DOS_HEADERS_OFFSET);
let table = read_u32(image, headers + NE_NON_RESIDENT_NAMES_OFFSET);
if table == 0 {
return 0;
}
let mut length = [0u8; 1];
if image.read_at(table, &mut length) != 1 {
return 0;
}
let wanted = core::cmp::min(length[0] as usize, into.len());
image.read_at(table + 1, &mut into[..wanted]) as usize
}
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack.windows(needle.len()).position(|window| window == needle)
}
fn is_best_of_its_size(image: &dyn Image, group: u32, count: u32, index: u32) -> bool {
let mine = entry_shape(image, group, index);
(0..count).filter(|other| *other != index).all(|other| {
let theirs = entry_shape(image, group, other);
theirs.0 != mine.0 || theirs.1 != mine.1 || theirs.2 < mine.2
|| (theirs.2 == mine.2 && other > index)
})
}
fn entry_shape(image: &dyn Image, group: u32, index: u32) -> (u8, u8, u16) {
let entry = group + GROUP_ENTRIES_OFFSET + index * GROUP_ENTRY_SIZE;
(
read_u8(image, entry + GROUP_ENTRY_WIDTH_OFFSET),
read_u8(image, entry + GROUP_ENTRY_HEIGHT_OFFSET),
read_u16(image, entry + GROUP_ENTRY_DEPTH_OFFSET),
)
}
// A resource has the address that it gets after the loader maps it. The section gives the
// distance from that address to the position in the file.
fn resource_directory(image: &dyn Image) -> Option<ResourceDirectory<'_>> {
let headers = read_u32(image, DOS_HEADERS_OFFSET);
if read_u32(image, headers) != PE_SIGNATURE {
return None;
}
let optional = headers + OPTIONAL_HEADER_OFFSET;
let directory_rva = read_u32(image, optional + resource_directory_offset(image, optional));
if directory_rva == 0 {
return None;
}
let sections = optional + read_u16(image, headers + OPTIONAL_HEADER_SIZE_OFFSET) as u32;
let section_count = read_u16(image, headers + SECTION_COUNT_OFFSET) as u32;
for index in 0..section_count {
let section = sections + index * SECTION_SIZE;
let virtual_address = read_u32(image, section + SECTION_RVA_OFFSET);
let virtual_size = read_u32(image, section + SECTION_VIRTUAL_SIZE_OFFSET);
if directory_rva >= virtual_address && directory_rva < virtual_address + virtual_size {
let raw_data = read_u32(image, section + SECTION_RAW_DATA_OFFSET);
return Some(ResourceDirectory {
image,
rva: directory_rva,
offset: directory_rva - virtual_address + raw_data,
});
}
}
None
}
// The magic value gives the form of the optional header, and the two forms have different
// widths before the data directories.
fn resource_directory_offset(image: &dyn Image, optional: u32) -> u32 {
if read_u16(image, optional) == PE32_MAGIC {
return PE32_RESOURCE_DIRECTORY_OFFSET;
}
PE32PLUS_RESOURCE_DIRECTORY_OFFSET
}
struct ResourceDirectory<'i> {
image: &'i dyn Image,
rva: u32,
offset: u32,
}
struct Resource {
offset: u32,
size: u32,
}
impl ResourceDirectory<'_> {
fn first_leaf(&self, kind: u16) -> Option<Resource> {
let by_name = self.subdirectory(self.offset, kind)?;
let by_language = self.first_subdirectory(by_name)?;
self.leaf_at(self.first_subdirectory(by_language)?)
}
fn leaf(&self, kind: u16, name: u16) -> Option<Resource> {
let by_name = self.subdirectory(self.offset, kind)?;
let by_language = self.subdirectory(by_name, name)?;
self.leaf_at(self.first_subdirectory(by_language)?)
}
fn subdirectory(&self, directory: u32, id: u16) -> Option<u32> {
for entry in self.entries(directory) {
if read_u32(self.image, entry) == id as u32 {
return Some(self.child(entry));
}
}
None
}
fn first_subdirectory(&self, directory: u32) -> Option<u32> {
self.entries(directory).next().map(|e| self.child(e))
}
fn entries(&self, directory: u32) -> impl Iterator<Item = u32> + '_ {
let named = read_u16(self.image, directory + NAMED_ENTRY_COUNT_OFFSET) as u32;
let identified = read_u16(self.image, directory + ID_ENTRY_COUNT_OFFSET) as u32;
let first = directory + DIRECTORY_HEADER_SIZE + named * DIRECTORY_ENTRY_SIZE;
(0..identified).map(move |index| first + index * DIRECTORY_ENTRY_SIZE)
}
fn child(&self, entry: u32) -> u32 {
let value = read_u32(self.image, entry + DIRECTORY_ENTRY_CHILD_OFFSET);
self.offset + (value & !SUBDIRECTORY_FLAG)
}
fn leaf_at(&self, data_entry: u32) -> Option<Resource> {
let data_rva = read_u32(self.image, data_entry);
Some(Resource {
offset: self.offset + data_rva - self.rva,
size: read_u32(self.image, data_entry + DATA_ENTRY_SIZE_OFFSET),
})
}
}
fn read_u32(image: &dyn Image, position: u32) -> u32 {
let mut bytes = [0u8; 4];
image.read_at(position, &mut bytes);
u32::from_le_bytes(bytes)
}
fn read_u8(image: &dyn Image, position: u32) -> u8 {
let mut bytes = [0u8; 1];
image.read_at(position, &mut bytes);
bytes[0]
}
fn read_u16(image: &dyn Image, position: u32) -> u16 {
let mut bytes = [0u8; 2];
image.read_at(position, &mut bytes);
u16::from_le_bytes(bytes)
}
fn read_blob(image: &dyn Image, position: u32, size: u32) -> Option<&'static [u8]> {
let blob = unsafe { &mut *core::ptr::addr_of_mut!(ICON_BUFFER) };
if size as usize > blob.len() {
return None;
}
let read = image.read_at(position, &mut blob[..size as usize]);
if read != size {
return None;
}
Some(&blob[..size as usize])
}
static mut ICON_BUFFER: [u8; MAX_ICON_SIZE] = [0; MAX_ICON_SIZE];
const MAX_ICON_SIZE: usize = 16 * 1024;
const DOS_HEADERS_OFFSET: u32 = 0x3c;
const PE_SIGNATURE: u32 = 0x00004550;
const SECTION_COUNT_OFFSET: u32 = 0x06;
const OPTIONAL_HEADER_SIZE_OFFSET: u32 = 0x14;
const OPTIONAL_HEADER_OFFSET: u32 = 0x18;
const PE32_MAGIC: u16 = 0x010b;
const PE32_RESOURCE_DIRECTORY_OFFSET: u32 = 0x70;
const PE32PLUS_RESOURCE_DIRECTORY_OFFSET: u32 = 0x80;
const SECTION_SIZE: u32 = 0x28;
const SECTION_VIRTUAL_SIZE_OFFSET: u32 = 0x08;
const SECTION_RVA_OFFSET: u32 = 0x0c;
const SECTION_RAW_DATA_OFFSET: u32 = 0x14;
const RT_ICON: u16 = 3;
const RT_GROUP_ICON: u16 = 14;
const RT_VERSION: u16 = 16;
const NE_SIGNATURE: u16 = 0x454e;
const MAX_PROGRAM_WORD: usize = 24;
const NE_NON_RESIDENT_NAMES_OFFSET: u32 = 0x2c;
const COMPANY_NAME: &[u8] = b"C\0o\0m\0p\0a\0n\0y\0N\0a\0m\0e\0\0\0";
const INTERNAL_NAME: &[u8] = b"I\0n\0t\0e\0r\0n\0a\0l\0N\0a\0m\0e\0\0\0";
const PRODUCT_NAME: &[u8] = b"P\0r\0o\0d\0u\0c\0t\0N\0a\0m\0e\0\0\0";
const ORIGINAL_FILENAME: &[u8] =
b"O\0r\0i\0g\0i\0n\0a\0l\0F\0i\0l\0e\0n\0a\0m\0e\0\0\0";
const FILE_DESCRIPTION: &[u8] = b"F\0i\0l\0e\0D\0e\0s\0c\0r\0i\0p\0t\0i\0o\0n\0\0\0";
const NAMED_ENTRY_COUNT_OFFSET: u32 = 0x0c;
const ID_ENTRY_COUNT_OFFSET: u32 = 0x0e;
const DIRECTORY_HEADER_SIZE: u32 = 0x10;
const DIRECTORY_ENTRY_SIZE: u32 = 0x08;
const DIRECTORY_ENTRY_CHILD_OFFSET: u32 = 0x04;
const SUBDIRECTORY_FLAG: u32 = 0x8000_0000;
const DATA_ENTRY_SIZE_OFFSET: u32 = 0x04;
const GROUP_COUNT_OFFSET: u32 = 0x04;
const GROUP_ENTRIES_OFFSET: u32 = 0x06;
const GROUP_ENTRY_SIZE: u32 = 0x0e;
const GROUP_ENTRY_WIDTH_OFFSET: u32 = 0x00;
const GROUP_ENTRY_HEIGHT_OFFSET: u32 = 0x01;
const GROUP_ENTRY_DEPTH_OFFSET: u32 = 0x06;
const GROUP_ENTRY_ID_OFFSET: u32 = 0x0c;
+139
View File
@@ -0,0 +1,139 @@
// Facade over the host kernel the agent is running inside of. Both backends
// expose the same primitives, so the rest of the agent never names a specific
// kernel.
//
// A kernel is reached from the outside -- a remote stub injects the agent and
// patches the addresses it needs into .kernel_addrs -- or from the inside,
// where the agent is a kernel module whose glue is an ordinary C translation
// unit compiled against the target kernel's headers. Linux is reached either
// way; the others only from the outside.
#[cfg(any(feature = "linux", feature = "linux-injected"))]
pub use crate::linux::*;
#[cfg(feature = "win9x")]
pub use crate::win9x::*;
#[cfg(feature = "winnt")]
pub use crate::winnt::*;
#[cfg(feature = "xnu-core")]
pub use crate::xnu::*;
#[cfg(feature = "xnu-core")]
pub use crate::xnu_applications::*;
#[cfg(feature = "xnu-core")]
pub use crate::xnu_bell::*;
#[cfg(feature = "xnu-core")]
pub use crate::xnu_hiding::*;
#[cfg(feature = "xnu-core")]
pub use crate::xnu_injection::*;
#[cfg(feature = "xnu-core")]
pub use crate::xnu_processes::*;
#[cfg(feature = "xnu-core")]
pub use crate::xnu_relay::*;
#[cfg(feature = "xnu-core")]
pub use crate::xnu_spawn::*;
use core::ffi::c_void;
pub type ThreadEntry = unsafe extern "C" fn(parameter: *mut c_void, wait_result: i32);
pub struct ThreadInfo {
pub id: u32,
pub state: Option<crate::bindings::GumThreadState>,
pub cpu_state: Option<CpuState>,
}
#[cfg(target_arch = "x86_64")]
pub struct CpuState {
pub rip: u64,
pub r15: u64,
pub r14: u64,
pub r13: u64,
pub r12: u64,
pub r11: u64,
pub r10: u64,
pub r9: u64,
pub r8: u64,
pub rdi: u64,
pub rsi: u64,
pub rbp: u64,
pub rsp: u64,
pub rbx: u64,
pub rdx: u64,
pub rcx: u64,
pub rax: u64,
}
#[cfg(target_arch = "arm")]
pub struct CpuState {
pub pc: u32,
pub sp: u32,
pub cpsr: u32,
pub r: [u32; 8],
pub r8: u32,
pub r9: u32,
pub r10: u32,
pub r11: u32,
pub r12: u32,
pub lr: u32,
}
#[cfg(target_arch = "aarch64")]
pub struct CpuState {
pub pc: u64,
pub sp: u64,
pub nzcv: u64,
pub x: [u64; 29],
pub fp: u64,
pub lr: u64,
}
#[cfg(target_arch = "x86")]
pub struct CpuState {
pub eip: u32,
pub edi: u32,
pub esi: u32,
pub ebp: u32,
pub esp: u32,
pub ebx: u32,
pub edx: u32,
pub ecx: u32,
pub eax: u32,
}
pub struct MemoryRegion {
pub base: u64,
pub size: u64,
pub protection: u32,
}
// What the host looked up before the agent started, for the kernels that say nothing about
// their own layout and leave nowhere for the agent to read it.
pub fn take_note_of(what: &str, number: u64) {
unsafe { noted_numbers() }.push((alloc::string::String::from(what), number));
}
pub fn noted(what: &str) -> Option<u64> {
unsafe { noted_numbers() }
.iter()
.find(|(name, _)| name == what)
.map(|(_, number)| *number)
}
pub fn take_note_of_mapping(address: u64, size: usize, protection: u32) {
unsafe { mappings() }.push((address, size, protection));
}
pub fn noted_mappings() -> &'static [(u64, usize, u32)] {
unsafe { mappings() }.as_slice()
}
unsafe fn mappings() -> &'static mut alloc::vec::Vec<(u64, usize, u32)> {
unsafe { (&raw mut MAPPINGS).as_mut().unwrap() }
}
static mut MAPPINGS: alloc::vec::Vec<(u64, usize, u32)> = alloc::vec::Vec::new();
unsafe fn noted_numbers() -> &'static mut alloc::vec::Vec<(alloc::string::String, u64)> {
unsafe { (&raw mut NOTED).as_mut().unwrap() }
}
static mut NOTED: alloc::vec::Vec<(alloc::string::String, u64)> = alloc::vec::Vec::new();
File diff suppressed because it is too large. Load diff
+518 -79
View File
@@ -1,35 +1,185 @@
// The C library's lower edge. Nothing here does real work: the agent has no
// filesystem and never exits, so these exist to satisfy references and to give
// malloc some memory to hand out.
//
// The two flavours link against different C libraries, and they disagree on what
// this layer is called. newlib asks for the underscore-prefixed syscall names;
// picolibc, which the soft-float flavour needs because newlib has no soft-float
// build for arm64, calls them without the prefix. The bodies are shared and the
// exported names live in the two `exports` modules at the end.
use core::ptr;
use core::sync::atomic::Ordering;
use crate::bindings::{GMutex, GRecMutex};
use crate::gthread;
static mut MALLOC_LOCK: GRecMutex = unsafe { core::mem::zeroed() };
// Serialises the C library. Which hooks the library reaches for differs: newlib asks
// for __malloc_lock/__malloc_unlock, picolibc for the retargetable lock API below,
// whose default implementation is an empty function — so on picolibc malloc runs
// unlocked unless we supply these, and any second thread corrupts the heap.
static mut LIBC_LOCK: GRecMutex = unsafe { core::mem::zeroed() };
#[unsafe(no_mangle)]
pub extern "C" fn __malloc_lock(_reent: *mut core::ffi::c_void) {
gthread::g_rec_mutex_lock(ptr::addr_of_mut!(MALLOC_LOCK));
fn libc_lock() {
gthread::g_rec_mutex_lock(ptr::addr_of_mut!(LIBC_LOCK));
}
#[unsafe(no_mangle)]
pub extern "C" fn __malloc_unlock(_reent: *mut core::ffi::c_void) {
gthread::g_rec_mutex_unlock(ptr::addr_of_mut!(MALLOC_LOCK));
fn libc_unlock() {
gthread::g_rec_mutex_unlock(ptr::addr_of_mut!(LIBC_LOCK));
offer_preemption_point();
}
#[unsafe(no_mangle)]
pub extern "C" fn _fini() {
fn offer_preemption_point() {
let total = ALLOCATIONS_TOTAL.fetch_add(1, Ordering::Relaxed) + 1;
if total % ALLOCATIONS_PER_YIELD == 0 {
crate::kernel::yield_now();
}
}
static ALLOCATIONS_TOTAL: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
const ALLOCATIONS_PER_YIELD: u32 = 1024;
// GLib's g_get_num_processors() probes this; report a single CPU so the
// parallel scanners fall back to their inline path.
#[unsafe(no_mangle)]
pub extern "C" fn _exit(_status: i32) -> ! {
loop {}
pub extern "C" fn sysconf(_name: i32) -> isize {
1
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[unsafe(no_mangle)]
pub extern "C" fn _kill(_pid: i32, _sig: i32) -> i32 {
-1
pub extern "C" fn __clear_cache(_start: *const u8, _end: *const u8) {}
#[cfg(all(target_arch = "arm", not(any(feature = "linux", feature = "linux-injected"))))]
#[unsafe(no_mangle)]
pub extern "C" fn __clear_cache(start: *const u8, end: *const u8) {
unsafe {
core::arch::asm!(
"push {{r7}}",
"mov r7, {number}",
"svc #0",
"pop {{r7}}",
number = in(reg) 0xf0002usize,
in("r0") start,
in("r1") end,
in("r2") 0,
options(nostack),
);
}
}
#[cfg(all(target_arch = "arm", any(feature = "linux", feature = "linux-injected")))]
#[unsafe(no_mangle)]
pub extern "C" fn _sbrk(incr: isize) -> *mut u8 {
pub extern "C" fn __clear_cache(start: *const u8, end: *const u8) {
unsafe {
let told: u32;
core::arch::asm!("mrc p15, 0, {}, c0, c0, 1", out(reg) told, options(nomem, nostack));
let data_line = 4usize << ((told >> 16) & 0xf);
let code_line = 4usize << (told & 0xf);
let mut at = (start as usize) & !(data_line - 1);
while at < end as usize {
core::arch::asm!("mcr p15, 0, {}, c7, c11, 1", in(reg) at, options(nostack));
at += data_line;
}
core::arch::asm!("dsb ish", options(nostack));
let mut at = (start as usize) & !(code_line - 1);
while at < end as usize {
core::arch::asm!("mcr p15, 0, {}, c7, c5, 1", in(reg) at, options(nostack));
at += code_line;
}
core::arch::asm!("dsb ish", "isb", options(nostack));
}
}
#[cfg(target_arch = "arm")]
#[unsafe(no_mangle)]
pub extern "C" fn __aeabi_uread4(address: *const u8) -> u32 {
let mut bytes = [0u8; 4];
for i in 0..4 {
bytes[i] = unsafe { address.add(i).read_volatile() };
}
u32::from_ne_bytes(bytes)
}
#[cfg(target_arch = "arm")]
#[unsafe(no_mangle)]
pub extern "C" fn __aeabi_uread8(address: *const u8) -> u64 {
let mut bytes = [0u8; 8];
for i in 0..8 {
bytes[i] = unsafe { address.add(i).read_volatile() };
}
u64::from_ne_bytes(bytes)
}
#[cfg(target_arch = "arm")]
#[unsafe(no_mangle)]
pub extern "C" fn __aeabi_uwrite4(value: u32, address: *mut u8) -> u32 {
let bytes = value.to_ne_bytes();
for i in 0..4 {
unsafe { address.add(i).write_volatile(bytes[i]) };
}
value
}
#[cfg(target_arch = "arm")]
#[unsafe(no_mangle)]
pub extern "C" fn __aeabi_uwrite8(value: u64, address: *mut u8) -> u64 {
let bytes = value.to_ne_bytes();
for i in 0..8 {
unsafe { address.add(i).write_volatile(bytes[i]) };
}
value
}
// Invalidating the whole instruction cache is the kernel's to do, and the copy runs where that
// instruction is not allowed. Line by line is what both halves may ask for.
#[cfg(target_arch = "aarch64")]
#[unsafe(no_mangle)]
pub extern "C" fn __clear_cache(start: *const u8, end: *const u8) {
#[cfg(feature = "winnt")]
if crate::winnt_user::flush_code(start, end) {
return;
}
#[cfg(feature = "xnu-core")]
let told = crate::kernel::cache_shape();
#[cfg(not(feature = "xnu-core"))]
let told: u64 = {
let read: u64;
unsafe { core::arch::asm!("mrs {}, ctr_el0", out(reg) read, options(nomem, nostack)) };
read
};
let data_line = 4usize << ((told >> 16) & 0xf);
let code_line = 4usize << (told & 0xf);
let data_is_coherent = (told >> 28) & 1 != 0;
let code_is_coherent = (told >> 29) & 1 != 0;
if !data_is_coherent {
let mut at = start as usize & !(data_line - 1);
while at < end as usize {
unsafe { core::arch::asm!("dc cvau, {}", in(reg) at, options(nostack)) };
at += data_line;
}
}
unsafe { core::arch::asm!("dsb ish", options(nomem, nostack)) };
if !code_is_coherent {
let mut at = start as usize & !(code_line - 1);
while at < end as usize {
unsafe { core::arch::asm!("ic ivau, {}", in(reg) at, options(nostack)) };
at += code_line;
}
unsafe { core::arch::asm!("dsb ish", options(nomem, nostack)) };
}
unsafe { core::arch::asm!("isb", options(nomem, nostack)) };
}
fn sbrk_impl(incr: isize) -> *mut u8 {
const HEAP_SIZE: usize = 32 * 1024 * 1024; // 32 MB
static mut HEAP_START: *mut u8 = ptr::null_mut();
@@ -40,7 +190,7 @@ pub extern "C" fn _sbrk(incr: isize) -> *mut u8 {
gthread::g_mutex_lock(ptr::addr_of_mut!(HEAP_MUTEX));
if HEAP_START.is_null() {
HEAP_START = crate::xnu::kalloc(HEAP_SIZE);
HEAP_START = crate::kernel::alloc_heap(HEAP_SIZE);
if HEAP_START.is_null() {
gthread::g_mutex_unlock(ptr::addr_of_mut!(HEAP_MUTEX));
panic!("Failed to allocate 32 MB heap");
@@ -49,7 +199,8 @@ pub extern "C" fn _sbrk(incr: isize) -> *mut u8 {
}
let prev_heap_current = HEAP_CURRENT;
let new_heap_current = HEAP_CURRENT.add(incr as usize);
// Signed: the allocator hands memory back by asking for a negative increment.
let new_heap_current = HEAP_CURRENT.offset(incr);
if new_heap_current > HEAP_START.add(HEAP_SIZE) {
gthread::g_mutex_unlock(ptr::addr_of_mut!(HEAP_MUTEX));
@@ -65,20 +216,14 @@ pub extern "C" fn _sbrk(incr: isize) -> *mut u8 {
}
}
#[unsafe(no_mangle)]
pub extern "C" fn _getpid() -> i32 {
0
}
#[repr(C)]
struct Timeval {
tv_sec: i64,
tv_usec: i64,
}
#[unsafe(no_mangle)]
pub extern "C" fn _gettimeofday(tp: *mut core::ffi::c_void, _tzp: *mut core::ffi::c_void) -> i32 {
let (secs, microsecs) = crate::xnu::clock_get_calendar_microtime();
fn gettimeofday_impl(tp: *mut core::ffi::c_void) -> i32 {
let (secs, microsecs) = crate::kernel::wall_clock_micros();
let timeval = Timeval {
tv_sec: secs as i64,
tv_usec: microsecs as i64,
@@ -93,68 +238,362 @@ pub extern "C" fn _gettimeofday(tp: *mut core::ffi::c_void, _tzp: *mut core::ffi
0
}
#[unsafe(no_mangle)]
pub extern "C" fn _isatty(_fd: i32) -> i32 {
1
}
#[unsafe(no_mangle)]
pub extern "C" fn _open(_path: *const u8, _flags: i32, _mode: i32) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn _close(_fd: i32) -> i32 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn _read(_fd: i32, _buf: *mut u8, _count: usize) -> isize {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn _write(_fd: i32, _buf: *const u8, count: usize) -> isize {
fn write_impl(count: usize) -> isize {
count as isize
}
#[unsafe(no_mangle)]
pub extern "C" fn _lseek(_fd: i32, _offset: isize, _whence: i32) -> isize {
0
}
mod exports {
use super::*;
#[unsafe(no_mangle)]
pub extern "C" fn _fstat(_fd: i32, _st: *mut core::ffi::c_void) -> i32 {
0
}
// The C library's own allocator sits on a fixed sbrk heap and carries its own
// locking; inside a kernel module the kernel's allocator is already there, already
// safe to share between threads, and not capped at a size we picked up front.
// Every C allocation goes here instead.
//
// The sixteen bytes before the returned pointer hold the block the kernel gave us
// and its total size, so a pointer that was moved forward to satisfy an alignment
// request frees exactly like one that was not.
use core::ffi::c_void;
#[unsafe(no_mangle)]
pub extern "C" fn _link(_old: *const u8, _new: *const u8) -> i32 {
-1
}
const HEAP_HEADER_SIZE: usize = 16;
#[unsafe(no_mangle)]
pub extern "C" fn _unlink(_path: *const u8) -> i32 {
-1
}
const MALLOC_ALIGNMENT: usize = 2 * core::mem::size_of::<*const u8>();
// GLib's g_get_num_processors() probes this; report a single CPU so the
// parallel scanners fall back to their inline path.
#[unsafe(no_mangle)]
pub extern "C" fn sysconf(_name: i32) -> isize {
1
}
#[unsafe(no_mangle)]
pub extern "C" fn malloc(size: usize) -> *mut c_void {
let total = size + HEAP_HEADER_SIZE + MALLOC_ALIGNMENT;
let block = crate::kernel::alloc(total);
if block.is_null() {
return ptr::null_mut();
}
#[unsafe(no_mangle)]
pub extern "C" fn __clear_cache(_start: *const u8, _end: *const u8) {
// On AArch64, we need to flush the instruction cache
// This is a simplified implementation for the kernel environment
unsafe {
core::arch::asm!(
"ic iallu", // Invalidate all instruction caches
"dsb sy", // Data synchronization barrier
"isb", // Instruction synchronization barrier
options(nomem, nostack),
);
offer_preemption_point();
unsafe {
let lowest = block.add(HEAP_HEADER_SIZE) as usize;
let aligned = (lowest + MALLOC_ALIGNMENT - 1) & !(MALLOC_ALIGNMENT - 1);
attach_header(block, total, aligned as *mut u8)
}
}
#[unsafe(no_mangle)]
pub extern "C" fn posix_memalign(
result: *mut *mut c_void,
alignment: usize,
size: usize,
) -> i32 {
let total = size + alignment + HEAP_HEADER_SIZE;
let block = crate::kernel::alloc(total);
if block.is_null() {
return 12; // ENOMEM
}
unsafe {
let lowest = block.add(HEAP_HEADER_SIZE) as usize;
let aligned = (lowest + alignment - 1) & !(alignment - 1);
*result = attach_header(block, total, aligned as *mut u8);
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn free(p: *mut c_void) {
if p.is_null() {
return;
}
unsafe {
let (block, total) = header_of(p);
crate::kernel::free(block, total);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn malloc_usable_size(p: *mut c_void) -> usize {
unsafe { usable_size(p) }
}
#[unsafe(no_mangle)]
pub extern "C" fn calloc(count: usize, size: usize) -> *mut c_void {
let total = count * size;
let p = malloc(total);
if !p.is_null() {
unsafe { ptr::write_bytes(p as *mut u8, 0, total) };
}
p
}
#[unsafe(no_mangle)]
pub extern "C" fn realloc(p: *mut c_void, size: usize) -> *mut c_void {
if p.is_null() {
return malloc(size);
}
let previous_size = unsafe { usable_size(p) };
let grown = malloc(size);
if !grown.is_null() {
unsafe {
ptr::copy_nonoverlapping(p as *const u8, grown as *mut u8, previous_size.min(size))
};
free(p);
}
grown
}
unsafe fn attach_header(block: *mut u8, total: usize, p: *mut u8) -> *mut c_void {
unsafe {
let header = p.sub(HEAP_HEADER_SIZE) as *mut usize;
*header = block as usize;
*header.add(1) = total;
}
p as *mut c_void
}
unsafe fn header_of(p: *mut c_void) -> (*mut u8, usize) {
unsafe {
let header = (p as *mut u8).sub(HEAP_HEADER_SIZE) as *const usize;
(*header as *mut u8, *header.add(1))
}
}
unsafe fn usable_size(p: *mut c_void) -> usize {
unsafe {
let (block, total) = header_of(p);
total - (p as usize - block as usize)
}
}
// One recursive mutex covers every lock the C library asks for. They are coarse
// and few, and over-serialising them is safe where getting it wrong is not.
type Lock = *mut core::ffi::c_void;
// The lock object itself, which we define so the C library's own no-op lock
// implementation is never pulled out of its archive. Its contents are never
// read: the address is just a token, and one mutex covers them all.
// Nothing here references it, so without #[used] it is dropped before the linker
// ever sees it, and the archive member arrives after all to supply it.
#[allow(non_upper_case_globals)]
#[unsafe(no_mangle)]
#[used]
pub static mut __lock___libc_recursive_mutex: u8 = 0;
#[unsafe(no_mangle)]
pub extern "C" fn __retarget_lock_init(_lock: *mut Lock) {}
#[unsafe(no_mangle)]
pub extern "C" fn __retarget_lock_init_recursive(_lock: *mut Lock) {}
#[unsafe(no_mangle)]
pub extern "C" fn __retarget_lock_close(_lock: Lock) {}
#[unsafe(no_mangle)]
pub extern "C" fn __retarget_lock_close_recursive(_lock: Lock) {}
#[unsafe(no_mangle)]
pub extern "C" fn __retarget_lock_acquire(_lock: Lock) {
libc_lock();
}
#[unsafe(no_mangle)]
pub extern "C" fn __retarget_lock_acquire_recursive(_lock: Lock) {
libc_lock();
}
#[unsafe(no_mangle)]
pub extern "C" fn __retarget_lock_release(_lock: Lock) {
libc_unlock();
}
#[unsafe(no_mangle)]
pub extern "C" fn __retarget_lock_release_recursive(_lock: Lock) {
libc_unlock();
}
#[unsafe(no_mangle)]
pub extern "C" fn exit(_status: i32) -> ! {
loop {}
}
#[unsafe(no_mangle)]
pub extern "C" fn _exit(_status: i32) -> ! {
loop {}
}
#[unsafe(no_mangle)]
pub extern "C" fn kill(_pid: i32, _sig: i32) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn sbrk(incr: isize) -> *mut u8 {
sbrk_impl(incr)
}
#[unsafe(no_mangle)]
pub extern "C" fn getpid() -> i32 {
crate::kernel::current_process_id() as i32
}
#[unsafe(no_mangle)]
pub extern "C" fn gettimeofday(
tp: *mut core::ffi::c_void,
_tzp: *mut core::ffi::c_void,
) -> i32 {
gettimeofday_impl(tp)
}
#[unsafe(no_mangle)]
pub extern "C" fn isatty(_fd: i32) -> i32 {
1
}
#[unsafe(no_mangle)]
pub extern "C" fn open(_path: *const u8, _flags: i32, _mode: i32) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn close(_fd: i32) -> i32 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn read(_fd: i32, _buf: *mut u8, _count: usize) -> isize {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn write(_fd: i32, _buf: *const u8, count: usize) -> isize {
write_impl(count)
}
#[unsafe(no_mangle)]
pub extern "C" fn lseek(_fd: i32, _offset: isize, _whence: i32) -> isize {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn fstat(_fd: i32, _st: *mut core::ffi::c_void) -> i32 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn stat(_path: *const u8, _st: *mut core::ffi::c_void) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn unlink(_path: *const u8) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn rename(_old: *const u8, _new: *const u8) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn access(_path: *const u8, _mode: i32) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn chmod(_path: *const u8, _mode: i32) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn chdir(_path: *const u8) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn rmdir(_path: *const u8) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn gethostname(_name: *mut u8, _len: usize) -> i32 {
-1
}
#[repr(C)]
pub struct Timespec {
tv_sec: i64,
tv_nsec: i64,
}
const CLOCK_MONOTONIC: i32 = 1;
#[unsafe(no_mangle)]
pub extern "C" fn clock_gettime(clock_id: i32, tp: *mut Timespec) -> i32 {
let (secs, nsecs) = if clock_id == CLOCK_MONOTONIC {
let micros = crate::kernel::monotonic_micros();
(micros / 1_000_000, (micros % 1_000_000) * 1000)
} else {
let (secs, micros) = crate::kernel::wall_clock_micros();
(secs as i64, (micros as i64) * 1000)
};
unsafe {
(*tp).tv_sec = secs;
(*tp).tv_nsec = nsecs;
}
0
}
// picolibc consults this for errno values it does not know itself.
#[unsafe(no_mangle)]
pub extern "C" fn _user_strerror(_errnum: i32) -> *const core::ffi::c_char {
ptr::null()
}
// No dynamic loader here. Gum calls dladdr() to describe an address and already
// falls back to printing it raw when the lookup fails.
#[unsafe(no_mangle)]
pub extern "C" fn dladdr(_address: *const core::ffi::c_void, _info: *mut core::ffi::c_void) -> i32 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn dlopen(_file: *const u8, _mode: i32) -> *mut core::ffi::c_void {
ptr::null_mut()
}
#[unsafe(no_mangle)]
pub extern "C" fn dlsym(
_handle: *mut core::ffi::c_void,
_name: *const u8,
) -> *mut core::ffi::c_void {
ptr::null_mut()
}
#[unsafe(no_mangle)]
pub extern "C" fn dlclose(_handle: *mut core::ffi::c_void) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn dlerror() -> *mut core::ffi::c_char {
ptr::null_mut()
}
// picolibc declares the standard streams and leaves defining them to the
// application. The agent has no I/O — its logging goes to the kernel's own log —
// so nothing should ever reach for one of these.
#[repr(transparent)]
pub struct Stream(*mut core::ffi::c_void);
unsafe impl Sync for Stream {}
#[unsafe(no_mangle)]
pub static stdin: Stream = Stream(ptr::null_mut());
#[unsafe(no_mangle)]
pub static stdout: Stream = Stream(ptr::null_mut());
#[unsafe(no_mangle)]
pub static stderr: Stream = Stream(ptr::null_mut());
}
+229
View File
@@ -0,0 +1,229 @@
use core::mem::size_of;
use core::sync::atomic::{AtomicU32, Ordering};
// What the kernel half and the copy leave for each other. Both reach it by its own address, so
// each side is told only where it begins.
#[derive(Clone, Copy)]
pub struct Arena {
begins: usize,
}
impl Arena {
pub fn at(begins: usize) -> Self {
Arena { begins }
}
// Until the frames flow, what the copy has to say is left here for the kernel half to read.
pub fn say(&self, msg: &str) {
let said = msg.as_bytes();
let length = said.iter().position(|byte| *byte == 0).unwrap_or(said.len());
let kept = length.min(SAID_SIZE - 1);
unsafe {
core::ptr::copy_nonoverlapping(said.as_ptr(), (self.begins + SAID) as *mut u8, kept);
((self.begins + SAID + kept) as *mut u8).write(0);
}
}
pub fn page_size(&self) -> u32 {
self.word(PAGE_SIZE).load(Ordering::Acquire)
}
pub fn leave(&self, offset: usize, value: u64) {
unsafe { ((self.begins + offset) as *mut u64).write_volatile(value) };
}
pub fn note(&self, step: u32) {
self.word(PROGRESS).store(step, Ordering::Release);
}
pub fn home(&self) -> u32 {
self.word(HOME).load(Ordering::Acquire)
}
pub fn report_home(&self) {
self.word(REPORTED).store(self.word(HOME).load(Ordering::Acquire), Ordering::Release);
}
pub fn reachable_at(&self, says: u32, hears: u32) {
self.word(SAYS).store(says, Ordering::Release);
self.word(HEARS).store(hears, Ordering::Release);
}
pub fn note_a_thread(&self, thread: u32, is_gone: bool) {
let head = self.word(THREADS_HEAD).load(Ordering::Relaxed);
let tail = self.word(THREADS_TAIL).load(Ordering::Acquire);
if head.wrapping_sub(tail) == THREAD_SLOTS {
return;
}
let slot = (head % THREAD_SLOTS) as usize;
self.word(THREADS + (slot * 4)).store((thread << 1) | is_gone as u32, Ordering::Relaxed);
self.word(THREADS_HEAD).store(head.wrapping_add(1), Ordering::Release);
}
pub fn take_a_thread(&self) -> Option<(u32, bool)> {
let head = self.word(THREADS_HEAD).load(Ordering::Acquire);
let tail = self.word(THREADS_TAIL).load(Ordering::Relaxed);
if head == tail {
return None;
}
let slot = (tail % THREAD_SLOTS) as usize;
let noted = self.word(THREADS + (slot * 4)).load(Ordering::Relaxed);
self.word(THREADS_TAIL).store(tail.wrapping_add(1), Ordering::Release);
Some((noted >> 1, noted & 1 != 0))
}
pub fn holds_a_thread(&self) -> bool {
self.word(THREADS_HEAD).load(Ordering::Acquire)
!= self.word(THREADS_TAIL).load(Ordering::Relaxed)
}
pub fn tell_it_to_go(&self) {
self.word(GO).store(1, Ordering::Release);
}
pub fn was_told_to_go(&self) -> bool {
self.word(GO).load(Ordering::Acquire) != 0
}
pub fn gone(&self) {
self.word(GONE).store(1, Ordering::Release);
}
pub fn has_gone(&self) -> bool {
self.word(GONE).load(Ordering::Acquire) != 0
}
pub fn says_through(&self) -> u32 {
self.word(SAYS).load(Ordering::Acquire)
}
pub fn hears_through(&self) -> u32 {
self.word(HEARS).load(Ordering::Acquire)
}
pub fn tell_the_kernel_half(&self) {
self.word(TO_KERNEL).fetch_add(1, Ordering::AcqRel);
super::user::say_something();
}
#[cfg(target_arch = "aarch64")]
pub fn request_executable(&self, address: u64, size: u64) -> u32 {
self.leave(EXEC_ADDR, address);
self.leave(EXEC_SIZE, size);
let seq = self.word(EXEC_SEQ).load(Ordering::Relaxed).wrapping_add(1).max(1);
self.word(EXEC_SEQ).store(seq, Ordering::Release);
seq
}
#[cfg(target_arch = "aarch64")]
pub fn executable_settled(&self, seq: u32) -> bool {
self.word(EXEC_ACK).load(Ordering::Acquire) == seq
}
#[cfg(target_arch = "aarch64")]
pub fn executable_was_granted(&self) -> bool {
self.word(EXEC_OK).load(Ordering::Acquire) != 0
}
pub fn wants_executable(&self) -> Option<(u64, u64)> {
let seq = self.word(EXEC_SEQ).load(Ordering::Acquire);
if seq == 0 || seq == self.word(EXEC_ACK).load(Ordering::Acquire) {
return None;
}
Some((self.read_wide(EXEC_ADDR), self.read_wide(EXEC_SIZE)))
}
#[cfg(target_arch = "aarch64")]
pub fn grant_executable(&self, ok: bool) {
self.word(EXEC_OK).store(ok as u32, Ordering::Release);
let seq = self.word(EXEC_SEQ).load(Ordering::Acquire);
self.word(EXEC_ACK).store(seq, Ordering::Release);
}
pub fn request_registers(&self, thread: u32) -> u32 {
self.word(REG_TID).store(thread, Ordering::Release);
let seq = self.word(REG_SEQ).load(Ordering::Relaxed).wrapping_add(1).max(1);
self.word(REG_SEQ).store(seq, Ordering::Release);
seq
}
pub fn registers_settled(&self, seq: u32) -> bool {
self.word(REG_ACK).load(Ordering::Acquire) == seq
}
pub fn registers_were_captured(&self) -> bool {
self.word(REG_OK).load(Ordering::Acquire) != 0
}
pub fn captured_registers(&self, into: &mut [u64]) {
for (slot, word) in into.iter_mut().enumerate() {
*word = self.read_wide(REG_STATE + slot * size_of::<u64>());
}
}
pub fn wants_registers(&self) -> Option<u32> {
let seq = self.word(REG_SEQ).load(Ordering::Acquire);
if seq == 0 || seq == self.word(REG_ACK).load(Ordering::Acquire) {
return None;
}
Some(self.word(REG_TID).load(Ordering::Acquire))
}
pub fn deliver_registers(&self, state: Option<&[u64]>) {
if let Some(words) = state {
for (slot, word) in words.iter().enumerate() {
self.leave(REG_STATE + slot * size_of::<u64>(), *word);
}
}
self.word(REG_OK).store(state.is_some() as u32, Ordering::Release);
let seq = self.word(REG_SEQ).load(Ordering::Acquire);
self.word(REG_ACK).store(seq, Ordering::Release);
}
fn read_wide(&self, offset: usize) -> u64 {
unsafe { ((self.begins + offset) as *const u64).read_volatile() }
}
fn word(&self, offset: usize) -> &AtomicU32 {
unsafe { &*((self.begins + offset) as *const AtomicU32) }
}
}
pub const PROGRESS: usize = 20;
pub const PAGE_SIZE: usize = 48;
pub const FAULT_KIND: usize = 24;
pub const FAULT_ADDRESS: usize = 32;
pub const FAULT_PC: usize = 40;
pub const FAULT_LR: usize = 56;
pub const SAID: usize = 64;
pub const GO: usize = 288;
pub const GONE: usize = 292;
const THREADS_HEAD: usize = 296;
const THREADS_TAIL: usize = 300;
const THREADS: usize = 304;
const THREAD_SLOTS: u32 = 32;
pub const SAID_SIZE: usize = 192;
pub const REPORTED: usize = 0;
pub const HOME: usize = 4;
pub const SAYS: usize = 8;
pub const HEARS: usize = 52;
pub const TO_KERNEL: usize = 12;
pub const WOKEN: usize = 16;
const EXEC_SEQ: usize = 432;
const EXEC_ACK: usize = 436;
const EXEC_ADDR: usize = 440;
const EXEC_SIZE: usize = 448;
#[cfg(target_arch = "aarch64")]
const EXEC_OK: usize = 456;
const REG_SEQ: usize = 460;
const REG_ACK: usize = 464;
const REG_TID: usize = 468;
const REG_OK: usize = 472;
const REG_STATE: usize = 480;
+143
View File
@@ -0,0 +1,143 @@
use core::ffi::c_void;
use crate::kernel::ThreadEntry;
pub fn log(msg: &str) {
(primitives().log)(msg)
}
pub fn panic(msg: &str) -> ! {
(primitives().panic)(msg)
}
pub fn alloc(size: usize) -> *mut u8 {
(primitives().alloc)(size)
}
pub fn free(ptr: *mut u8, size: usize) {
(primitives().free)(ptr, size)
}
pub fn alloc_heap(size: usize) -> *mut u8 {
(primitives().alloc_heap)(size)
}
pub fn alloc_code(size: usize) -> *mut u8 {
(primitives().alloc_code)(size)
}
pub fn free_code(ptr: *mut u8, size: usize) {
(primitives().free_code)(ptr, size)
}
pub fn page_size() -> usize {
(primitives().page_size)()
}
pub fn protect(address: u64, size: usize, protection: u32) -> bool {
(primitives().protect)(address, size, protection)
}
pub fn spawn_thread(entry: ThreadEntry, parameter: *mut c_void) -> isize {
(primitives().spawn_thread)(entry, parameter)
}
pub fn wait(token: *const u8, timeout_us: Option<u64>, check: &mut dyn FnMut() -> bool) {
(primitives().wait)(token, timeout_us, check)
}
pub fn wake(token: *const u8) {
(primitives().wake)(token)
}
pub fn yield_now() {
(primitives().yield_now)()
}
pub fn monotonic_micros() -> i64 {
(primitives().monotonic_micros)()
}
pub fn wall_clock_micros() -> (u32, u32) {
(primitives().wall_clock_micros)()
}
pub fn home_process_id() -> u32 {
(primitives().home_process_id)()
}
pub fn current_process_id() -> u32 {
(primitives().current_process_id)()
}
pub fn current_thread_id() -> u64 {
(primitives().current_thread_id)()
}
pub fn install_fault_reporter() {
(primitives().install_fault_reporter)()
}
pub fn poke_loop_wakeup() {
(primitives().poke_loop_wakeup)()
}
pub fn select_user() {
unsafe { ACTIVE = &super::user::USER };
}
pub fn in_copy() -> bool {
core::ptr::eq(primitives(), &super::user::USER)
}
fn primitives() -> &'static Primitives {
unsafe { ACTIVE }
}
static mut ACTIVE: &Primitives = &KERNEL;
static KERNEL: Primitives = Primitives {
log: super::native::log,
panic: super::native::panic,
alloc: super::native::alloc,
free: super::native::free,
alloc_heap: super::native::alloc_heap,
alloc_code: super::native::alloc_code,
free_code: super::native::free_code,
page_size: super::native::page_size,
protect: super::native::protect,
spawn_thread: super::native::spawn_thread,
wait: super::native::wait,
wake: super::native::wake,
yield_now: super::native::yield_now,
monotonic_micros: super::native::monotonic_micros,
wall_clock_micros: super::native::wall_clock_micros,
home_process_id: super::native::current_process_id,
current_process_id: super::native::current_process_id,
current_thread_id: super::native::current_thread_id,
install_fault_reporter: super::native::install_fault_reporter,
poke_loop_wakeup: super::native::poke_loop_wakeup,
};
pub struct Primitives {
pub log: fn(&str),
pub panic: fn(&str) -> !,
pub alloc: fn(usize) -> *mut u8,
pub free: fn(*mut u8, usize),
pub alloc_heap: fn(usize) -> *mut u8,
pub alloc_code: fn(usize) -> *mut u8,
pub free_code: fn(*mut u8, usize),
pub page_size: fn() -> usize,
pub protect: fn(u64, usize, u32) -> bool,
pub spawn_thread: fn(ThreadEntry, *mut c_void) -> isize,
pub wait: fn(*const u8, Option<u64>, &mut dyn FnMut() -> bool),
pub wake: fn(*const u8),
pub yield_now: fn(),
pub monotonic_micros: fn() -> i64,
pub wall_clock_micros: fn() -> (u32, u32),
pub home_process_id: fn() -> u32,
pub current_process_id: fn() -> u32,
pub current_thread_id: fn() -> u64,
pub install_fault_reporter: fn(),
pub poke_loop_wakeup: fn(),
}
@@ -0,0 +1,216 @@
// Linux hostlink over the Android emulator's QEMU pipe, which is how the emulator
// carries adb as well: open /dev/qemu_pipe, name the "pipe:unix:<path>" service,
// and the emulator joins that stream to the host UNIX socket <path>, where
// frida-core listens. Framed like the other hostlinks, with a 4-byte
// little-endian length in front of each payload.
//
// Every open of the pipe is a channel of its own, handed out by the guest's own
// driver, so the agent shares no device with the drivers already running.
use core::cell::UnsafeCell;
use core::ffi::{c_int, c_void};
use core::ptr;
use core::sync::atomic::{AtomicPtr, AtomicU32, Ordering};
use alloc::vec::Vec;
use super::native;
const O_RDWR: c_int = 2;
const PIPE_PATH: &str = "/dev/qemu_pipe\0";
const LAST_ERRNO: usize = 4095;
static PENDING: AtomicU32 = AtomicU32::new(0);
static WAKE_TOKEN: AtomicPtr<u8> = AtomicPtr::new(ptr::null_mut());
struct Shared(UnsafeCell<Vec<u8>>);
unsafe impl Sync for Shared {}
static INCOMING: Shared = Shared(UnsafeCell::new(Vec::new()));
static INCOMING_LOCK: AtomicU32 = AtomicU32::new(0);
static PIPE: AtomicPtr<c_void> = AtomicPtr::new(ptr::null_mut());
pub fn a_turn_is_wanted() -> bool {
PENDING.load(Ordering::Acquire) != 0
}
struct Inner {
file: *mut c_void,
rx_lenbuf: [u8; 4],
rx_lenhave: usize,
rx_buf: Vec<u8>,
rx_have: usize,
rx_need: usize,
}
pub struct Hostlink {
state: UnsafeCell<Inner>,
on_rx: Option<fn(&[u8])>,
}
unsafe impl Send for Hostlink {}
unsafe impl Sync for Hostlink {}
impl Hostlink {
pub fn init(path: &str, on_rx: Option<fn(&[u8])>, wake_token: *const u8) -> Result<Self, ()> {
WAKE_TOKEN.store(wake_token as *mut u8, Ordering::Release);
let file = open_the_pipe()?;
PIPE.store(file, Ordering::Release);
let mut service = Vec::with_capacity(path.len() + 12);
service.extend_from_slice(b"pipe:unix:");
service.extend_from_slice(path.as_bytes());
service.push(0);
if write_all(file, &service).is_err() {
return Err(());
}
native::spawn_thread(read_forever, ptr::null_mut());
Ok(Hostlink {
state: UnsafeCell::new(Inner {
file,
rx_lenbuf: [0; 4],
rx_lenhave: 0,
rx_buf: Vec::new(),
rx_have: 0,
rx_need: 0,
}),
on_rx,
})
}
pub fn send(&self, payload: &[u8]) {
let s = unsafe { &*self.state.get() };
let length = (payload.len() as u32).to_le_bytes();
let _ = write_all(s.file, &length);
let _ = write_all(s.file, payload);
}
pub fn process(&self) {
PENDING.store(0, Ordering::Release);
let arrived = take_what_arrived();
if arrived.is_empty() {
return;
}
let s = unsafe { &mut *self.state.get() };
let mut at = 0;
while at < arrived.len() {
if s.rx_lenhave < 4 {
let want = core::cmp::min(4 - s.rx_lenhave, arrived.len() - at);
s.rx_lenbuf[s.rx_lenhave..s.rx_lenhave + want]
.copy_from_slice(&arrived[at..at + want]);
s.rx_lenhave += want;
at += want;
if s.rx_lenhave < 4 {
break;
}
s.rx_need = u32::from_le_bytes(s.rx_lenbuf) as usize;
s.rx_have = 0;
s.rx_buf.resize(s.rx_need, 0);
}
let want = core::cmp::min(s.rx_need - s.rx_have, arrived.len() - at);
let lo = s.rx_have;
s.rx_buf[lo..lo + want].copy_from_slice(&arrived[at..at + want]);
s.rx_have += want;
at += want;
if s.rx_have < s.rx_need {
break;
}
let frame = core::mem::take(&mut s.rx_buf);
let need = s.rx_need;
s.rx_lenhave = 0;
s.rx_have = 0;
s.rx_need = 0;
if let Some(cb) = self.on_rx {
cb(&frame[..need]);
}
}
}
pub fn shutdown(&self) {
PIPE.store(ptr::null_mut(), Ordering::Release);
}
}
fn open_the_pipe() -> Result<*mut c_void, ()> {
let file = unsafe { _filp_open(PIPE_PATH.as_ptr(), O_RDWR, 0) };
if (file as usize) > usize::MAX - LAST_ERRNO {
return Err(());
}
Ok(file)
}
// The pipe blocks until the host has something, so a thread of its own is what
// reads it; the loop is told there is a turn to take rather than asking.
unsafe extern "C" fn read_forever(_parameter: *mut c_void, _wait_result: i32) {
let mut chunk = [0u8; 4096];
loop {
let file = PIPE.load(Ordering::Acquire);
if file.is_null() {
return;
}
let n = read_some(file, &mut chunk);
if n <= 0 {
return;
}
held(|| unsafe { (*INCOMING.0.get()).extend_from_slice(&chunk[..n as usize]) });
PENDING.store(1, Ordering::Release);
crate::nudge_the_loop(WAKE_TOKEN.load(Ordering::Acquire) as *const u8);
}
}
fn take_what_arrived() -> Vec<u8> {
held(|| core::mem::take(unsafe { &mut *INCOMING.0.get() }))
}
fn held<T>(action: impl FnOnce() -> T) -> T {
while INCOMING_LOCK
.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
core::hint::spin_loop();
}
let outcome = action();
INCOMING_LOCK.store(0, Ordering::Release);
outcome
}
fn read_some(file: *mut c_void, into: &mut [u8]) -> isize {
let mut position: i64 = 0;
unsafe { native::pipe_read(file, into.as_mut_ptr(), into.len(), &mut position) }
}
fn write_all(file: *mut c_void, bytes: &[u8]) -> Result<(), ()> {
let mut sent = 0;
while sent < bytes.len() {
let mut position: i64 = 0;
let n = unsafe {
native::pipe_write(file, bytes[sent..].as_ptr(), bytes.len() - sent, &mut position)
};
if n <= 0 {
return Err(());
}
sent += n as usize;
}
Ok(())
}
unsafe extern "C" {
static _filp_open: unsafe extern "C" fn(*const u8, c_int, c_int) -> *mut c_void;
}
@@ -0,0 +1,280 @@
// Linux hostlink over AF_VSOCK, bridged to the host by the Android emulator's
// pipe-over-vsock connector: connect to CID 2 (host) on port 5002, send a
// NUL-terminated "pipe:unix:<path>" handshake, and the emulator connects that
// stream to the host UNIX socket <path>, where frida-core listens. Framed the
// same way as the other hostlinks (4-byte little-endian length prefix).
//
// The agent uses the guest kernel's own socket API, so it never drives a virtio
// device and never fights the guest's own drivers.
use core::cell::UnsafeCell;
use core::ffi::{c_int, c_void};
use core::ptr;
use core::sync::atomic::{AtomicPtr, AtomicU32, AtomicUsize, Ordering};
use alloc::vec::Vec;
const AF_VSOCK: c_int = 40;
const SOCK_STREAM: c_int = 1;
const VMADDR_CID_HOST: u32 = 2;
const MSG_DONTWAIT: c_int = 0x40;
const PIPE_CONNECTOR_PORT: u32 = 5002;
#[repr(C)]
struct SockaddrVm {
svm_family: u16,
svm_reserved1: u16,
svm_port: u32,
svm_cid: u32,
svm_zero: [u8; 4],
}
#[repr(C)]
struct Kvec {
iov_base: *mut c_void,
iov_len: usize,
}
type Socket = *mut c_void;
static PENDING: AtomicU32 = AtomicU32::new(0);
static WAKE_TOKEN: AtomicPtr<u8> = AtomicPtr::new(ptr::null_mut());
static ORIG_DATA_READY: AtomicUsize = AtomicUsize::new(0);
pub fn a_turn_is_wanted() -> bool {
PENDING.load(Ordering::Acquire) != 0
}
struct Inner {
so: Socket,
rx_lenbuf: [u8; 4],
rx_lenhave: usize,
rx_buf: Vec<u8>,
rx_have: usize,
rx_need: usize,
}
pub struct Hostlink {
state: UnsafeCell<Inner>,
on_rx: Option<fn(&[u8])>,
}
unsafe impl Send for Hostlink {}
impl Hostlink {
pub fn init(path: &str, on_rx: Option<fn(&[u8])>, wake_token: *const u8) -> Result<Self, ()> {
WAKE_TOKEN.store(wake_token as *mut u8, Ordering::Release);
unsafe {
let mut so: Socket = ptr::null_mut();
if _sock_create_kern(_init_net_addr(), AF_VSOCK, SOCK_STREAM, 0, &mut so) != 0
|| so.is_null()
{
return Err(());
}
let addr = SockaddrVm {
svm_family: AF_VSOCK as u16,
svm_reserved1: 0,
svm_port: PIPE_CONNECTOR_PORT,
svm_cid: VMADDR_CID_HOST,
svm_zero: [0; 4],
};
if _kernel_connect(
so,
&addr as *const _ as *const c_void,
core::mem::size_of::<SockaddrVm>() as c_int,
0,
) != 0
{
_sock_release(so);
return Err(());
}
hook_data_ready(so);
let hostlink = Hostlink {
state: UnsafeCell::new(Inner {
so,
rx_lenbuf: [0; 4],
rx_lenhave: 0,
rx_buf: Vec::new(),
rx_have: 0,
rx_need: 0,
}),
on_rx,
};
let mut handshake = Vec::with_capacity(path.len() + 12);
handshake.extend_from_slice(b"pipe:unix:");
handshake.extend_from_slice(path.as_bytes());
handshake.push(0);
send_all(so, &handshake);
Ok(hostlink)
}
}
pub fn send(&self, payload: &[u8]) {
let s = unsafe { &*self.state.get() };
let length = (payload.len() as u32).to_le_bytes();
unsafe {
send_all(s.so, &length);
send_all(s.so, payload);
}
}
pub fn process(&self) {
PENDING.store(0, Ordering::Release);
let s = unsafe { &mut *self.state.get() };
loop {
if s.rx_lenhave < 4 {
let lo = s.rx_lenhave;
let n = unsafe { recv_nonblocking(s.so, &mut s.rx_lenbuf[lo..4]) };
if n == 0 {
return;
}
s.rx_lenhave += n;
if s.rx_lenhave < 4 {
continue;
}
s.rx_need = u32::from_le_bytes(s.rx_lenbuf) as usize;
s.rx_have = 0;
s.rx_buf.resize(s.rx_need, 0);
}
while s.rx_have < s.rx_need {
let lo = s.rx_have;
let hi = s.rx_need;
let n = unsafe { recv_nonblocking(s.so, &mut s.rx_buf[lo..hi]) };
if n == 0 {
return;
}
s.rx_have += n;
}
let frame = core::mem::take(&mut s.rx_buf);
let need = s.rx_need;
s.rx_lenhave = 0;
s.rx_have = 0;
s.rx_need = 0;
if let Some(cb) = self.on_rx {
cb(&frame[..need]);
}
}
}
pub fn shutdown(&self) {
let s = unsafe { &*self.state.get() };
unsafe { _sock_release(s.so) };
}
}
// The vsock stack calls sk->sk_data_ready(sk) via a CONFIG_CFI_CLANG-checked indirect
// call, so the replacement must carry the type id of a void(struct sock*) callback;
// sock_def_readable is the kernel's own one. Store the original and chain to it so the
// socket's normal wakeups still fire.
unsafe fn hook_data_ready(so: Socket) {
let (Some(sk_off), Some(dr_off)) = (
super::layout::field_offset("socket", "sk"),
super::layout::field_offset("sock", "sk_data_ready"),
) else {
return;
};
let sk = unsafe { *((so as usize + sk_off) as *const usize) };
if sk == 0 {
return;
}
let slot = (sk + dr_off) as *mut usize;
ORIG_DATA_READY.store(unsafe { *slot }, Ordering::Release);
let thunk =
super::native::kcfi_thunk(on_data_ready as *const () as usize, sock_def_readable_ref());
unsafe { *slot = thunk };
}
fn sock_def_readable_ref() -> Option<usize> {
let addr = unsafe { _sock_def_readable_addr() };
if addr == 0 { None } else { Some(addr) }
}
unsafe extern "C" fn on_data_ready(sk: *mut c_void) {
PENDING.fetch_add(1, Ordering::Release);
let token = WAKE_TOKEN.load(Ordering::Acquire);
if !token.is_null() {
crate::nudge_the_loop(token as *const u8);
}
let orig = ORIG_DATA_READY.load(Ordering::Acquire);
if orig != 0 {
let f: unsafe extern "C" fn(*mut c_void) = unsafe { core::mem::transmute(orig) };
unsafe { f(sk) };
}
}
unsafe fn recv_nonblocking(so: Socket, dst: &mut [u8]) -> usize {
let mut vec = Kvec {
iov_base: dst.as_mut_ptr() as *mut c_void,
iov_len: dst.len(),
};
let mut msg = [0u64; 16];
let n = unsafe {
_kernel_recvmsg(
so,
msg.as_mut_ptr() as *mut c_void,
&mut vec,
1,
dst.len(),
MSG_DONTWAIT,
)
};
if n <= 0 { 0 } else { n as usize }
}
unsafe fn send_all(so: Socket, bytes: &[u8]) {
let mut gone = 0;
while gone < bytes.len() {
let mut vec = Kvec {
iov_base: unsafe { bytes.as_ptr().add(gone) } as *mut c_void,
iov_len: bytes.len() - gone,
};
let mut msg = [0u64; 16];
let n = unsafe {
_kernel_sendmsg(
so,
msg.as_mut_ptr() as *mut c_void,
&mut vec,
1,
bytes.len() - gone,
)
};
if n <= 0 {
return;
}
gone += n as usize;
}
}
unsafe fn _init_net_addr() -> *mut c_void {
ptr::addr_of!(_init_net) as *mut c_void
}
unsafe fn _sock_def_readable_addr() -> usize {
unsafe { _sock_def_readable }.map_or(0, |f| f as usize)
}
unsafe extern "C" {
static _init_net: c_void;
static _sock_create_kern:
unsafe extern "C" fn(*mut c_void, c_int, c_int, c_int, *mut Socket) -> c_int;
static _kernel_connect:
unsafe extern "C" fn(Socket, *const c_void, c_int, c_int) -> c_int;
static _kernel_sendmsg:
unsafe extern "C" fn(Socket, *mut c_void, *mut Kvec, usize, usize) -> c_int;
static _kernel_recvmsg:
unsafe extern "C" fn(Socket, *mut c_void, *mut Kvec, usize, usize, c_int) -> c_int;
static _sock_release: unsafe extern "C" fn(Socket);
static _sock_def_readable: Option<unsafe extern "C" fn()>;
}
File diff suppressed because it is too large. Load diff
+591
View File
@@ -0,0 +1,591 @@
use core::ffi::{c_int, c_void};
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use core::ffi::c_long;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use core::ptr;
use crate::bindings::{
gum_barebone_handle_exception, GumCpuContext,
_GumExceptionType_GUM_EXCEPTION_ACCESS_VIOLATION as ACCESS_VIOLATION,
};
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[repr(C)]
struct NotifierBlock {
notifier_call: Option<unsafe extern "C" fn(*mut NotifierBlock, c_long, *mut c_void) -> c_int>,
next: *mut NotifierBlock,
priority: c_int,
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
unsafe impl Sync for NotifierBlock {}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
static mut REPORTER: NotifierBlock =
NotifierBlock { notifier_call: None, next: ptr::null_mut(), priority: 0 };
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[repr(C)]
struct DieArgs {
regs: *mut c_void,
message: *const u8,
err: c_long,
trapnr: c_int,
signr: c_int,
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
const DIE_OOPS: c_long = 1;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
const NOTIFY_DONE: c_int = 0;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
const NOTIFY_STOP: c_int = 0x8001;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
unsafe fn dispatch(action: c_long, data: *mut c_void) -> c_int {
if action != DIE_OOPS {
return NOTIFY_DONE;
}
let regs = unsafe { (*(data as *const DieArgs)).regs };
if unsafe { recovered(regs) } {
return NOTIFY_STOP;
}
NOTIFY_DONE
}
#[cfg(target_arch = "x86")]
static mut RESUME_CONTEXT: GumCpuContext = unsafe { core::mem::zeroed() };
#[cfg(target_arch = "x86")]
static mut RESUME_STUB: usize = 0;
#[cfg(target_arch = "x86")]
const RESUME_SCRATCH_SIZE: usize = 512;
#[cfg(target_arch = "x86")]
static mut RESUME_SCRATCH: [u8; RESUME_SCRATCH_SIZE] = [0; RESUME_SCRATCH_SIZE];
#[cfg(target_arch = "x86")]
pub fn install() {
unsafe { build_resume_stub() };
unsafe {
(&raw mut REPORTER).write(NotifierBlock {
notifier_call: Some(frida_kcb_die),
next: ptr::null_mut(),
priority: 0,
});
frida_k_register_die_notifier(&raw mut REPORTER as *mut c_void);
}
}
#[cfg(target_arch = "x86")]
pub fn uninstall() {
unsafe { frida_k_unregister_die_notifier(&raw mut REPORTER as *mut c_void) };
}
#[cfg(target_arch = "x86")]
unsafe fn build_resume_stub() {
let stub = crate::kernel::alloc_code(16);
if stub.is_null() {
return;
}
unsafe {
stub.write(0xbf);
(stub.add(1) as *mut u32).write((&raw mut RESUME_CONTEXT) as u32);
stub.add(5).write(0xe9);
let rel = (frida_i386_resume as *const () as usize).wrapping_sub(stub as usize + 10) as u32;
(stub.add(6) as *mut u32).write(rel);
RESUME_STUB = stub as usize;
}
}
#[cfg(target_arch = "x86")]
unsafe fn recovered(regs: *mut c_void) -> bool {
let faulted_at = unsafe { (regs as *const u32).add(12).read() as usize };
let (base, size) = crate::own_range();
if faulted_at < base || faulted_at >= base + size {
return false;
}
let mut context = unsafe { context_of(regs) };
let handled = unsafe {
gum_barebone_handle_exception(
ACCESS_VIOLATION,
faulted_at as *mut c_void,
faulting_address() as *mut c_void,
&mut context,
)
};
if handled == 0 || unsafe { RESUME_STUB } == 0 {
return false;
}
unsafe {
(&raw mut RESUME_CONTEXT).write(context);
(regs as *mut u32).add(12).write(RESUME_STUB as u32);
}
true
}
#[cfg(target_arch = "x86")]
fn faulting_address() -> usize {
let value: usize;
unsafe { core::arch::asm!("mov {}, cr2", out(reg) value, options(nomem, nostack)) };
value
}
#[cfg(target_arch = "x86")]
unsafe fn context_of(regs: *mut c_void) -> GumCpuContext {
let r = regs as *const u32;
let mut c: GumCpuContext = unsafe { core::mem::zeroed() };
unsafe {
c.ebx = r.add(0).read();
c.ecx = r.add(1).read();
c.edx = r.add(2).read();
c.esi = r.add(3).read();
c.edi = r.add(4).read();
c.ebp = r.add(5).read();
c.eax = r.add(6).read();
c.eip = r.add(12).read();
c.esp = ((&raw mut RESUME_SCRATCH) as usize + RESUME_SCRATCH_SIZE - 16) as u32;
}
c
}
#[cfg(target_arch = "x86")]
#[unsafe(naked)]
unsafe extern "C" fn frida_i386_resume() {
core::arch::naked_asm!(
"mov esi, [edi + 8]",
"mov ebp, [edi + 12]",
"mov ebx, [edi + 20]",
"mov edx, [edi + 24]",
"mov ecx, [edi + 28]",
"mov eax, [edi + 32]",
"mov esp, [edi + 16]",
"push dword ptr [edi]",
"mov edi, [edi + 4]",
"ret",
);
}
#[cfg(target_arch = "x86")]
#[unsafe(no_mangle)]
unsafe extern "C" fn frida_cb_die(_nb: *mut NotifierBlock, action: c_long, data: *mut c_void) -> c_int {
unsafe { dispatch(action, data) }
}
#[cfg(target_arch = "x86")]
unsafe extern "C" {
fn frida_kcb_die(nb: *mut NotifierBlock, action: c_long, data: *mut c_void) -> c_int;
fn frida_k_register_die_notifier(nb: *mut c_void) -> c_int;
fn frida_k_unregister_die_notifier(nb: *mut c_void) -> c_int;
}
#[cfg(target_arch = "x86_64")]
pub fn install() {
unsafe {
(&raw mut REPORTER).write(NotifierBlock {
notifier_call: Some(on_die),
next: ptr::null_mut(),
priority: 0,
});
_register_die_notifier(&raw mut REPORTER as *mut c_void);
}
}
#[cfg(target_arch = "x86_64")]
pub fn uninstall() {
unsafe { _unregister_die_notifier(&raw mut REPORTER as *mut c_void) };
}
#[cfg(target_arch = "x86_64")]
unsafe extern "C" fn on_die(_nb: *mut NotifierBlock, action: c_long, data: *mut c_void) -> c_int {
unsafe { dispatch(action, data) }
}
#[cfg(target_arch = "x86_64")]
unsafe fn recovered(regs: *mut c_void) -> bool {
let faulted_at = unsafe { program_counter(regs) };
let (base, size) = crate::own_range();
if faulted_at < base || faulted_at >= base + size {
return false;
}
let mut context = unsafe { context_of(regs) };
let handled = unsafe {
gum_barebone_handle_exception(
ACCESS_VIOLATION,
faulted_at as *mut c_void,
faulting_address() as *mut c_void,
&mut context,
)
};
if handled == 0 {
return false;
}
unsafe { restore_context(regs, &context) };
true
}
#[cfg(target_arch = "x86_64")]
fn faulting_address() -> usize {
let value: usize;
unsafe { core::arch::asm!("mov {}, cr2", out(reg) value, options(nomem, nostack)) };
value
}
#[cfg(target_arch = "x86_64")]
unsafe fn program_counter(regs: *mut c_void) -> usize {
unsafe { (regs as *const u64).add(16).read() as usize }
}
#[cfg(target_arch = "x86_64")]
unsafe fn context_of(regs: *mut c_void) -> GumCpuContext {
let r = regs as *const u64;
let mut c: GumCpuContext = unsafe { core::mem::zeroed() };
unsafe {
c.r15 = r.add(0).read();
c.r14 = r.add(1).read();
c.r13 = r.add(2).read();
c.r12 = r.add(3).read();
c.rbp = r.add(4).read();
c.rbx = r.add(5).read();
c.r11 = r.add(6).read();
c.r10 = r.add(7).read();
c.r9 = r.add(8).read();
c.r8 = r.add(9).read();
c.rax = r.add(10).read();
c.rcx = r.add(11).read();
c.rdx = r.add(12).read();
c.rsi = r.add(13).read();
c.rdi = r.add(14).read();
c.rip = r.add(16).read();
c.rsp = r.add(19).read();
}
c
}
#[cfg(target_arch = "x86_64")]
unsafe fn restore_context(regs: *mut c_void, c: &GumCpuContext) {
let r = regs as *mut u64;
unsafe {
r.add(0).write(c.r15);
r.add(1).write(c.r14);
r.add(2).write(c.r13);
r.add(3).write(c.r12);
r.add(4).write(c.rbp);
r.add(5).write(c.rbx);
r.add(6).write(c.r11);
r.add(7).write(c.r10);
r.add(8).write(c.r9);
r.add(9).write(c.r8);
r.add(10).write(c.rax);
r.add(11).write(c.rcx);
r.add(12).write(c.rdx);
r.add(13).write(c.rsi);
r.add(14).write(c.rdi);
r.add(16).write(c.rip);
r.add(19).write(c.rsp);
}
}
#[cfg(target_arch = "x86_64")]
unsafe extern "C" {
static _register_die_notifier: unsafe extern "C" fn(*mut c_void) -> c_int;
static _unregister_die_notifier: unsafe extern "C" fn(*mut c_void) -> c_int;
}
#[cfg(target_arch = "arm")]
const PROLOGUE: [u32; 2] = [0xe92d4010, 0xe52de004];
#[cfg(target_arch = "arm")]
static mut ORIGINAL_STUB: usize = 0;
#[cfg(target_arch = "arm")]
pub fn install() {
let target = unsafe { _fixup_exception } as *const u32;
for slot in 0..2 {
if unsafe { target.add(slot).read_volatile() } != PROLOGUE[slot] {
return;
}
}
unsafe { build_original_stub(target) };
if unsafe { ORIGINAL_STUB } == 0 {
return;
}
unsafe {
let addr = target as *mut u32;
_patch_text(addr.add(1) as *mut c_void, frida_arm_fixup as *const () as usize as u32);
_patch_text(addr as *mut c_void, 0xe51ff004);
}
}
#[cfg(target_arch = "arm")]
pub fn uninstall() {
let target = unsafe { _fixup_exception } as *mut u32;
unsafe {
_patch_text(target.add(1) as *mut c_void, PROLOGUE[1]);
_patch_text(target as *mut c_void, PROLOGUE[0]);
}
}
#[cfg(target_arch = "arm")]
unsafe fn build_original_stub(target: *const u32) {
let stub = crate::kernel::alloc_code(16) as *mut u32;
if stub.is_null() {
return;
}
unsafe {
stub.add(0).write(PROLOGUE[0]);
stub.add(1).write(PROLOGUE[1]);
stub.add(2).write(0xe51ff004);
stub.add(3).write(target.add(2) as u32);
ORIGINAL_STUB = stub as usize;
}
}
#[cfg(target_arch = "arm")]
unsafe extern "C" fn frida_arm_fixup(regs: *mut c_void) -> c_int {
if unsafe { recovered(regs) } {
return 1;
}
let original: unsafe extern "C" fn(*mut c_void) -> c_int =
unsafe { core::mem::transmute(ORIGINAL_STUB) };
unsafe { original(regs) }
}
#[cfg(target_arch = "arm")]
unsafe fn recovered(regs: *mut c_void) -> bool {
let faulted_at = unsafe { (regs as *const u32).add(15).read() as usize };
let (base, size) = crate::own_range();
if faulted_at < base || faulted_at >= base + size {
return false;
}
let accessed: usize;
unsafe { core::arch::asm!("mrc p15, 0, {}, c6, c0, 0", out(reg) accessed, options(nomem, nostack)) };
let mut context = unsafe { context_of(regs) };
let handled = unsafe {
gum_barebone_handle_exception(
ACCESS_VIOLATION,
faulted_at as *mut c_void,
accessed as *mut c_void,
&mut context,
)
};
if handled == 0 {
return false;
}
unsafe { restore_context(regs, &context) };
true
}
#[cfg(target_arch = "arm")]
unsafe fn context_of(regs: *mut c_void) -> GumCpuContext {
let g = regs as *const u32;
let mut r = [0u32; 8];
for slot in 0..8 {
r[slot] = unsafe { g.add(slot).read() };
}
let mut c: GumCpuContext = unsafe { core::mem::zeroed() };
c.r = r;
unsafe {
c.r8 = g.add(8).read();
c.r9 = g.add(9).read();
c.r10 = g.add(10).read();
c.r11 = g.add(11).read();
c.r12 = g.add(12).read();
c.sp = g.add(13).read();
c.lr = g.add(14).read();
c.pc = g.add(15).read();
c.cpsr = g.add(16).read();
}
c
}
#[cfg(target_arch = "arm")]
unsafe fn restore_context(regs: *mut c_void, c: &GumCpuContext) {
let g = regs as *mut u32;
unsafe {
for slot in 0..8 {
g.add(slot).write(c.r[slot]);
}
g.add(8).write(c.r8);
g.add(9).write(c.r9);
g.add(10).write(c.r10);
g.add(11).write(c.r11);
g.add(12).write(c.r12);
g.add(13).write(c.sp);
g.add(14).write(c.lr);
g.add(15).write(c.pc);
g.add(16).write(c.cpsr);
}
}
#[cfg(target_arch = "arm")]
unsafe extern "C" {
static _fixup_exception: unsafe extern "C" fn();
static _patch_text: unsafe extern "C" fn(*mut c_void, u32);
}
#[cfg(target_arch = "aarch64")]
const PROLOGUE: [u32; 4] = [0xd503233f, 0xa9be7bfd, 0x910003fd, 0xa90153f3];
#[cfg(target_arch = "aarch64")]
#[unsafe(no_mangle)]
static mut FRIDA_ARM64_ORIGINAL_CONT: u64 = 0;
#[cfg(target_arch = "aarch64")]
pub fn install() {
let target = unsafe { _fixup_exception } as *const u32;
for slot in 0..4 {
if unsafe { target.add(slot).read_volatile() } != PROLOGUE[slot] {
return;
}
}
unsafe { FRIDA_ARM64_ORIGINAL_CONT = target.add(4) as u64 };
let jump = detour_to(frida_arm64_fixup as *const () as usize);
unsafe {
let addr = target as *mut u32;
_aarch64_insn_patch_text_nosync(addr.add(2) as *mut c_void, jump[2]);
_aarch64_insn_patch_text_nosync(addr.add(3) as *mut c_void, jump[3]);
_aarch64_insn_patch_text_nosync(addr.add(1) as *mut c_void, jump[1]);
_aarch64_insn_patch_text_nosync(addr as *mut c_void, jump[0]);
}
}
#[cfg(target_arch = "aarch64")]
pub fn uninstall() {
let target = unsafe { _fixup_exception } as *mut u32;
unsafe {
for slot in 0..4 {
_aarch64_insn_patch_text_nosync(target.add(slot) as *mut c_void, PROLOGUE[slot]);
}
}
}
#[cfg(target_arch = "aarch64")]
fn detour_to(destination: usize) -> [u32; 4] {
[0x58000050, 0xd61f0200, destination as u32, (destination >> 32) as u32]
}
#[cfg(target_arch = "aarch64")]
core::arch::global_asm!(
".globl frida_arm64_call_original",
"frida_arm64_call_original:",
"paciasp",
"stp x29, x30, [sp, #-32]!",
"mov x29, sp",
"stp x19, x20, [sp, #16]",
"adrp x16, {cont}",
"add x16, x16, :lo12:{cont}",
"ldr x16, [x16]",
"br x16",
cont = sym FRIDA_ARM64_ORIGINAL_CONT,
);
#[cfg(target_arch = "aarch64")]
unsafe extern "C" {
fn frida_arm64_call_original(regs: *mut c_void) -> c_int;
}
#[cfg(target_arch = "aarch64")]
unsafe extern "C" fn frida_arm64_fixup(regs: *mut c_void) -> c_int {
if unsafe { recovered(regs) } {
return 1;
}
unsafe { frida_arm64_call_original(regs) }
}
#[cfg(target_arch = "aarch64")]
unsafe fn recovered(regs: *mut c_void) -> bool {
let faulted_at = unsafe { (regs as *const u64).add(32).read() as usize };
let (base, size) = crate::own_range();
if faulted_at < base || faulted_at >= base + size {
return false;
}
let accessed: usize;
unsafe { core::arch::asm!("mrs {}, far_el1", out(reg) accessed, options(nomem, nostack)) };
let mut context = unsafe { context_of(regs) };
let handled = unsafe {
gum_barebone_handle_exception(
ACCESS_VIOLATION,
faulted_at as *mut c_void,
accessed as *mut c_void,
&mut context,
)
};
if handled == 0 {
let esr: u64;
unsafe { core::arch::asm!("mrs {}, esr_el1", out(reg) esr, options(nomem, nostack)) };
crate::note_unhandled_fault(esr, faulted_at as u64, accessed as u64);
context.pc = frida_arm64_park as *const () as usize as u64;
unsafe { restore_context(regs, &context) };
return true;
}
unsafe { restore_context(regs, &context) };
true
}
#[cfg(target_arch = "aarch64")]
extern "C" fn frida_arm64_park() -> ! {
static mut PARK_TOKEN: u8 = 0;
loop {
crate::kernel::wait(core::ptr::addr_of!(PARK_TOKEN), Some(1_000_000), &mut || false);
}
}
#[cfg(target_arch = "aarch64")]
unsafe fn context_of(regs: *mut c_void) -> GumCpuContext {
let r = regs as *const u64;
let mut x = [0u64; 29];
for slot in 0..29 {
x[slot] = unsafe { r.add(slot).read() };
}
GumCpuContext {
pc: unsafe { r.add(32).read() },
sp: unsafe { r.add(31).read() },
nzcv: unsafe { r.add(33).read() },
x,
fp: unsafe { r.add(29).read() },
lr: unsafe { r.add(30).read() },
}
}
#[cfg(target_arch = "aarch64")]
unsafe fn restore_context(regs: *mut c_void, c: &GumCpuContext) {
let r = regs as *mut u64;
unsafe {
for slot in 0..29 {
r.add(slot).write(c.x[slot]);
}
r.add(29).write(c.fp);
r.add(30).write(c.lr);
r.add(31).write(c.sp);
r.add(32).write(c.pc);
r.add(33).write(c.nzcv);
}
}
#[cfg(target_arch = "aarch64")]
unsafe extern "C" {
static _fixup_exception: unsafe extern "C" fn();
static _aarch64_insn_patch_text_nosync: unsafe extern "C" fn(*mut c_void, u32) -> c_int;
}
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "arm", target_arch = "aarch64")))]
pub fn install() {}
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "arm", target_arch = "aarch64")))]
pub fn uninstall() {}
+357
View File
@@ -0,0 +1,357 @@
// The agent as a kernel module: every primitive is an ordinary call into
// linux/frida-kmod.c, compiled by kbuild against the target kernel's headers,
// which is what keeps us out of the business of guessing struct layouts,
// config-dependent macros and per-version symbol names.
use core::ffi::{CStr, c_char, c_int, c_void};
use alloc::string::String;
use alloc::vec::Vec;
use super::{LoadedModule, ModuleEvent};
use crate::kernel::ThreadEntry;
pub fn log(msg: &str) {
unsafe { frida_kmod_log(msg.as_ptr() as *const c_char) };
}
pub fn panic(msg: &str) -> ! {
unsafe { frida_kmod_panic(msg.as_ptr() as *const c_char) }
}
pub fn run_when_ready(action: fn()) {
action();
}
pub fn spawn_thread(entry: ThreadEntry, parameter: *mut c_void) -> isize {
unsafe { frida_kmod_spawn_thread(entry, parameter) as isize }
}
pub fn install_hooks() {
unsafe { frida_kmod_install_hooks() };
}
pub fn alloc(size: usize) -> *mut u8 {
unsafe { frida_kmod_alloc(size) }
}
pub fn free(ptr: *mut u8, size: usize) {
unsafe { frida_kmod_free(ptr, size) };
}
pub fn alloc_code(size: usize) -> *mut u8 {
unsafe { frida_kmod_alloc_code(size) }
}
pub fn free_code(ptr: *mut u8, size: usize) {
unsafe { frida_kmod_free_code(ptr, size) };
}
pub fn page_size() -> usize {
crate::gum::page_size_the_kernel_runs_with()
}
pub fn alloc_heap(size: usize) -> *mut u8 {
alloc(size)
}
pub fn alloc_dma(size: usize) -> *mut u8 {
alloc_code(size)
}
pub fn free_dma(ptr: *mut u8, size: usize) {
free_code(ptr, size);
}
pub fn own_range() -> (u64, u64) {
let mut base: u64 = 0;
let mut size: u64 = 0;
unsafe { frida_kmod_own_range(&mut base, &mut size) };
(base, size)
}
// The sequence number is taken before `check` runs, so a wakeup racing with the
// condition we are about to observe still makes the commit return immediately.
pub fn wait(_token: *const u8, timeout_us: Option<u64>, check: &mut dyn FnMut() -> bool) {
let seq = unsafe { frida_kmod_wait_prepare() };
if check() {
return;
}
unsafe { frida_kmod_wait_commit(seq, timeout_us.map_or(-1, |us| us as i64)) };
}
pub fn poke_loop_wakeup() {}
pub fn wake(_token: *const u8) {
unsafe { frida_kmod_wake() };
}
/// Gives the scheduler a chance to run something else. A kernel thread that loops
/// without doing this is a hard lockup waiting to happen.
pub fn yield_now() {
unsafe { frida_kmod_yield() };
}
pub fn monotonic_micros() -> i64 {
unsafe { frida_kmod_monotonic_micros() }
}
pub fn wall_clock_micros() -> (u32, u32) {
let mut secs: u32 = 0;
let mut micros: u32 = 0;
unsafe { frida_kmod_wall_clock_micros(&mut secs, &mut micros) };
(secs, micros)
}
pub fn current_process_id() -> u32 {
KERNEL_PROCESS
}
pub fn current_thread_id() -> u64 {
unsafe { frida_kmod_current_thread_id() }
}
pub fn protect(address: u64, size: usize, gum_prot: u32) -> bool {
unsafe { frida_kmod_protect(address, size, gum_prot) != 0 }
}
pub fn remap_writable(first_page: u64, n_pages: u32) -> *mut c_void {
unsafe { frida_kmod_remap_writable(first_page, n_pages) }
}
/// Tears down an alias handed out by [`remap_writable`] and flushes the icache
/// over the range it aliased; the shim remembers which range that was.
pub fn unmap_writable(mapping: *mut c_void) {
unsafe { frida_kmod_unmap_writable(mapping) };
}
pub fn get_kernel_base() -> u64 {
unsafe { frida_kmod_kernel_base() }
}
pub fn get_kernel_size() -> u64 {
unsafe { frida_kmod_kernel_size() }
}
pub fn enumerate_modules() -> Vec<LoadedModule> {
unsafe extern "C" fn on_module(
name: *const c_char,
version: *const c_char,
base: u64,
size: u64,
user_data: *mut c_void,
) -> c_int {
unsafe {
let modules = &mut *(user_data as *mut Vec<LoadedModule>);
modules.push(LoadedModule {
name: String::from(CStr::from_ptr(name).to_str().unwrap_or("?")),
version: String::from(CStr::from_ptr(version).to_str().unwrap_or("")),
base,
size,
});
1
}
}
let mut modules: Vec<LoadedModule> = Vec::new();
unsafe {
frida_kmod_enumerate_modules(on_module, &mut modules as *mut _ as *mut c_void);
}
modules
}
pub fn watch_modules(on_event: fn(ModuleEvent, LoadedModule)) {
unsafe extern "C" fn on_module_event(
loaded: c_int,
name: *const c_char,
version: *const c_char,
base: u64,
size: u64,
user_data: *mut c_void,
) {
unsafe {
let on_event: fn(ModuleEvent, LoadedModule) = core::mem::transmute(user_data);
on_event(
if loaded != 0 {
ModuleEvent::Loaded
} else {
ModuleEvent::Unloaded
},
LoadedModule {
name: String::from(CStr::from_ptr(name).to_str().unwrap_or("?")),
version: String::from(CStr::from_ptr(version).to_str().unwrap_or("")),
base,
size,
},
);
}
}
unsafe {
frida_kmod_watch_modules(on_module_event, on_event as *mut c_void);
}
}
pub fn unwatch_modules() {
unsafe { frida_kmod_unwatch_modules() };
}
pub fn enumerate_module_symbols(
base: u64,
on_symbol: &mut dyn FnMut(*const c_char, u64, u64, u8, bool) -> bool,
) -> bool {
unsafe extern "C" fn on_module_symbol(
name: *const c_char,
address: u64,
size: u64,
kind: u8,
is_global: c_int,
user_data: *mut c_void,
) -> c_int {
unsafe {
let callback =
&mut *(user_data as *mut &mut dyn FnMut(*const c_char, u64, u64, u8, bool) -> bool);
if callback(name, address, size, kind, is_global != 0) { 1 } else { 0 }
}
}
let mut boxed: &mut dyn FnMut(*const c_char, u64, u64, u8, bool) -> bool = on_symbol;
unsafe {
frida_kmod_enumerate_module_symbols(base, on_module_symbol, &mut boxed as *mut _ as *mut c_void) != 0
}
}
pub fn enumerate_module_exports(
base: u64,
on_export: &mut dyn FnMut(*const c_char, u64) -> bool,
) -> bool {
unsafe extern "C" fn on_module_export(
name: *const c_char,
address: u64,
user_data: *mut c_void,
) -> c_int {
unsafe {
let callback = &mut *(user_data as *mut &mut dyn FnMut(*const c_char, u64) -> bool);
if callback(name, address) { 1 } else { 0 }
}
}
let mut boxed: &mut dyn FnMut(*const c_char, u64) -> bool = on_export;
unsafe {
frida_kmod_enumerate_module_exports(base, on_module_export, &mut boxed as *mut _ as *mut c_void) != 0
}
}
pub fn find_symbol(name: &CStr) -> u64 {
unsafe { frida_kmod_find_symbol(name.as_ptr()) }
}
pub fn find_function(name: &CStr) -> u64 {
unsafe { frida_kmod_find_function(name.as_ptr()) }
}
/// Resolves `address` to `(name, symbol_address)`, or `None` when it falls
/// outside every known symbol.
pub fn symbol_from_address(address: u64) -> Option<(String, u64)> {
const NAME_CAPACITY: usize = 256;
let mut buffer = [0u8; NAME_CAPACITY];
let mut symbol_address: u64 = 0;
let found = unsafe {
frida_kmod_symbol_name_from_address(
address,
buffer.as_mut_ptr() as *mut c_char,
NAME_CAPACITY,
&mut symbol_address,
)
};
if found == 0 {
return None;
}
let name = CStr::from_bytes_until_nul(&buffer).unwrap();
Some((String::from(name.to_str().unwrap()), symbol_address))
}
/// Visits every symbol the running kernel knows about. Returns false when
/// kallsyms is unavailable, which is the case on kernels built without
/// CONFIG_KALLSYMS or when the shim could not resolve its entry points.
pub fn enumerate_symbols(callback: &mut dyn FnMut(&CStr, u64) -> bool) -> bool {
unsafe extern "C" fn on_symbol(
name: *const c_char,
address: u64,
user_data: *mut c_void,
) -> c_int {
unsafe {
let callback = &mut *(user_data as *mut &mut dyn FnMut(&CStr, u64) -> bool);
if callback(CStr::from_ptr(name), address) { 1 } else { 0 }
}
}
let mut boxed: &mut dyn FnMut(&CStr, u64) -> bool = callback;
unsafe { frida_kmod_enumerate_symbols(on_symbol, &mut boxed as *mut _ as *mut c_void) != 0 }
}
const KERNEL_PROCESS: u32 = 0;
type FoundSymbolFunc =
unsafe extern "C" fn(name: *const c_char, address: u64, user_data: *mut c_void) -> c_int;
type ModuleEventFunc = unsafe extern "C" fn(c_int, *const c_char, *const c_char, u64, u64, *mut c_void);
type FoundModuleSymbolFunc =
unsafe extern "C" fn(*const c_char, u64, u64, u8, c_int, *mut c_void) -> c_int;
type FoundModuleExportFunc = unsafe extern "C" fn(*const c_char, u64, *mut c_void) -> c_int;
type FoundModuleFunc = unsafe extern "C" fn(
name: *const c_char,
version: *const c_char,
base: u64,
size: u64,
user_data: *mut c_void,
) -> c_int;
unsafe extern "C" {
fn frida_kmod_log(message: *const c_char);
fn frida_kmod_panic(message: *const c_char) -> !;
fn frida_kmod_spawn_thread(entry: ThreadEntry, parameter: *mut c_void) -> c_int;
fn frida_kmod_install_hooks();
fn frida_kmod_alloc(size: usize) -> *mut u8;
fn frida_kmod_free(ptr: *mut u8, size: usize);
fn frida_kmod_alloc_code(size: usize) -> *mut u8;
fn frida_kmod_free_code(ptr: *mut u8, size: usize);
fn frida_kmod_own_range(base: *mut u64, size: *mut u64);
fn frida_kmod_wait_prepare() -> u32;
fn frida_kmod_wait_commit(seq: u32, timeout_us: i64);
fn frida_kmod_wake();
fn frida_kmod_yield();
fn frida_kmod_monotonic_micros() -> i64;
fn frida_kmod_wall_clock_micros(secs: *mut u32, micros: *mut u32);
fn frida_kmod_current_thread_id() -> u64;
fn frida_kmod_protect(address: u64, size: usize, gum_prot: u32) -> c_int;
fn frida_kmod_remap_writable(first_page: u64, n_pages: u32) -> *mut c_void;
fn frida_kmod_unmap_writable(mapping: *mut c_void);
fn frida_kmod_kernel_base() -> u64;
fn frida_kmod_kernel_size() -> u64;
fn frida_kmod_enumerate_modules(func: FoundModuleFunc, user_data: *mut c_void);
fn frida_kmod_watch_modules(func: ModuleEventFunc, user_data: *mut c_void);
fn frida_kmod_unwatch_modules();
fn frida_kmod_enumerate_module_symbols(
base: u64,
func: FoundModuleSymbolFunc,
user_data: *mut c_void,
) -> c_int;
fn frida_kmod_enumerate_module_exports(
base: u64,
func: FoundModuleExportFunc,
user_data: *mut c_void,
) -> c_int;
fn frida_kmod_find_symbol(name: *const c_char) -> u64;
fn frida_kmod_find_function(name: *const c_char) -> u64;
fn frida_kmod_symbol_name_from_address(
address: u64,
buffer: *mut c_char,
size: usize,
symbol_address: *mut u64,
) -> c_int;
fn frida_kmod_enumerate_symbols(func: FoundSymbolFunc, user_data: *mut c_void) -> c_int;
}
+622
View File
@@ -0,0 +1,622 @@
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use core::ffi::c_void;
pub fn field_offset(container: &str, field: &str) -> Option<usize> {
let types = described_types()?;
let described = types.named(container)?;
types.offset_of(&described, field).map(|bits| bits / 8)
}
pub fn resolve_struct(container: &str) -> Option<usize> {
let described = described_types()?.named(container)?;
let resolved = unsafe { (&raw mut RESOLVED).as_mut().unwrap() };
resolved.push(described);
Some(resolved.len() - 1)
}
pub fn size_of_struct(handle: usize) -> usize {
resolved_struct(handle).size as usize
}
pub fn field_offset_in(handle: usize, field: &str) -> Option<usize> {
let types = described_types()?;
types
.offset_of(&resolved_struct(handle), field)
.map(|bits| bits / 8)
}
pub fn enumerate_fields_in(handle: usize, on_field: &mut dyn FnMut(&str, &Field) -> bool) {
let Some(types) = described_types() else {
return;
};
types.visit_fields(&resolved_struct(handle), 0, on_field);
}
pub fn find_constant(name: &str) -> Option<i64> {
described_types()?.constant_named(name)
}
pub fn enumerate_constants_in(container: &str, on_constant: &mut dyn FnMut(&str, i64)) -> bool {
let Some(types) = described_types() else {
return false;
};
let Some(described) = types.enum_named(container) else {
return false;
};
types.visit_constants(&described, &mut |name, value| {
on_constant(name, value);
true
});
true
}
pub fn enumerate_parameters_in(
name: &str,
on_parameter: &mut dyn FnMut(&str, &str),
) -> Option<String> {
let types = described_types()?;
let described = types.function_named(name)?;
let signature = types.with_id(described.size)?;
types.visit_parameters(&signature, on_parameter);
Some(types.render(signature.size, 0))
}
pub fn name_of_type(id: u32) -> String {
described_types().map_or_else(String::new, |types| types.render(id, 0))
}
pub fn size_of_type(id: u32) -> usize {
described_types().map_or(0, |types| types.span_of(id, 0))
}
pub struct Field {
pub offset: usize,
pub bit_offset: usize,
pub bit_size: usize,
pub id: u32,
}
fn resolved_struct(handle: usize) -> Described {
unsafe { (&raw const RESOLVED).as_ref().unwrap()[handle].clone() }
}
pub fn types_are_described() -> bool {
described_types().is_some()
}
pub fn struct_size(container: &str) -> Option<usize> {
let types = described_types()?;
Some(types.named(container)?.size as usize)
}
struct DescribedTypes {
types: &'static [u8],
names: &'static [u8],
}
impl DescribedTypes {
fn named(&self, container: &str) -> Option<Described> {
let index = self.index();
if let Some(offset) = index.containers.get(container) {
return self.at(*offset as usize);
}
while let Some(described) = self.take_in_next_type(index) {
if self.names_container(&described, container) {
return Some(described);
}
}
None
}
fn with_id(&self, wanted: u32) -> Option<Described> {
if wanted == 0 {
return None;
}
let index = self.index();
let checkpoint = (wanted as usize - FIRST_ID) / CHECKPOINT_SPAN;
while index.checkpoints.len() <= checkpoint {
self.take_in_next_type(index)?;
}
let mut described = self.at(index.checkpoints[checkpoint] as usize)?;
let mut id = (checkpoint * CHECKPOINT_SPAN + FIRST_ID) as u32;
while id != wanted {
described = self.at(described.body + described.body_size)?;
id += 1;
}
Some(described)
}
fn take_in_next_type(&self, index: &mut Index) -> Option<Described> {
let described = self.at(index.offset)?;
if (index.id as usize - FIRST_ID) % CHECKPOINT_SPAN == 0 {
index.checkpoints.push(index.offset as u32);
}
if self.holds_fields(&described) && described.name != 0 {
index
.containers
.insert(self.name_of(described.name), index.offset as u32);
}
index.offset = described.body + described.body_size;
index.id += 1;
Some(described)
}
fn names_container(&self, described: &Described, container: &str) -> bool {
self.holds_fields(described)
&& described.name != 0
&& self.name_of(described.name) == container
}
fn holds_fields(&self, described: &Described) -> bool {
described.kind == KIND_STRUCT || described.kind == KIND_UNION
}
fn index(&self) -> &'static mut Index {
let known = unsafe { (&raw mut INDEX).as_mut().unwrap() };
if known.is_none() {
*known = Some(Index {
containers: BTreeMap::new(),
checkpoints: Vec::new(),
offset: 0,
id: FIRST_ID as u32,
});
}
known.as_mut().unwrap()
}
fn visit_fields(
&self,
described: &Described,
base: usize,
on_field: &mut dyn FnMut(&str, &Field) -> bool,
) -> bool {
for index in 0..described.members {
let member = described.body + (index * MEMBER_SIZE);
let name = self.name_of(word_at(self.types, member));
let placement = base + self.placement_of(described, member);
if name.is_empty() {
let Some(within) = self.with_id(word_at(self.types, member + 4)) else {
continue;
};
if within.kind == KIND_STRUCT || within.kind == KIND_UNION {
if !self.visit_fields(&within, placement, on_field) {
return false;
}
}
} else {
let field = Field {
offset: placement / 8,
bit_offset: placement,
bit_size: self.bits_of(described, member),
id: word_at(self.types, member + 4),
};
if !on_field(name, &field) {
return false;
}
}
}
true
}
fn function_named(&self, wanted: &str) -> Option<Described> {
let known = self.functions();
let at = known
.binary_search_by(|entry| self.name_of(entry.name).cmp(wanted))
.ok()?;
self.at(known[at].offset as usize)
}
fn functions(&self) -> &'static [Named] {
let known = unsafe { (&raw mut FUNCTIONS).as_mut().unwrap() };
if known.is_none() {
let mut found = Vec::new();
self.visit_every_type_at(&mut |offset, described| {
if described.kind == KIND_FUNCTION && described.name != 0 {
found.push(Named {
name: described.name,
offset: offset as u32,
});
}
true
});
found.sort_unstable_by(|a, b| self.name_of(a.name).cmp(self.name_of(b.name)));
*known = Some(found);
}
known.as_ref().unwrap()
}
fn visit_parameters(&self, signature: &Described, on_parameter: &mut dyn FnMut(&str, &str)) {
for index in 0..signature.members {
let parameter = signature.body + (index * PARAMETER_SIZE);
let name = self.name_of(word_at(self.types, parameter));
let id = word_at(self.types, parameter + 4);
if id == 0 {
on_parameter("", "...");
continue;
}
on_parameter(name, &self.render(id, 0));
}
}
fn render(&self, id: u32, depth: usize) -> String {
if depth == MAX_DEPTH {
return String::from("...");
}
let Some(described) = self.with_id(id) else {
return String::from("void");
};
let named = self.name_of(described.name);
match described.kind {
KIND_POINTER => alloc::format!("{} *", self.render(described.size, depth + 1)),
KIND_ARRAY => {
let element = word_at(self.types, described.body);
let count = word_at(self.types, described.body + 8);
alloc::format!("{}[{}]", self.render(element, depth + 1), count)
}
KIND_STRUCT => alloc::format!("struct {named}"),
KIND_UNION => alloc::format!("union {named}"),
KIND_ENUM | KIND_WIDE_ENUM => alloc::format!("enum {named}"),
KIND_CONSTANT => alloc::format!("const {}", self.render(described.size, depth + 1)),
KIND_VOLATILE => alloc::format!("volatile {}", self.render(described.size, depth + 1)),
KIND_RESTRICT => alloc::format!("{} restrict", self.render(described.size, depth + 1)),
KIND_SIGNATURE => self.render_signature(&described, depth),
KIND_LOOKING_AHEAD => alloc::format!("struct {named}"),
_ => String::from(named),
}
}
fn render_signature(&self, described: &Described, depth: usize) -> String {
let mut rendered = alloc::format!("{} (", self.render(described.size, depth + 1));
for index in 0..described.members {
let parameter = described.body + (index * PARAMETER_SIZE);
if index != 0 {
rendered.push_str(", ");
}
rendered.push_str(&self.render(word_at(self.types, parameter + 4), depth + 1));
}
rendered.push(')');
rendered
}
fn span_of(&self, id: u32, depth: usize) -> usize {
if depth == MAX_DEPTH {
return 0;
}
let Some(described) = self.with_id(id) else {
return 0;
};
match described.kind {
KIND_POINTER => core::mem::size_of::<usize>(),
KIND_ARRAY => {
let element = word_at(self.types, described.body);
let count = word_at(self.types, described.body + 8) as usize;
self.span_of(element, depth + 1) * count
}
KIND_INT | KIND_STRUCT | KIND_UNION | KIND_ENUM | KIND_WIDE_ENUM | KIND_FLOAT => {
described.size as usize
}
KIND_TYPEDEF | KIND_CONSTANT | KIND_VOLATILE | KIND_RESTRICT | KIND_TAGGED_TYPE => {
self.span_of(described.size, depth + 1)
}
_ => 0,
}
}
fn bits_of(&self, described: &Described, member: usize) -> usize {
if !described.members_carry_bitfields {
return 0;
}
(word_at(self.types, member + 8) >> 24) as usize
}
fn constant_named(&self, wanted: &str) -> Option<i64> {
let known = unsafe { (&raw mut CONSTANTS).as_mut().unwrap() };
if let Some(value) = known.get(wanted) {
return Some(*value);
}
let mut found = None;
self.visit_every_type(&mut |described| {
if !self.holds_constants(described) {
return true;
}
self.visit_constants(described, &mut |name, value| {
if name == wanted {
found = Some(value);
}
found.is_none()
});
found.is_none()
});
if let Some(value) = found {
known.insert(String::from(wanted), value);
}
found
}
fn enum_named(&self, wanted: &str) -> Option<Described> {
let mut found = None;
self.visit_every_type(&mut |described| {
if self.holds_constants(described)
&& described.name != 0
&& self.name_of(described.name) == wanted
{
found = Some(described.clone());
}
found.is_none()
});
found
}
fn visit_constants(&self, described: &Described, on_constant: &mut dyn FnMut(&str, i64) -> bool) {
let wide = described.kind == KIND_WIDE_ENUM;
let stride = if wide { 12 } else { 8 };
for index in 0..described.members {
let constant = described.body + (index * stride);
let name = self.name_of(word_at(self.types, constant));
let value = if wide {
((word_at(self.types, constant + 8) as u64) << 32
| word_at(self.types, constant + 4) as u64) as i64
} else {
word_at(self.types, constant + 4) as i32 as i64
};
if !on_constant(name, value) {
return;
}
}
}
fn visit_every_type(&self, on_type: &mut dyn FnMut(&Described) -> bool) {
self.visit_every_type_at(&mut |_offset, described| on_type(described));
}
fn visit_every_type_at(&self, on_type: &mut dyn FnMut(usize, &Described) -> bool) {
let mut offset = 0;
while let Some(described) = self.at(offset) {
if !on_type(offset, &described) {
return;
}
offset = described.body + described.body_size;
}
}
fn holds_constants(&self, described: &Described) -> bool {
described.kind == KIND_ENUM || described.kind == KIND_WIDE_ENUM
}
// A structure the kernel lays out itself keeps its fields in a member with no name of its
// own, so a field is looked for through those as well, and answers where it sits in the
// structure the question was asked about.
fn offset_of(&self, described: &Described, field: &str) -> Option<usize> {
for index in 0..described.members {
let member = described.body + (index * MEMBER_SIZE);
let name = self.name_of(word_at(self.types, member));
let placement = self.placement_of(described, member);
if name == field {
return Some(placement);
}
if name.is_empty() {
let within = self.with_id(word_at(self.types, member + 4))?;
if within.kind == KIND_STRUCT || within.kind == KIND_UNION {
if let Some(deeper) = self.offset_of(&within, field) {
return Some(placement + deeper);
}
}
}
}
None
}
fn placement_of(&self, described: &Described, member: usize) -> usize {
let placement = word_at(self.types, member + 8);
let bits = if described.members_carry_bitfields {
placement & 0x00ff_ffff
} else {
placement
};
bits as usize
}
fn at(&self, offset: usize) -> Option<Described> {
if offset + DESCRIPTION_SIZE > self.types.len() {
return None;
}
let name = word_at(self.types, offset);
let info = word_at(self.types, offset + 4);
let size = word_at(self.types, offset + 8);
let kind = (info >> 24) & 0x1f;
let members = (info & 0xffff) as usize;
Some(Described {
name,
kind,
size,
members,
members_carry_bitfields: (info >> 31) != 0,
body: offset + DESCRIPTION_SIZE,
body_size: body_size_of(kind, members),
})
}
fn name_of(&self, offset: u32) -> &'static str {
let start = offset as usize;
let end = self.names[start..]
.iter()
.position(|letter| *letter == 0)
.map(|length| start + length)
.unwrap_or(self.names.len());
core::str::from_utf8(&self.names[start..end]).unwrap_or("")
}
}
static mut CONSTANTS: BTreeMap<String, i64> = BTreeMap::new();
static mut FUNCTIONS: Option<Vec<Named>> = None;
static mut INDEX: Option<Index> = None;
static mut RESOLVED: Vec<Described> = Vec::new();
struct Named {
name: u32,
offset: u32,
}
struct Index {
containers: BTreeMap<&'static str, u32>,
checkpoints: Vec<u32>,
offset: usize,
id: u32,
}
#[derive(Clone)]
struct Described {
name: u32,
kind: u32,
size: u32,
members: usize,
members_carry_bitfields: bool,
body: usize,
body_size: usize,
}
fn described_types() -> Option<DescribedTypes> {
let start = unsafe { ___start_BTF } as usize;
let stop = unsafe { ___stop_BTF } as usize;
if start == 0 || stop <= start {
return None;
}
let described = unsafe { core::slice::from_raw_parts(start as *const u8, stop - start) };
if u16::from_ne_bytes([described[0], described[1]]) != DESCRIPTION_MAGIC {
return None;
}
let header_size = word_at(described, 4) as usize;
let types = header_size + word_at(described, 8) as usize;
let types_size = word_at(described, 12) as usize;
let names = header_size + word_at(described, 16) as usize;
let names_size = word_at(described, 20) as usize;
if names + names_size > described.len() {
return None;
}
Some(DescribedTypes {
types: &described[types..types + types_size],
names: &described[names..names + names_size],
})
}
fn body_size_of(kind: u32, members: usize) -> usize {
match kind {
KIND_INT | KIND_VARIABLE | KIND_TAG => 4,
KIND_ARRAY => 12,
KIND_STRUCT | KIND_UNION => members * MEMBER_SIZE,
KIND_ENUM | KIND_SIGNATURE => members * 8,
KIND_SECTION | KIND_WIDE_ENUM => members * 12,
_ => 0,
}
}
fn word_at(bytes: &[u8], offset: usize) -> u32 {
u32::from_ne_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
])
}
const DESCRIPTION_MAGIC: u16 = 0xeb9f;
const DESCRIPTION_SIZE: usize = 12;
const FIRST_ID: usize = 1;
const CHECKPOINT_SPAN: usize = 64;
const MEMBER_SIZE: usize = 12;
const KIND_INT: u32 = 1;
const KIND_POINTER: u32 = 2;
const KIND_ARRAY: u32 = 3;
const KIND_STRUCT: u32 = 4;
const KIND_UNION: u32 = 5;
const KIND_ENUM: u32 = 6;
const KIND_FUNCTION: u32 = 12;
const KIND_LOOKING_AHEAD: u32 = 7;
const KIND_TYPEDEF: u32 = 8;
const KIND_VOLATILE: u32 = 9;
const KIND_CONSTANT: u32 = 10;
const KIND_RESTRICT: u32 = 11;
const KIND_SIGNATURE: u32 = 13;
const KIND_VARIABLE: u32 = 14;
const KIND_SECTION: u32 = 15;
const KIND_TAG: u32 = 17;
const KIND_FLOAT: u32 = 16;
const KIND_TAGGED_TYPE: u32 = 18;
const KIND_WIDE_ENUM: u32 = 19;
const PARAMETER_SIZE: usize = 8;
const MAX_DEPTH: usize = 8;
unsafe extern "C" {
static ___start_BTF: *const c_void;
static ___stop_BTF: *const c_void;
}
+386
View File
@@ -0,0 +1,386 @@
use alloc::string::String;
use alloc::vec::Vec;
use core::ffi::{CStr, c_void};
use core::ptr;
use crate::bindings::{
GError, GumElfModule, GumExportDetails, GumMemoryRange, GumModuleRegistry, gboolean,
g_bytes_new, g_bytes_unref, g_clear_error, g_object_unref, gconstpointer, gpointer, gsize,
gum_barebone_register_module, gum_barebone_unregister_module,
gum_elf_module_enumerate_exports, gum_elf_module_enumerate_symbols,
gum_elf_module_get_preferred_address, gum_elf_module_new_from_blob, gum_interceptor_begin_transaction,
gum_interceptor_end_transaction, gum_interceptor_obtain, gum_interceptor_replace,
};
use crate::bindings::{GumElfSymbolBind_GUM_ELF_BIND_LOCAL, GumElfSymbolDetails};
use crate::gum::{self, FoundExportCallback, FoundSymbolCallback};
use crate::kernel::MemoryRegion;
use alloc::ffi::CString;
use super::user::{EXECUTABLE, READABLE, WRITABLE, contents_of};
pub fn register_what_the_copy_lives_among(registry: *mut GumModuleRegistry) {
unsafe { REGISTRY = registry };
for image in mapped_images() {
announce(&image);
unsafe { known() }.push(image);
}
}
// The loader calls the same empty function every time it has changed what is mapped -- that is
// what a debugger watches -- so the copy asks again from there.
pub fn watch_the_loader() {
let Some(rendezvous) = where_the_loader_says_so() else {
return;
};
unsafe {
let interceptor = gum_interceptor_obtain();
gum_interceptor_begin_transaction(interceptor);
gum_interceptor_replace(interceptor, rendezvous as gpointer,
the_loader_changed_something as gpointer, &raw mut LOADER_SAYS_SO,
ptr::null_mut());
gum_interceptor_end_transaction(interceptor);
}
}
unsafe extern "C" fn the_loader_changed_something() {
let said_so: unsafe extern "C" fn() = unsafe { core::mem::transmute(LOADER_SAYS_SO) };
unsafe { said_so() };
look_again();
}
fn look_again() {
let registry = unsafe { REGISTRY };
if registry.is_null() {
return;
}
let mapped = mapped_images();
let known = unsafe { known() };
for image in &mapped {
if !known.iter().any(|seen| seen.base == image.base) {
announce(image);
}
}
for seen in known.iter() {
if !mapped.iter().any(|image| image.base == seen.base) {
unsafe { gum_barebone_unregister_module(registry, seen.base) };
}
}
*known = mapped;
}
fn announce(image: &Image) {
let range = GumMemoryRange {
base_address: image.base,
size: image.size as gsize,
};
let module = gum::gum_native_module_new(&image.path, "", &range);
unsafe {
gum_barebone_register_module(REGISTRY, module);
g_object_unref(module as gpointer);
}
}
pub fn enumerate_exports_in_range(from: u64, to: u64, found: &mut FoundExportCallback<'_>) {
let Some(image) = mapped_images().into_iter().find(|image| image.base == from) else {
return;
};
let Some(module) = read_the_image(&image) else {
return;
};
let mut asking = Asking {
found,
slide: slide_of(&image, module),
from,
to,
};
unsafe {
gum_elf_module_enumerate_exports(
module,
Some(crate::signed_to_be_called_back(note_an_export, 0)),
&mut asking as *mut Asking<'_, '_> as gpointer,
);
g_object_unref(module as gpointer);
}
}
pub fn export_named(wanted: &str) -> u64 {
let mut found: u64 = 0;
for image in mapped_images() {
enumerate_exports_in_range(image.base, image.base + image.size, &mut |name, address| {
if !name.is_null() && unsafe { CStr::from_ptr(name as *const _) }.to_bytes() == wanted.as_bytes() {
found = address;
}
found == 0
});
if found != 0 {
break;
}
}
found
}
pub fn enumerate_symbols_in_range(from: u64, to: u64, found: &mut FoundSymbolCallback<'_>) {
let Some(image) = mapped_images().into_iter().find(|image| image.base == from) else {
return;
};
let Some(module) = read_the_image(&image) else {
return;
};
let mut asking = AskingForSymbols {
found,
slide: slide_of(&image, module),
from,
to,
};
unsafe {
gum_elf_module_enumerate_symbols(
module,
Some(crate::signed_to_be_called_back(note_a_symbol, 0)),
&mut asking as *mut AskingForSymbols<'_, '_> as gpointer,
);
g_object_unref(module as gpointer);
}
}
struct AskingForSymbols<'a, 'b> {
found: &'a mut FoundSymbolCallback<'b>,
slide: u64,
from: u64,
to: u64,
}
unsafe extern "C" fn note_a_symbol(
details: *const GumElfSymbolDetails,
asking: gpointer,
) -> gboolean {
let asking = unsafe { &mut *(asking as *mut AskingForSymbols<'_, '_>) };
let details = unsafe { &*details };
if details.address == 0 {
return 1;
}
let address = details.address.wrapping_add(asking.slide);
if address < asking.from || address >= asking.to {
return 1;
}
(asking.found)(
details.name,
address,
details.size as u64,
details.type_ as u8,
details.bind != GumElfSymbolBind_GUM_ELF_BIND_LOCAL,
) as gboolean
}
fn slide_of(image: &Image, module: *mut GumElfModule) -> u64 {
image
.base
.wrapping_sub(unsafe { gum_elf_module_get_preferred_address(module) })
}
fn read_the_image(image: &Image) -> Option<*mut GumElfModule> {
let path = CString::new(image.path.as_str()).ok()?;
let data = contents_of(&path);
if data.is_empty() {
return None;
}
unsafe {
let blob = g_bytes_new(data.as_ptr() as gconstpointer, data.len() as gsize);
let mut error: *mut GError = ptr::null_mut();
let module = gum_elf_module_new_from_blob(blob, &mut error);
g_bytes_unref(blob);
if module.is_null() {
g_clear_error(&mut error);
return None;
}
Some(module)
}
}
struct Asking<'a, 'b> {
found: &'a mut FoundExportCallback<'b>,
slide: u64,
from: u64,
to: u64,
}
unsafe extern "C" fn note_an_export(details: *const GumExportDetails, asking: gpointer) -> gboolean {
let asking = unsafe { &mut *(asking as *mut Asking<'_, '_>) };
let details = unsafe { &*details };
let address = details.address.wrapping_add(asking.slide);
if address < asking.from || address >= asking.to {
return 1;
}
(asking.found)(details.name, address) as gboolean
}
fn where_the_loader_says_so() -> Option<u64> {
let mut found = 0u64;
for image in mapped_images() {
let mut on_export = |name: *const crate::bindings::gchar, address: u64| {
if unsafe { core::ffi::CStr::from_ptr(name) } == c"_dl_debug_state" {
found = address;
}
found == 0
};
enumerate_exports_in_range(image.base, image.base + image.size, &mut on_export);
if found != 0 {
return Some(found);
}
}
None
}
unsafe fn known() -> &'static mut Vec<Image> {
unsafe { (&raw mut KNOWN).as_mut().unwrap() }
}
static mut KNOWN: Vec<Image> = Vec::new();
static mut REGISTRY: *mut GumModuleRegistry = ptr::null_mut();
static mut LOADER_SAYS_SO: *mut c_void = ptr::null_mut();
pub fn enumerate_ranges(found: &mut dyn FnMut(u64, u64, u32)) {
let listed = contents_of(c"/proc/self/maps");
for line in listed.split(|byte| *byte == b'\n') {
let Some(mapping) = mapping_in(line) else {
continue;
};
if super::user::range_is_ours(mapping.start) {
continue;
}
found(mapping.start, mapping.end - mapping.start, mapping.protection);
}
}
pub fn region_at(address: u64) -> Option<MemoryRegion> {
if super::user::range_is_ours(address) {
return None;
}
let listed = contents_of(c"/proc/self/maps");
for line in listed.split(|byte| *byte == b'\n') {
let Some(mapping) = mapping_in(line) else {
continue;
};
if address >= mapping.start && address < mapping.end {
return Some(MemoryRegion {
base: mapping.start,
size: mapping.end - mapping.start,
protection: mapping.protection,
});
}
}
None
}
fn mapped_images() -> Vec<Image> {
let listed = contents_of(c"/proc/self/maps");
let mut images: Vec<Image> = Vec::new();
for line in listed.split(|byte| *byte == b'\n') {
let Some(mapping) = mapping_in(line) else {
continue;
};
if mapping.path.is_empty() || super::user::range_is_ours(mapping.start) {
continue;
}
let holds_code = mapping.protection & EXECUTABLE as u32 != 0;
match images.last_mut() {
Some(image) if image.path.as_bytes() == mapping.path => {
image.size = mapping.end - image.base;
image.holds_code |= holds_code;
}
_ => images.push(Image {
path: String::from_utf8_lossy(mapping.path).into_owned(),
base: mapping.start,
size: mapping.end - mapping.start,
holds_code,
}),
}
}
images.retain(|image| image.holds_code);
images
}
fn mapping_in(line: &[u8]) -> Option<Mapping<'_>> {
let boundary = line.iter().position(|byte| *byte == b'-')?;
let start = number_in(&line[..boundary])?;
let rest = &line[boundary + 1..];
let mut fields = rest.split(|byte| *byte == b' ');
let end = number_in(fields.next()?)?;
let how = fields.next()?;
Some(Mapping {
path: line[..].iter().position(|byte| *byte == b'/').map_or(&[], |at| &line[at..]),
start,
end,
protection: protection_of(how),
})
}
fn protection_of(how: &[u8]) -> u32 {
let mut protection = 0;
if how[0] == b'r' {
protection |= READABLE as u32;
}
if how[1] == b'w' {
protection |= WRITABLE as u32;
}
if how[2] == b'x' {
protection |= EXECUTABLE as u32;
}
protection
}
fn number_in(digits: &[u8]) -> Option<u64> {
let mut value = 0u64;
for digit in digits {
value = (value << 4) | (digit.to_ascii_lowercase() as char).to_digit(16)? as u64;
}
Some(value)
}
struct Image {
path: String,
base: u64,
size: u64,
holds_code: bool,
}
struct Mapping<'a> {
path: &'a [u8],
start: u64,
end: u64,
protection: u32,
}
+94
View File
@@ -0,0 +1,94 @@
// Linux backend. The agent reaches the kernel in one of two ways, and the rest
// of the agent cannot tell which: loaded from the inside as a kernel module,
// where every primitive is a call into a shim kbuild compiled against the
// target's headers; or injected from the outside like XNU and Windows, where
// the host patches the addresses it needs into .kernel_addrs and the memory it
// cannot touch is reached over the hostlink.
use alloc::string::String;
pub fn stack_headroom() -> usize {
let marker = 0usize;
let here = &marker as *const usize as usize;
here - (here & !(STACK_SPAN - 1))
}
#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
pub(crate) const STACK_SPAN: usize = 16 * 1024;
#[cfg(any(target_arch = "arm", target_arch = "x86"))]
pub(crate) const STACK_SPAN: usize = 8 * 1024;
#[cfg(feature = "linux")]
mod kmod;
#[cfg(feature = "linux-injected")]
mod arena;
#[cfg(feature = "linux-injected")]
mod facade;
#[cfg(feature = "linux-injected")]
pub mod hostlink_pipe_goldfish;
#[cfg(feature = "linux-injected")]
pub mod hostlink_pipe_vsock;
#[cfg(feature = "linux-injected")]
mod injection;
#[cfg(feature = "linux-injected")]
pub(crate) mod layout;
#[cfg(feature = "linux-injected")]
mod mapped;
#[cfg(feature = "linux-injected")]
mod modules;
#[cfg(feature = "linux-injected")]
mod kfault;
#[cfg(feature = "linux-injected")]
mod native;
#[cfg(feature = "linux-injected")]
mod relay;
#[cfg(feature = "linux-injected")]
mod spawn;
#[cfg(feature = "linux-injected")]
mod threads;
#[cfg(feature = "linux-injected")]
pub(crate) mod user;
#[cfg(feature = "linux-injected")]
mod processes;
#[cfg(feature = "linux")]
pub use self::kmod::*;
#[cfg(feature = "linux-injected")]
pub use self::injection::*;
#[cfg(feature = "linux-injected")]
pub use self::relay::*;
#[cfg(feature = "linux-injected")]
pub use self::facade::*;
#[cfg(feature = "linux-injected")]
pub use self::native::{
alloc_dma, free_dma, get_kernel_base, get_kernel_size, install_interrupt_handler, map_io,
map_pages,
mmio_interrupt, pci_interrupt, release_fault_reporter, release_interrupt, run_when_ready,
patch_text, set_kernel_base, set_protection, virt_to_phys,
};
#[cfg(feature = "linux-injected")]
pub use self::mapped::*;
#[cfg(feature = "linux-injected")]
pub use self::modules::*;
#[cfg(feature = "linux-injected")]
pub use self::processes::*;
#[cfg(feature = "linux-injected")]
pub use self::spawn::*;
#[cfg(feature = "linux-injected")]
pub use self::threads::*;
#[derive(Debug, Clone)]
pub enum ModuleEvent {
Loaded,
Unloaded,
}
#[derive(Debug, Clone)]
pub struct LoadedModule {
pub name: String,
pub version: String,
pub base: u64,
pub size: u64,
}
+788
View File
@@ -0,0 +1,788 @@
use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
use core::ffi::{c_char, c_int, c_long, c_void};
use core::ptr;
use super::layout::{field_offset, struct_size};
use crate::bindings::{g_free, gsize, gum_memory_read};
use super::{LoadedModule, ModuleEvent};
pub fn enumerate_modules() -> Vec<LoadedModule> {
let Some(layout) = module_layout() else {
return Vec::new();
};
let head = unsafe { _modules } as usize;
if head == 0 {
return Vec::new();
}
let mut modules = Vec::new();
let mut node = word_at(head);
let mut left = MAX_MODULES;
while node != head && node != 0 && left != 0 {
let module = node - layout.list;
if is_live(module, &layout) {
modules.push(describe(module, &layout));
}
node = word_at(node);
left -= 1;
}
modules
}
pub fn enumerate_module_symbols(
base: u64,
on_symbol: &mut dyn FnMut(*const c_char, u64, u64, u8, bool) -> bool,
) -> bool {
let Some(layout) = module_layout() else {
return false;
};
let Some(kallsyms) = layout.kallsyms.as_ref() else {
return false;
};
let Some(module) = module_at(base, layout) else {
return false;
};
let table = word_at(module + kallsyms.at);
if table == 0 {
return false;
}
let symbols = word_at(table + kallsyms.symtab);
let names = word_at(table + kallsyms.strtab);
let count = u32_at(table + kallsyms.num_symtab) as usize;
if symbols == 0 || names == 0 {
return false;
}
for index in 0..count {
let symbol = symbols + index * SYMBOL_SIZE;
let value = symbol_value(symbol);
if value == 0 {
continue;
}
let info = symbol_info(symbol);
let name = (names + u32_at(symbol) as usize) as *const c_char;
if !on_symbol(
name,
value as u64,
symbol_size(symbol),
info & 0xf,
info >> 4 != LOCAL,
) {
break;
}
}
true
}
pub fn enumerate_module_exports(
base: u64,
on_export: &mut dyn FnMut(*const c_char, u64) -> bool,
) -> bool {
let Some(layout) = module_layout() else {
return false;
};
let Some(exports) = layout.exports.as_ref() else {
return false;
};
let Some(module) = module_at(base, layout) else {
return false;
};
let table = word_at(module + exports.at);
let count = u32_at(module + exports.count) as usize;
if table == 0 {
return true;
}
for index in 0..count {
let entry = table + index * exports.stride;
let (name, address) = if exports.relative {
(
relative_target(entry + OFFSET_SIZE),
relative_target(entry) as u64,
)
} else {
(word_at(entry + WORD), word_at(entry) as u64)
};
if name == 0 || address == 0 {
continue;
}
if !on_export(name as *const c_char, address) {
break;
}
}
true
}
fn module_at(base: u64, layout: &Layout) -> Option<usize> {
let head = unsafe { _modules } as usize;
if head == 0 {
return None;
}
let mut node = word_at(head);
let mut left = MAX_MODULES;
while node != head && node != 0 && left != 0 {
let module = node - layout.list;
if describe(module, layout).base == base {
return Some(module);
}
node = word_at(node);
left -= 1;
}
None
}
fn relative_target(at: usize) -> usize {
let offset = unsafe { (at as *const i32).read_volatile() };
at.wrapping_add_signed(offset as isize)
}
pub fn watch_modules(on_event: fn(ModuleEvent, LoadedModule)) {
if module_layout().is_none() {
return;
}
unsafe {
if _register_module_notifier.is_none() {
return;
}
WATCHER = Some(on_event);
NOTIFIER = Box::into_raw(Box::new(NotifierBlock {
notifier_call: Some(NOTIFIER_CALL),
next: ptr::null_mut(),
priority: 0,
}));
register_notifier(NOTIFIER as *mut c_void);
}
}
pub fn unwatch_modules() {
unsafe {
if (&raw const WATCHER).read().is_none() {
return;
}
if _unregister_module_notifier.is_some() {
unregister_notifier(NOTIFIER as *mut c_void);
}
drop(Box::from_raw(NOTIFIER));
NOTIFIER = ptr::null_mut();
WATCHER = None;
}
}
#[cfg(not(target_arch = "x86"))]
unsafe fn register_notifier(block: *mut c_void) {
unsafe {
if let Some(register) = _register_module_notifier {
register(block);
}
}
}
#[cfg(not(target_arch = "x86"))]
unsafe fn unregister_notifier(block: *mut c_void) {
unsafe {
if let Some(unregister) = _unregister_module_notifier {
unregister(block);
}
}
}
#[cfg(target_arch = "x86")]
unsafe fn register_notifier(block: *mut c_void) {
unsafe { frida_k_register_module_notifier(block) };
}
#[cfg(target_arch = "x86")]
unsafe fn unregister_notifier(block: *mut c_void) {
unsafe { frida_k_unregister_module_notifier(block) };
}
#[cfg(not(target_arch = "x86"))]
const NOTIFIER_CALL: unsafe extern "C" fn(*mut NotifierBlock, c_long, *mut c_void) -> c_int =
on_module_state;
#[cfg(target_arch = "x86")]
const NOTIFIER_CALL: unsafe extern "C" fn(*mut NotifierBlock, c_long, *mut c_void) -> c_int =
frida_kcb_module_state;
#[cfg(target_arch = "x86")]
#[unsafe(no_mangle)]
unsafe extern "C" fn frida_cb_module_state(
block: *mut NotifierBlock,
action: c_long,
module: *mut c_void,
) -> c_int {
unsafe { on_module_state(block, action, module) }
}
unsafe extern "C" fn on_module_state(
_block: *mut NotifierBlock,
action: c_long,
module: *mut c_void,
) -> c_int {
let kind = match action {
MODULE_STATE_LIVE => ModuleEvent::Loaded,
MODULE_STATE_GOING => ModuleEvent::Unloaded,
_ => return NOTIFY_DONE,
};
let Some(on_event) = (unsafe { WATCHER }) else {
return NOTIFY_DONE;
};
let Some(layout) = module_layout() else {
return NOTIFY_DONE;
};
on_event(kind, describe(module as usize, &layout));
NOTIFY_DONE
}
fn describe(module: usize, layout: &Layout) -> LoadedModule {
let (base, size) = match layout.memory {
Memory::Described { at, base, size } => (word_at(module + at + base), word_at(module + at + size)),
Memory::Split { base, size } => (word_at(module + base), word_at(module + size)),
};
LoadedModule {
name: text_at(module + layout.name),
version: layout
.version
.map(|at| text_at(word_at(module + at)))
.unwrap_or_default(),
base: base as u64,
size: (size & 0xffff_ffff) as u64,
}
}
fn is_live(module: usize, layout: &Layout) -> bool {
let Some(at) = layout.state else {
return true;
};
let state = unsafe { ((module + at) as *const u32).read_volatile() };
state == MODULE_STATE_LIVE as u32
}
fn module_layout() -> Option<&'static Layout> {
unsafe {
let known = (&raw mut LAYOUT).as_mut().unwrap();
if known.is_none() {
*known = discover_layout();
}
known.as_ref()
}
}
static mut LAYOUT: Option<Layout> = None;
struct Layout {
list: usize,
name: usize,
version: Option<usize>,
state: Option<usize>,
memory: Memory,
kallsyms: Option<Kallsyms>,
exports: Option<Exports>,
}
struct Kallsyms {
at: usize,
symtab: usize,
num_symtab: usize,
strtab: usize,
}
struct Exports {
at: usize,
count: usize,
stride: usize,
relative: bool,
}
enum Memory {
Described { at: usize, base: usize, size: usize },
Split { base: usize, size: usize },
}
fn discover_layout() -> Option<Layout> {
layout_from_types().or_else(layout_from_probing)
}
fn layout_from_types() -> Option<Layout> {
let memory = match field_offset("module", "mem") {
Some(at) => Memory::Described {
at,
base: field_offset("module_memory", "base")?,
size: field_offset("module_memory", "size")?,
},
None => match field_offset("module", "core_layout") {
Some(at) => Memory::Described {
at,
base: field_offset("module_layout", "base")?,
size: field_offset("module_layout", "size")?,
},
None => Memory::Split {
base: field_offset("module", "module_core")?,
size: field_offset("module", "core_size")?,
},
},
};
let list = field_offset("module", "list")?;
Some(Layout {
list,
name: field_offset("module", "name")?,
version: field_offset("module", "version"),
state: field_offset("module", "state"),
memory,
kallsyms: kallsyms_from_types(),
exports: exports_from_types(list),
})
}
fn layout_from_probing() -> Option<Layout> {
let list = WORD;
let name = list + 2 * WORD;
let start = (name + MODULE_NAME_LEN + WORD - 1) & !(WORD - 1);
let (at, size) = probe_memory(list, start)?;
let memory = Memory::Described { at, base: 0, size };
Some(Layout {
list,
name,
version: None,
state: Some(0),
kallsyms: probe_kallsyms(list, start, &memory),
exports: probe_exports(list, start, &memory),
memory,
})
}
fn probe_memory(list: usize, start: usize) -> Option<(usize, usize)> {
let head = unsafe { _modules } as usize;
if head == 0 {
return None;
}
for at in (start..PROBE_SPAN).step_by(WORD) {
for size in (WORD..=MAX_SIZE_DISTANCE).step_by(WORD) {
if each_module(head, list, |module| describes_memory(module, at, size)) {
return Some((at, size));
}
}
}
None
}
fn describes_memory(module: usize, at: usize, size: usize) -> bool {
let base = word_at(module + at);
let size = unsafe { ((module + at + size) as *const u32).read_volatile() } as usize;
base != 0
&& base & (PAGE_SIZE - 1) == 0
&& size != 0
&& size & (PAGE_SIZE - 1) == 0
&& size <= MAX_MODULE_SIZE
&& base.abs_diff(module) <= MODULE_REACH
}
fn each_module(head: usize, list: usize, is_sane: impl Fn(usize) -> bool) -> bool {
let mut node = word_at(head);
let mut seen = 0;
let mut left = MAX_MODULES;
while node != head && node != 0 && left != 0 {
if !is_sane(node - list) {
return false;
}
seen += 1;
node = word_at(node);
left -= 1;
}
seen != 0
}
fn kallsyms_from_types() -> Option<Kallsyms> {
Some(Kallsyms {
at: field_offset("module", "kallsyms")?,
symtab: field_offset("mod_kallsyms", "symtab")?,
num_symtab: field_offset("mod_kallsyms", "num_symtab")?,
strtab: field_offset("mod_kallsyms", "strtab")?,
})
}
fn exports_from_types(_list: usize) -> Option<Exports> {
let at = field_offset("module", "syms")?;
let count = field_offset("module", "num_syms")?;
let relative = field_offset("kernel_symbol", "value_offset").is_some();
Some(Exports {
at,
count,
stride: struct_size("kernel_symbol").unwrap_or(stride_of_exports(relative)),
relative,
})
}
fn probe_kallsyms(list: usize, start: usize, memory: &Memory) -> Option<Kallsyms> {
let head = unsafe { _modules } as usize;
for at in (start..PROBE_SPAN).step_by(WORD) {
if each_module(head, list, |module| describes_kallsyms(module, at, memory)) {
return Some(Kallsyms {
at,
symtab: 0,
num_symtab: WORD,
strtab: 2 * WORD,
});
}
}
None
}
fn describes_kallsyms(module: usize, at: usize, memory: &Memory) -> bool {
let owned = regions_of(module, memory);
let table = word_at(module + at);
if !owned.holds(table, 3 * WORD) {
return false;
}
let Some(symbols) = try_read_word(table) else {
return false;
};
let Some(count) = try_read_u32(table + WORD) else {
return false;
};
let Some(names) = try_read_word(table + 2 * WORD) else {
return false;
};
let count = count as usize;
if count == 0 || count > MAX_SYMBOLS {
return false;
}
if !owned.holds(symbols, count * SYMBOL_SIZE) || !owned.holds(names, 1) {
return false;
}
try_read_byte(names) == Some(0)
}
fn probe_exports(list: usize, start: usize, memory: &Memory) -> Option<Exports> {
let head = unsafe { _modules } as usize;
for at in (start..PROBE_SPAN).step_by(WORD) {
for count in counts_near(at, start) {
for relative in [true, false] {
let stride = stride_of_exports(relative);
if exports_shaped(head, list, at, count, stride, relative, memory) {
return Some(Exports {
at,
count,
stride,
relative,
});
}
}
}
}
None
}
fn exports_shaped(
head: usize,
list: usize,
at: usize,
count: usize,
stride: usize,
relative: bool,
memory: &Memory,
) -> bool {
let mut evidence = false;
let mut node = word_at(head);
let mut left = MAX_MODULES;
while node != head && node != 0 && left != 0 {
let module = node - list;
let table = word_at(module + at);
let exported = u32_at(module + count) as usize;
if table == 0 || exported == 0 {
if table != 0 || exported != 0 {
return false;
}
} else {
if exported > MAX_EXPORTS {
return false;
}
let owned = regions_of(module, memory);
if !owned.holds(table, exported * stride) {
return false;
}
for index in 0..exported.min(EXPORTS_SAMPLED) {
if !names_an_export(table + index * stride, relative, &owned) {
return false;
}
}
evidence = true;
}
node = word_at(node);
left -= 1;
}
evidence
}
fn names_an_export(entry: usize, relative: bool, owned: &Regions) -> bool {
let name = if relative {
let Some(offset) = try_read_u32(entry + OFFSET_SIZE) else {
return false;
};
(entry + OFFSET_SIZE).wrapping_add_signed(offset as i32 as isize)
} else {
match try_read_word(entry + WORD) {
Some(name) => name,
None => return false,
}
};
if !owned.holds(name, 1) {
return false;
}
match try_read_byte(name) {
Some(first) => first.is_ascii_alphabetic() || first == b'_',
None => false,
}
}
fn counts_near(at: usize, start: usize) -> impl Iterator<Item = usize> {
let after = (WORD..=MAX_COUNT_DISTANCE).step_by(WORD).map(move |d| at + d);
let before = (WORD..=MAX_COUNT_DISTANCE)
.step_by(WORD)
.filter_map(move |d| at.checked_sub(d).filter(|&count| count >= start));
after.chain(before)
}
fn stride_of_exports(relative: bool) -> usize {
if relative { 3 * OFFSET_SIZE } else { 3 * WORD }
}
fn regions_of(module: usize, memory: &Memory) -> Regions {
let Memory::Described { at, base, size } = *memory else {
return Regions::default();
};
let mut owned = Regions::default();
for offset in (0..REGION_WINDOW).step_by(WORD) {
let entry = module + at + offset;
let start = word_at(entry + base);
let span = u32_at(entry + size) as usize;
if start != 0
&& start & (PAGE_SIZE - 1) == 0
&& span != 0
&& span & (PAGE_SIZE - 1) == 0
&& span <= MAX_MODULE_SIZE
&& start.abs_diff(module) <= MODULE_REACH
{
owned.add(start, span);
}
}
owned
}
#[derive(Default)]
struct Regions {
entries: [(usize, usize); MAX_REGIONS],
count: usize,
}
impl Regions {
fn add(&mut self, base: usize, size: usize) {
if self.count == MAX_REGIONS {
return;
}
self.entries[self.count] = (base, size);
self.count += 1;
}
fn holds(&self, address: usize, span: usize) -> bool {
let Some(end) = address.checked_add(span) else {
return false;
};
self.entries[..self.count]
.iter()
.any(|&(base, size)| address >= base && end <= base + size)
}
}
fn try_read_word(address: usize) -> Option<usize> {
Some(usize::from_ne_bytes(try_read(address)?))
}
fn try_read_u32(address: usize) -> Option<u32> {
let bytes = try_read::<4>(address)?;
Some(u32::from_ne_bytes(bytes))
}
fn try_read_byte(address: usize) -> Option<u8> {
Some(try_read::<1>(address)?[0])
}
fn try_read<const N: usize>(address: usize) -> Option<[u8; N]> {
unsafe {
let mut read: gsize = 0;
let data = gum_memory_read(address as *const c_void, N as gsize, &mut read);
if data.is_null() {
return None;
}
let mut bytes = [0u8; N];
core::ptr::copy_nonoverlapping(data as *const u8, bytes.as_mut_ptr(), N);
g_free(data as *mut c_void);
Some(bytes)
}
}
fn word_at(address: usize) -> usize {
unsafe { (address as *const usize).read_volatile() }
}
fn u32_at(address: usize) -> u32 {
unsafe { (address as *const u32).read_volatile() }
}
#[cfg(target_pointer_width = "64")]
fn symbol_value(symbol: usize) -> usize {
word_at(symbol + 8)
}
#[cfg(target_pointer_width = "64")]
fn symbol_size(symbol: usize) -> u64 {
word_at(symbol + 16) as u64
}
#[cfg(target_pointer_width = "64")]
fn symbol_info(symbol: usize) -> u8 {
unsafe { ((symbol + 4) as *const u8).read_volatile() }
}
#[cfg(target_pointer_width = "32")]
fn symbol_value(symbol: usize) -> usize {
word_at(symbol + 4)
}
#[cfg(target_pointer_width = "32")]
fn symbol_size(symbol: usize) -> u64 {
word_at(symbol + 8) as u64
}
#[cfg(target_pointer_width = "32")]
fn symbol_info(symbol: usize) -> u8 {
unsafe { ((symbol + 12) as *const u8).read_volatile() }
}
fn text_at(address: usize) -> String {
if address == 0 {
return String::new();
}
let mut text = String::new();
for i in 0..MAX_NAME {
let byte = unsafe { ((address + i) as *const u8).read_volatile() };
if byte == 0 {
break;
}
text.push(byte as char);
}
text
}
static mut WATCHER: Option<fn(ModuleEvent, LoadedModule)> = None;
static mut NOTIFIER: *mut NotifierBlock = ptr::null_mut();
#[repr(C)]
struct NotifierBlock {
notifier_call:
Option<unsafe extern "C" fn(*mut NotifierBlock, c_long, *mut c_void) -> c_int>,
next: *mut NotifierBlock,
priority: c_int,
}
const MODULE_STATE_LIVE: c_long = 0;
const MODULE_STATE_GOING: c_long = 2;
const NOTIFY_DONE: c_int = 0;
const MAX_MODULES: usize = 4096;
const WORD: usize = core::mem::size_of::<usize>();
const MODULE_NAME_LEN: usize = 64 - WORD;
const PAGE_SIZE: usize = 4096;
const PROBE_SPAN: usize = 1024;
const MAX_MODULE_SIZE: usize = 256 * 1024 * 1024;
const MODULE_REACH: usize = 512 * 1024 * 1024;
const MAX_SIZE_DISTANCE: usize = 3 * WORD;
const MAX_COUNT_DISTANCE: usize = 3 * WORD;
const REGION_WINDOW: usize = 512;
const MAX_REGIONS: usize = 16;
const OFFSET_SIZE: usize = 4;
const SYMBOL_SIZE: usize = if WORD == 8 { 24 } else { 16 };
const MAX_SYMBOLS: usize = 1 << 20;
const MAX_EXPORTS: usize = 1 << 16;
const EXPORTS_SAMPLED: usize = 64;
const LOCAL: u8 = 0;
const MAX_NAME: usize = 64;
#[cfg(target_arch = "x86")]
unsafe extern "C" {
fn frida_kcb_module_state(
block: *mut NotifierBlock,
action: c_long,
module: *mut c_void,
) -> c_int;
fn frida_k_register_module_notifier(block: *mut c_void) -> c_int;
fn frida_k_unregister_module_notifier(block: *mut c_void) -> c_int;
}
unsafe extern "C" {
static _modules: *const c_void;
static _register_module_notifier: Option<unsafe extern "C" fn(*mut c_void) -> c_int>;
#[allow(dead_code)]
static _unregister_module_notifier: Option<unsafe extern "C" fn(*mut c_void) -> c_int>;
}
File diff suppressed because it is too large. Load diff
Loaded 100 of 289 files, more files were not shown because too many files have changed in this diff. Show more