Under `adb shell`, frida-server's stdin and stdout are the same socket.
The crash monitor's logcat inherited it, so when frida-server died
abnormally, logcat kept it open and `adb shell` never saw EOF.
Give logcat /dev/null for stdin instead.
On the emulated QNX 6.5 sabrelite board, threads that concurrently
take first-touch page faults on code mapped from the boot image's
/opt sometimes get SIGSEGV with SEGV_MAPERR, even though the pages
are mapped. A standalone dlopen() test with two or more threads
calling into a fresh library crashes on the first iteration, and
passes when the library lives in /tmp. This made the agent tests
crash intermittently once the reloaded agent spun up its threads.
Gum.Linux.enumerate_ranges() gives paths as they appear in the maps,
like "/memfd:jit-zygote-cache (deleted)", so the "memfd:" prefix check
from 72a96226 never matched. When libc could not be found in the
target, the code swap then picked ART's read-only JIT zygote cache and
the first write failed with "unable to perform ptrace pokedata: I/O
error".
Mapping names are not always valid UTF-8: file paths may hold any
bytes, and on Android kernels before 5.17 an anonymous mapping's name
is read from a user pointer whenever the maps are read, so a name whose
string got unmapped shows up as random bytes. We hit this with Magisk's
Zygisk, where apps inherit [anon:System property context nodes] from
zygote with a garbage name.
GRegex rejects a subject that is not valid UTF-8 unless the pattern is
raw, so one such line made ProcMapsIter yield no mappings at all. The
injector then could not find libc in the target, and attach failed.
g_variant_new_string() returns NULL when given invalid UTF-8, and the
g_variant_ref_sink() that Vala adds around it then dereferences the
NULL, crashing frida-server.
Module paths in resolve-symbols, and string arguments and AF_UNIX
socket paths in read-events, come from /proc/<pid>/maps and from the
kernel, so they can hold any byte. Replace invalid sequences before
they go into a GVariant.
Houdini v3 rejects the deprecated NativeBridgeLoadLibrary() entrypoint
and returns NULL, which surfaces as "Process is not using emulation".
When the plain call fails and the extended entrypoint is available,
retry through it with a null namespace.
The emulated agent sees the native agent's threads when it walks the
host's thread list, as they are only cloaked in the native agent's Gum.
Have the native agent share its cloaked threads with the emulated agent,
as a snapshot followed by changes, applied to the emulated agent's own
cloak.
Depends on the CloakObserver interface in frida-gum.
On Android's native bridge, threads are created and torn down by the
host's Bionic, which the emulated agent's Interceptor cannot hook. This
broke Process.attachThreadObserver() in the emulated realm.
Have the native agent expose its own thread registry to the emulated
agent, which plugs it into Gum as a ThreadProvider: a snapshot on
activation, followed by added, removed and renamed events.
Depends on the ThreadProvider interface in frida-gum.
On x86_64 Android with a native bridge, the arm64 guest libraries may
be mapped before the host ones, and since the mapper looks up
dependencies by basename, the x86_64 agent got bound to the guest libc.
Only accept libraries whose machine and pointer size match the agent.
Fixes#1281.
Since 00d8ba24, the mapper picks library bases through
find_module_by_path(), whose heuristic starts a new candidate at every
mapping with file offset 0 and prefers executable ones. This breaks
libraries whose code shares file page 0 with their headers, such as
Android's libdl.so on x86_64, where the code mapping was picked as the
base, one page too high.
Instead, accept a mapping at file offset 0 as the base only if the
library's code segments are mapped executable where the linker would
have put them. This also rejects stray mappings of the file, so the
Android-specific libc workaround is no longer needed.
Fixes#1280.
When code memory is delegated to the host, Gum's early destructors need
to run while the host connection is still up, as some of them revert
hooks. Do so as the last step of removing instrumentation, which happens
inside unload() for agents that delegate.
A process may map a library file outside of the dynamic linker. When
such a mapping came before the loaded image, the mapper used it as the
library's base and bound imports into non-executable memory.
Pick each library's base through find_module_by_path(), which prefers
the mapping that the linker actually loaded.
Fixes#1278.
When the target exits, its other threads are terminated before DLL
detach notifications run, including the agent's. We then saw the
injector thread end without the agent having asked to stay resident,
and unmapped it while hooks such as the unwind broker's were still in
place. Anything running during the remainder of the shutdown could then
call into unmapped memory, e.g. the shim engine walking the stack.
Leave the agent mapped when the process is already exiting.
Fixesfrida/frida-gum#1200.
A spawned process used to make GLib replace the host's SIGCHLD handler
with its own, and reset it to SIG_DFL once the process was gone, which
kept Node.js from reaping its child_process children. And if the host
reset SIGCHLD in the meantime, the spawned process was never reaped.
Both rely on GLib using pidfd for child watches, as it does since
frida/glib#17, so they are skipped on kernels without pidfd.
See frida/frida-node#121.
Wait for in-flight agent unloads before cancelling pending I/O, so the
AgentController stays registered while agents that delegate code memory
to us revert their instrumentation. Also unload such agents instead of
just dropping their connection, so they don't stay resident.
The XNU agent is built for none-arm64-softfloat_pic with a target spec
from its directory, and the SDKs are published prebuilt. The kext is
built for macos-arm64e-kernel.
When the process can't create code itself, e.g. due to ACG on Windows,
plug in Gum memory hooks that have the host allocate and protect code
pages through the AgentController. Tear down instrumentation before
replying to unload(), while the host is still able to serve requests.
Map the agent out-of-process instead of having the target dlopen() it.
The loader loads any of the agent's dependencies that the target lacks,
reserves memory, and reports its address back. Imports are then
resolved against the target's libraries, and the loader is sent a plan
that it applies: the read-only segments are mapped straight from the
agent's file, so their pages are shared across targets, while the rest
is streamed over the control socket. The loader also applies
protections, runs the constructor, and on unload runs the destructor
and unmaps.
The target's libraries are opened through /proc/<pid>/map_files when
permitted, then through /proc/<pid>/root with an inode check, so
targets in containers resolve against their own files. When neither
works, e.g. because a library was replaced on disk, the image is copied
from the target's memory.
Gated by the mapper option, now enabled by default on Linux, and used
by the ptrace, procmem and kernel injectors alike. No memfd support is
required.
An agent may make its process exit right away, e.g. through exit() in
its entrypoint, and if that happens before we detach, the detach fails
with ESRCH even though the injection succeeded.
The Linux container handed the agent its control socket through a
LinuxInjectorState, making it the only non-injected caller that passes
an injector state. Encode it in the transport URI instead and pass NULL
like on the other OSes.
Map the agent straight into the target with the cross-platform
Gum.Mapper instead of writing a temporary DLL and loading it through
LoadLibrary. A small emitted stub runs the mapped images' constructors,
calls the agent's entrypoint, and runs the destructors when it is not
staying resident, in which case the mapper then unmaps it.
The agent is copied once into a pagefile-backed section per
architecture, whose handle helpers duplicate to map it, so its code
pages are shared across every target. The dbghelp and symsrv DLLs are
mapped alongside it and handed to the agent, so symbolication doesn't
load them either. The agent cloaks these mappings.
Gated by the existing mapper option, now enabled by default on
Windows, so nothing touches the filesystem; the LoadLibrary path stays
for builds with the mapper disabled. On either path the agent is handed
the range of the stub, so it stays cloaked for as long as it runs.
The Mach-O parser and mapper were renamed from GumDarwin* to GumMacho*
in frida-gum, and the mapper now allocates the payload itself, with the
image first, followed by the runtime and the extra memory holding our
stub code, data and stack. It also no longer drives Mach APIs on the
resolver's task, so give it a GumNativeProcessMemory for the target
task.
When they're subprojects their vapis are passed by path, so valac never
sees the packages they refer to, such as json-glib-1.0 and libsoup-3.0.
Use the packages they export through "vala_packages".
Have Meson write go-config.json through configure_file(), instead of
referring to the file detect.py wrote by name, which Meson warns about
and will reject in 2.0.
Resolving REPO_ROOT followed a symlinked subprojects/frida-core to the
frida-core checkout, so the parent's releng was not found and
frida-core's own was used instead. When that pinned a different deps
version, it wiped the shared toolchain in the middle of the build.
Idun now always goes through idund on Windows and Linux, so ship the
daemon inside the library when assets are embedded, extracting it to a
temporary file before starting the device monitor. With installed
assets Idun installs idund next to the library and resolves it from
there. A daemon that fails to start is reported on stderr and leaves
Apple devices unavailable instead of failing the whole backend.
Idun keeps its pairing identity and peers under its own config
directory now. Move existing records over on first use so pairings
survive the upgrade. Drop this on the next major bump.
Move the generic building blocks in lib/base into Efni, and the Apple
device protocol stack in src/fruity into Idun, both consumed as Meson
subprojects. The Gum-dependent injector and the host session stay as
the fruity backend on top of libidun. Tests for the moved code go
with it.
Efni has its own error domain; frida-core translates it to Frida.Error
at interface boundaries, and awaits go through a Promise facade so the
rest of the code keeps seeing Frida.Error. Barebone builds directly on
the Efni debugger abstraction and uses Efni.Error throughout.
sysprof is unused and fails to build on several targets, so disable it
for every machine in the GLib fallback options instead of only QNX.
While here, order the options to follow GLib's meson.options, with the
conditional ones grouped last in the same order.
Dispatch resume_input() to the main context that owns the WebSocket.
libsoup reads synchronously from it, which raced with that context's
thread over the incoming buffer and crashed in process_frame().
Split VirtualNetworkStack, which needs lwIP, out of network-stack.vala
and build the rest on all hosts. The macOS tunnel uses UdpSocket and
SystemNetworkStack.
Newer libsoup also limits the size of reassembled fragmented messages
through max-total-message-size. Set it to the same limit as the
incoming payload size when the property is available.
Return the address the data is loaded at, as ImHex does, so a pattern
can turn an absolute pointer into an offset. A section is its own
address space and keeps 0.
A view cannot run the base function, so it read such a field as an
absolute pointer.
Leave these fields out of the view, toJSON(), pattern() and the
declarations, and warn that they are only resolved when parsing.
A base function returns an offset in pattern space, like $ and
addressof(parent), but the decoder and parse() added it to the raw
value as an address. That only worked at address zero.
Resolve the target as the raw value plus the base, with $ at the
pointer field while the base function runs. Sign-extend sized
pointers such as T *p : s32, and treat zero as no target.
Add ImHex's std::ptr::relative_to_* functions and report the base
function in the type description.
A watch rebuild that runs after the project's files are gone has no
sources and no patterns, and commonDirectory() then indexed an empty
slice, panicking the whole process.
QNX's stdout is &_Stdout rather than a pointer variable, so code built
against its stdio.h handed libc's FILE to our fprintf(), which crashed
on its first log message. Define _Stdin, _Stdout and _Stderr ourselves,
and bind the slots through stdin, stdout and stderr.
Releasing a Compiler right after starting a watch could re-enter
finalization while a build callback took a reference, deleting the same
cgo.Handle twice. Stop the watch in dispose() and clear the handle
before calling into the backend.
Hold the session lock while starting the watch, so callbacks only see a
fully initialized session, and make disposal terminal so a queued
rebuild cannot recreate a context after the delegates are gone. Release
the delegates when construction fails.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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": "..." }
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Both Compiler and LanguageServer initialize it from their static
construct, and starting the Go runtime twice aborts the process
with "randinit twice".
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Loaded 100 of 415 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.