The per-frame RGBA-to-BGRA conversion ran on the main actor
and starved the async continuations that drive injection, so
attaching to a booted emulator stalled. Move it into
nonisolated helpers that adopt() awaits, and mark
SharedFrameBuffer Sendable so it can cross the actor hop.
Race adb wait-for-device against the launcher exiting, so a
failed launch fails fast rather than blocking forever, and
reap the qemu by its gdb port when a boot dies before it
records the pid.
templates and availability spawned `emulator -list-avds` on every
render, so opening the boot sheet -- and clicking a template, which
re-queries them all -- stuttered for seconds. Cache the listing and
warm it off the main thread when the picker or sheet appears.
Collapse the two per-architecture Android entries into one; the
chosen AVD now decides the architecture, retiring the message that
wrongly claimed Frida could only instrument the other one.
Closing the display unmapped the shared frame at once, but cancelling a
task only asks it to stop: a screenshot already in flight goes on to read
through the base address, and the process dies on a page that is no
longer there.
Let the mapping go when the last reader does.
Inflating and scanning the kernel image to see whether it names vsock
takes some 28 seconds, and the launch waits for it. On Windows the
answer changes nothing: the transport is the virtio-serial hostlink
either way. A run of the emulator check goes from 66 to 39 seconds.
An agent that oopses inside the guest looks like one that hangs while
the emulator's output is thrown away. FRIDA_EMULATOR_KERNEL_LOG names
a file to collect it in.
frida-core now reads a kernel once and answers what it names, so the
image config carries that rather than a path, and the vsock probe asks
it for the symbol instead of searching the image for the word.
That probe was never a symbol lookup: kallsyms stores its names
compressed, so the match came from whatever else in the kernel happens
to spell vsock. It took an LZ4 inflater of its own to look, which goes
with it.
An x86_64 AVD boots and streams, and its kernel now gives up its
symbols, but the agent never reaches it: the backend parks on a
breakpoint at schedule() that is never reported hit. The gate was
widened while that was being chased and should not have been.
QEMU's named-pipe chardev on Windows delivers what it writes and never
reads what is sent back, so the QMP handshake cannot complete over one.
A loopback port works, and with `server,nowait` QEMU no longer holds
the machine still until something connects -- so the pipe is opened,
held and handed down no longer.
The emulator bridges a guest's vsock to a host UNIX socket, which
Windows has none of, so the guest is reached over the virtio-serial
hostlink there whatever its kernel offers. That channel is opened
through QMP, which QEMU serves over a named pipe on Windows -- and it
holds the machine still until something connects to that pipe, then
gives the pipe up for good once the connection goes.
So the pipe is opened here, as soon as QEMU is spawned and before the
guest is waited on, and held for the barebone backend to adopt by
handle. Waiting for adb first would have deadlocked: the guest cannot
boot until the pipe is answered.
The transport carries the QMP address as a string now rather than a
socket URL, since each host spells it differently.
Also drops the single-core launch, which nothing turned out to need.
frida-core now skips the emulator instrumentation off Darwin and
carries an i386-64bit feature document, so the aarch64 gdbstub shim
is no longer what keeps an x86_64 AVD out. Its kallsyms parser still
wants a raw or gzipped arm64 Image, which an LZ4-packed x86 bzImage
is not, so the gate stays as it is.
An AVD's config names the ABI it boots, and the agent that gets
injected has to match it, so the ABI is read rather than assumed and
the flavor follows from it. Everything on a Windows host is x86_64,
where the old arm64 assumption would have asked for an agent that
does not fit.
Which is as far as it goes for now: Frida reaches the emulator's QEMU
through a gdbstub shim that resolves aarch64_cpu_gdb_write_register,
so an x86_64 guest boots, streams its display and reports its stub,
then fails inside instrumentation. The backend offers only what can
actually be injected into, and says which ABIs it did find when that
leaves nothing:
Frida can only instrument arm64 AVDs, and the ones installed are
x86-64. Create one in Android Studio.
The emulator's control channel was the only thing pulling grpc-swift
in, and with it swift-nio-ssl, which does not build on Windows at all
-- it has no branch for the platform and its PosixPort reaches for
mlock, lstat and readlink. Nothing here wants TLS: the endpoint is
plaintext on the loopback, and only seven calls are ever made.
So the wire is spoken directly. GRPCConnection opens one HTTP/2
stream per call over swift-nio-http2 and frames length-prefixed
protobuf into it; EmulatorControlClient names the seven. That drops
grpc-swift-2, grpc-swift-nio-transport, grpc-swift-protobuf and
swift-nio-ssl from the graph, along with 7840 lines of generated
service stubs. The generated messages stay.
The rest is what running it on Windows turned up:
- AVD discovery split on "\n" and trimmed only .whitespaces, so
every name kept a trailing \r and every AVD was dropped.
- An AVD config spells its paths with backslashes, which URL keeps
as ordinary characters rather than separators.
- emulator and adb want .exe, and the SDK sits under LOCALAPPDATA.
- Port reservation and process lookup move into HostPort and
HostProcessLookup, which QemuMachine's two copies also wanted.
- The launcher re-execs into QEMU there, so the discovery ini is
named after that process; the one naming this launch's debug port
is taken when the launcher's own is absent.
Frames still arrive through a mapping rather than the socket:
CreateFileMapping backs the same file:// handle the emulator is
handed elsewhere.
A guest without vsock is refused outright on Windows, where the
virtio-serial fallback would ask QEMU for a QMP UNIX socket and be
told "unix sockets are not available on windows".
LUMA_EMULATOR_DISPLAY_ONLY drives launch, control and streaming
without a guest agent. Against a Pixel_2 AVD it reports a 1080x1920
first frame and nine more in three seconds.
Android builds its x86 kernels with CONFIG_KERNEL_LZ4, so the payload
is neither gzip nor a PE and the image the scan sees is the bare
bzImage, which names none of the kernel's symbols. Teach the scan the
LZ4 legacy frame and add names(_:in:) for asking whether a kernel has
a symbol at all.
The emulator's own android-30 x86_64 kernel-ranchu is such an image:
39MB of payload behind an LZ4 stream at offset 16106, with "vsock"
inside it and nothing to find outside.
The emulator launcher hands QEMU off to launchd, so terminating
the launcher leaves the VM and its injected helper running.
Kill the QEMU process directly.
Drive the emulator's gRPC control API: stream its screen into an
embedded display through a shared-memory frame transport sized from the
device's reported resolution, re-opened when a notification says it
changed; forward pointer and key input; and take ready snapshots with
the SnapshotService.
kernelSupportsVsock spawns gzip and reads back the whole kernel; on the
main thread it froze the boot -- gzip's output filled the pipe before
the write finished, wedging both ends. Feed gzip off-thread while
draining it, and run the whole inspection off the main actor.
A guest kernel without vsock cannot use the pipe-vsock transport, so
inspect the AVD's kernel image and, when it names no vsock, launch the
emulator single-core and hand the agent a virtio-serial port it drives
over the guest's PCIe config space at the ranchu ECAM base.
Boot from FRIDA_EMULATOR_SNAPSHOT when set, for a fast iteration loop.
Launch an AVD made in Android Studio with a GDB stub, find its QEMU by
the debug port on its command line, and report the stub, kernel image
and hostlink so Frida can attach. The user picks from the AVDs the
emulator lists; the kernel image is mined for symbols, so none is asked
for.
Add the androidEmulator debug stub and the pipe-over-vsock agent
transport, and map both onto their barebone config: the connection
carries the hosting QEMU's pid so the backend can instrument its
gdbstub, and the guest dials out to a host socket for the hostlink.
Windows calls it Path, so looking the variable up as PATH found
nothing and set a second one beside it holding only the installation
directory. Names are matched without regard to case, so the emulators
were handed whichever came first and could not see the libraries the
app had lined up for them.
Read and write the name that is already there, and lay the block out
the way CreateProcessW asks, so a pair that differs only in case
cannot change places.
Off Apple this ran /usr/bin/env gzip, which Windows answers the way
it answered xz: no such file. The Alpine arm64 kernel is gzipped, so
that starter image could not be unpacked at all.
Drop the gzip packaging with it -- no template asks for it, and the
kernels that are wrapped come through LinuxKernelImage.
Foundation calls CreateProcessW with CREATE_UNICODE_ENVIRONMENT and
nothing else, and offers no way to add to that. The app has no
console of its own, so every console child it started -- an
emulator, qemu-img, the agent CLI -- was given a fresh command
prompt to look at.
Spawn through ChildProcess instead, which asks for CREATE_NO_WINDOW
on Windows and is Process everywhere else.
The emulators were downloaded from a qemu-<build> release built in
MSYS2 against MinGW's GLib and pixman, so the installer carried
those beside the vcpkg ones the app links: two copies of GLib and
friends.
Build them in the Windows job instead, against the vcpkg prefix,
cached like the other dependencies on the QEMU version and the
build script. QEMU builds under MinGW and nowhere else on Windows,
but UCRT64 and CLANGARM64 sit on the same C runtime as the MSVC
DLLs they now link, so a descriptor means the same on both sides.
The x86_64 environment moves from MINGW64 for that reason.
The emulators live a directory below the app, where nothing looks
for its GLib on its own, so they are started with the installation
root on PATH.
Windows drops out of the QEMU workflow, which macOS still uses.
A rejected response became a file-does-not-exist error, and both
libraries then threw the cause away and guessed at it: a refused
agent was reported as one nobody had published.
Name the status the server answered, and carry it through.
Only Windows looks for it beside the executable, so a copy travelling
with the app comes up without its romfiles anywhere else. Pass -L when
a share directory sits next to the emulator.
Keep a stopping machine listed until its shutdown returns, guard
against a second stop, and let the button say so and go quiet
instead of firing again.
Map the shifted row so keys like ':' reach the guest, and hold
shift around a stroke that needs it. Send a character on its own
key-down, tapping down and up, rather than tracking its phases.
Read the version stamped into a fetched agent's note, and list the
last releases from GitHub to learn what each flavor has published.
Offer the latest to download, name it on the button, and show an
update once a newer one lands. Drop the pinned version constant.