Frida 15.1 Released ∞release
Introducing the brand new Swift bridge! Now that Swift has been ABI-stable since version 5, this long-awaited bridge allows Frida to play nicely with binaries written in Swift. Whether you consider Swift a static or a dynamic language, one thing is for sure, it just got a lot more dynamic with this Frida release.
Probably the first thing a reverser does when they start reversing a binary is
getting to know the different data structures that the binary defines. So it
made most sense to start by building the Swift equivalent of the
ObjC.protocols APIs. But since Swift has other first-class types,
i.e. structs and enums, and since the Swift runtime doesn’t offer reflection
primitives, at least not in the sense that Objective-C does, it meant we had to
dig a little deeper.
Luckily for us, the Swift compiler emits metadata for each type
defined by the binary. This metadata is bundled in a
TargetTypeContextDescriptor C++ struct, defined in
include/swift/ABI/Metadata.h at the time of writing. This data structure
includes the type name, its fields, its methods (if applicable,) and other useful
data depending on the type at hand. These data structures are pointed to by
relative pointers (defined in include/swift/Basic/RelativePointer.h.) In
Mach-O binaries, these are stored in the
So to dump types, Frida basically iterates over these data structures and parses them along the way, very similar to what dsdump does, except that you don’t have to build the Swift compiler in order to tinker with it.
Frida also has the advantage of being able to probe into
internal Apple dylibs written in Swift, and that’s because we don’t need to
dyld_shared_cache thanks to the private
getsectiondata API, which
gives us section offsets hassle-free.
To be on par with the Objective-C bridge, the Swift bridge has to support calling Swift functions, which also proved to be not as straight forward.
Swift defines its own calling convention,
swiftcall, which, to put it
succinctly, tries to be as efficient as possible. That means, not wasting load
and store instructions on structs that are smaller than 4 registers-worth of
size. That is, to pass those kinds of structs directly in registers. And since
that could quickly over-book our precious 8 argument registers
x7), it doesn’t use the first register for the
argument. It also defines an
error register where callees can store errors
which they throw.
What we just described above is termed “physical lowering” in the Swift compiler docs, and it’s implemented by the back-end, LLVM.
The process that precedes physical lowering is termed “semantic lowering,” which is the compiler front-end figuring out who “owns” a value and whether it’s direct or indirect. Some structs, even though they might be smaller than 4 registers, have to be passed indirectly, because, for example, they are generic and thus their exact memory layout is not known at compile time, or because they include a weak reference that has to be in-memory at all times.
We had to implement both semantic and physical lowering in order to be able
to call Swift functions. Physical lowering is implemented using JIT-compiled
adapter functions (thanks to the
Arm64Writer API) that does the necessary
swiftcall translation. Semantic lowering is implemented by utilizing
the type’s metadata to figure out whether we should pass a value directly or
The compiler docs are a great resource to learn more about the calling convention.
Because Swift passes structs directly in registers, there isn’t a 1-to-1 mapping between registers and actual arguments.
functions from the JS runtime, a good next step would be extending
to support Swift functions.
For functions that are not stripped, we use a simple regex to parse argument types and names, same for return values. After parsing them we retrieve the type metadata, figure out the type’s layout, then simply construct JS wrappers for each argument, which we pass the Swift argument value, however many registers it occupies.
Note that the bridge is still very early in development, and so:
- Currently supports Darwin arm64(e) only.
- Performance is not yet in tip-top shape, some edge case might not be handled properly and some bugs are to be expected.
- There’s a chance the API might change in breaking ways in the short-to-medium term.
- PRs and issues are very welcome!
Refer to the documentation for an up-to-date resource on the current API.
Changes in 15.1.0
- Implement the Swift bridge, which allows Frida to:
- Explore Swift modules along with types implemented in them, i.e. classes, structs, enums and protocols.
- Invoke functions that use the
- Intercept Swift functions and automatically parse their arguments and return values.
- Fix i/macOS regression where changes related to iOS 15 support ended up breaking support for attaching to Apple system daemons.
- gadget: Add interaction.parameters in connect mode. These parameters are then
“reflected” into app’s info under
parameters.config. Thanks @mrmacete!
Changes in 15.1.1
- gumjs: Fix Swift lifetime logic in the V8 runtime.