Last week I wrote about a proof-of-concept (poc) hot-code reloading on macOS/arm64 with Zig. By the way, if you didn’t get
a chance to read it yet, you can do so by following this link here. To summarise,
the poc was a success, and we have a way to perform hot-code reloading in Zig by directly writing to the running
process’ memory. There was one huge problem though: we needed sudo
. If you recall, the Mach mechanisms we employed
to get it to work are also used by debuggers on macOS including lldb
. If you launch lldb
right this instant,
you will notice you are not required to elevate your privileges with sudo
. Huh. So how is that? Is it because
we are using Apple’s lldb
which would somehow be autotrusted by the OS? But wait, if I build stock lldb
I can
still debug everything without sudo
1. The short answer to this is: entitlements!
According to Apple’s official docs, an entitlement is 2
a right or privilege that grants an executable particular capabilities. For example, an app needs the HomeKit Entitlement—along with explicit user consent—to access a user’s home automation network. An app stores its entitlements as key-value pairs embedded in the code signature of its binary executable.
If you look closely at lldb
’s sources 1, you will notice a *.plist
file in resources/
subdir. If you cat
its contents, you will see something like this
$ cat resources/debugserver-macosx-entitlements.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.debugger</key>
<true/>
</dict>
</plist>
This file asks for com.apple.security.cs.debugger
entitlement, and if we consult Apple’s docs again 3, we will
see that this is a debugging tool entitlement which is
a boolean value that indicates whether the app is a debugger and may attach to other processes or get task ports.
Woah—may attach to other processes or get task ports—this is exactly what we need for hot-code reloading! Actually, this is somewhat of an overkill as we don’t really want to attach to any running process. We want to attach to a child process that we, the compiler, spawned. Alas, I couldn’t find an entitlement that would be less powerful, so this will have to do for now.
So, we have found the right entitlement, but how do we now make it part of the Zig compiler executable?
If you recall, as part of the OS hardening, every executable on arm64/macOS has to be code signed at all times. Both
Apple’s system linker ld64
, and Zig’s zld
will create an adhoc code signature and embed it within the binary by
default. As it turns out, embedding entitlements can be done as part of the said code signature too!
If you are on macOS, you can use codesign
tool to overwrite the code signature with included entitlements file as
follows
$ codesign --entitlements Info.plist -f -s - myexe
This command will replace the existing signature with an adhoc code signature with entitlements from Info.plist
.
This is great, and that’s all that’s required to fix the use of sudo
in our hot-code reloading poc. But, if you are like
me, and you value complete understanding of the entire process involved, it’s not enough. So let’s try adding the
missing functionality to Zig’s linker instead. We will then expose a new flag to Zig’s build system which will let
us pass path to entitlements, which can then be routed to the linker for inclusion as part of the generated code
signature. As an added bonus to this approach, besides enhancing our knowledge and understanding, Zig users will not be
required to have command-line tools (CLT) or Xcode installed at all to be able to bake in entitlements in their
executables/dynamic libraries.
First things first though, let’s examine the structure of a typical (adhoc) code signature with entitlements. For this,
I will compile a simple hello world style program in C with zig cc
and replace the signature generated
by zld
with that generated with codesign
tool with embedded entitlements. The C program will be dead simple
// hello.c
#include <stdio.h>
int main(int argc, char* argv[]) {
fprintf(stderr, "Hello, world!\n");
return 0;
}
We will embed debugging tool entitlement depicted in the previous section.
$ zig cc hello.c -o hello
$ codesign --entitlements Info.plist -f -s - hello
hello: replacing existing signature
In order to confirm we have properly embedded the entitlement, let’s use codesign
to examine the entitlements embedded
in the binary (if any)
$ codesign --display --entitlements - hello
Executable=/Users/kubkon/dev/examples/hello_c/hello
[Dict]
[Key] com.apple.security.cs.debugger
[Value]
[Bool] true
You can actually use codesign
to debug-print a lot of information about the embedded code signature, but since we
are trying to explore the precise binary structure of the code signature, we could use with a bit more verbosity still.
For that very reason, I have started writing a replacement tool for otool
called zacho
with the intention of having
a lot more freedom in choosing what and how to parse and how to display. The source is freely available on GitHub 4,
and here’s the output it produces for hello
$ zacho -c hello
Code signature data:
file = { 49776, 68944 }
{
Magic = 0xfade0cc0
Length = 1153
Count = 5
}
{
Type: CSSLOT_CODEDIRECTORY(0x0)
Offset: 52
}
{
Type: CSSLOT_REQUIREMENTS(0x2)
Offset: 827
}
{
Type: CSSLOT_ENTITLEMENTS(0x5)
Offset: 839
}
{
Type: CSSLOT_DER_ENTITLEMENTS(0x7)
Offset: 1093
}
{
Type: CSSLOT_SIGNATURESLOT(0x10000)
Offset: 1145
}
{
Magic: CSMAGIC_CODEDIRECTORY(0xfade0c02)
Length: 775
Version: 0x20400
Flags: 0x2
Hash offset: 359
Ident offset: 88
Number of special slots: 7
Number of code slots: 13
Code limit: 49776
Hash size: 32
Hash type: 2
Platform: 0
Page size: 12
Reserved: 0
Scatter offset: 0
Team offset: 0
Reserved: 0
Code limit 64: 0
Offset of executable segment: 0
Limit of executable segment: 16384
Executable segment flags: 0x1
Ident: hello-55554944a708652fc7506bccf6a2b70216280825
Special slot for CSSLOT_DER_ENTITLEMENTS:
8c0a93d2a721c7b7 8cde6cb284c2e798
9e9ae0dbe2f85ac1 0fc847f2ca1e0a77
Special slot for CSSLOT_SIGNATURESLOT:
0000000000000000 0000000000000000
0000000000000000 0000000000000000
Special slot for CSSLOT_ENTITLEMENTS:
e8c4dc3360497b20 568c1420ccafec93
a1511b6b10a9f6da cf9bdfd39e18afed
Special slot for CSSLOT_APPLICATION:
0000000000000000 0000000000000000
0000000000000000 0000000000000000
Special slot for CSSLOT_RESOURCEDIR:
0000000000000000 0000000000000000
0000000000000000 0000000000000000
Special slot for CSSLOT_REQUIREMENTS:
987920904eab650e 75788c054aa0b052
4e6a80bfc71aa32d f8d237a61743f986
Special slot for CSSLOT_INFOSLOT:
0000000000000000 0000000000000000
0000000000000000 0000000000000000
Code slot (0x100000000 - 0x100001000):
b0a6307466b8b198 fb4efed2bf0a035e
d11954111f2cf856 fb725b145bb91e77
Code slot (0x100001000 - 0x100002000):
ad7facb2586fc6e9 66c004d7d1d16b02
4f5805ff7cb47c7a 85dabd8b48892ca7
Code slot (0x100002000 - 0x100003000):
ad7facb2586fc6e9 66c004d7d1d16b02
4f5805ff7cb47c7a 85dabd8b48892ca7
Code slot (0x100003000 - 0x100004000):
110721bd92675d0f 0641b8963afc09f8
45d34efd762677c6 a4a3bb222fa75562
Code slot (0x100004000 - 0x100005000):
ad7facb2586fc6e9 66c004d7d1d16b02
4f5805ff7cb47c7a 85dabd8b48892ca7
Code slot (0x100005000 - 0x100006000):
ad7facb2586fc6e9 66c004d7d1d16b02
4f5805ff7cb47c7a 85dabd8b48892ca7
Code slot (0x100006000 - 0x100007000):
ad7facb2586fc6e9 66c004d7d1d16b02
4f5805ff7cb47c7a 85dabd8b48892ca7
Code slot (0x100007000 - 0x100008000):
ad7facb2586fc6e9 66c004d7d1d16b02
4f5805ff7cb47c7a 85dabd8b48892ca7
Code slot (0x100008000 - 0x100009000):
5ab20475893bf05f 3e1078c8270f0efc
82319c5257e92cea 05f969d3eaeb2d4c
Code slot (0x100009000 - 0x10000a000):
ad7facb2586fc6e9 66c004d7d1d16b02
4f5805ff7cb47c7a 85dabd8b48892ca7
Code slot (0x10000a000 - 0x10000b000):
ad7facb2586fc6e9 66c004d7d1d16b02
4f5805ff7cb47c7a 85dabd8b48892ca7
Code slot (0x10000b000 - 0x10000c000):
ad7facb2586fc6e9 66c004d7d1d16b02
4f5805ff7cb47c7a 85dabd8b48892ca7
Code slot (0x10000c000 - 0x10000d000):
46fa859070a7a3b8 232b701b96750232
c34d6c25ed775969 a90932f3417526dc
}
{
Magic: CSMAGIC_REQUIREMENTS(0xfade0c01)
Length: 12
Data:
0000000000000000 0000000000000000 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
}
{
Magic: CSMAGIC_EMBEDDED_ENTITLEMENTS(0xfade7171)
Length: 254
Data:
3c3f786d6c207665 7273696f6e3d2231 <?xml version="1
2e302220656e636f 64696e673d225554 .0" encoding="UT
462d38223f3e0a3c 21444f4354595045 F-8"?>\x0a<!DOCTYPE
20706c6973742050 55424c494320222d plist PUBLIC "-
2f2f4170706c652f 2f44544420504c49 //Apple//DTD PLI
535420312e302f2f 454e222022687474 ST 1.0//EN" "htt
703a2f2f7777772e 6170706c652e636f p://www.apple.co
6d2f445444732f50 726f70657274794c m/DTDs/PropertyL
6973742d312e302e 647464223e0a3c70 ist-1.0.dtd">\x0a<p
6c69737420766572 73696f6e3d22312e list version="1.
30223e0a3c646963 743e0a202020203c 0">\x0a<dict>\x0a <
6b65793e636f6d2e 6170706c652e7365 key>com.apple.se
6375726974792e63 732e646562756767 curity.cs.debugg
65723c2f6b65793e 0a202020203c7472 er</key>\x0a <tr
75652f3e0a3c2f64 6963743e0a3c2f70 ue/>\x0a</dict>\x0a</p
6c6973743e0a0000 0000000000000000 list>\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
}
{
Magic: CSMAGIC_EMBEDDED_DER_ENTITLEMENTS(0xfade7172)
Length: 52
Data:
702a020101b02530 230c1e636f6d2e61 p*\x02\x01\x01\xb0%0#\x0c\x1ecom.a
70706c652e736563 75726974792e6373 pple.security.cs
2e64656275676765 720101ff00000000 .debugger\x01\x01\xff\x00\x00\x00\x00
}
{
Magic: CSMAGIC_BLOBWRAPPER(0xfade0b01)
Length: 8
Data:
}
(NB the zero-padding you see in zacho
’s output is just an artifact of how it prints the binary blobs. The actual
contents of the code signature structure does not require any alignment between its different internal blobs.)
Hmm, OK, this is quite a lot… Let’s start from the beginning then. Each code signature starts with a header called
SuperBlob
const CSMAGIC_EMBEDDED_SIGNATURE: u32 = 0xfade0cc0;
const SuperBlob = extern struct {
/// Magic number
magic: u32 = CSMAGIC_EMBEDDED_SIGNATURE,
/// Total length of SuperBlob
length: u32,
/// Number of index BlobIndex entries following this struct
count: u32,
};
(NB almost forgot to mention that every field in the code signature is in big-endian, which if forgotten about, can render your code signing tool absolutely useless.)
For our purposes, magic
will always equal CSMAGIC_EMBEDDED_SIGNATURE
but, in general, as far as I can tell, this
does not have to be the case. The length
field denotes the total length of the code signature (with all its bells
and whistles), while count
denotes how many BlobIndex
es are following it. Here is the definition of a BlobIndex
const BlobIndex = extern struct {
/// Type of entry
@"type": u32,
/// Offset of entry
offset: u32,
};
As you probably have guessed by now, each BlobIndex
tells you at which offset in the code signature an inner blob of
some type is embedded. In our example, we have a total count of 5 inner blobs: code directory (CSSLOT_CODEDIRECTORY
),
requirements (CSSLOT_REQUIREMENTS
), entitlements (CSSLOT_ENTITLEMENTS
), Distinguished Encoding Rules (DER)
entitlements (CSSLOT_DER_ENTITLEMENTS
), and signature (CSSLOT_SIGNATURESLOT
).
The parser can use BlobIndex
to selectively jump to the desired embedded blob without having to parse the entire
code signature. Each embedded blob shares a common header called GenericBlob
which is defined as
const GenericBlob = extern struct {
/// Magic number
magic: u32,
/// Total length of blob
length: u32,
/// Followed by a slice of bytes of length `length - 8`
data: [*]u8,
};
Currently, GenericBlob
acts as a generic placeholder for most blobs with the exception of code directory which has
a specialised blob structure.
In what follows, I’d like us to focus on code directory (CSMAGIC_CODEDIRECTORY
) and entitlements (CSMAGIC_EMBEDDED_ENTITLEMENTS
)
blobs. This is all we need to embed the entitlements within a code signature actually; none of the extras generated
by codesign
tool are actually needed. It’s worth pointing out that the adhoc code signature as generated by the linkers
consists of only the former, i.e., the code directory.
The code directory structure comprises of many fields
const CodeDirectory = extern struct {
/// Magic number (CSMAGIC_CODEDIRECTORY)
magic: u32,
/// Total length of CodeDirectory blob
length: u32,
/// Compatibility version
version: u32,
/// Setup and mode flags
flags: u32,
/// Offset of hash slot element at index zero
hashOffset: u32,
/// Offset of identifier string
identOffset: u32,
/// Number of special hash slots
nSpecialSlots: u32,
/// Number of ordinary (code) hash slots
nCodeSlots: u32,
/// Limit to main image signature range
codeLimit: u32,
/// Size of each hash in bytes
hashSize: u8,
/// Type of hash (cdHashType* constants)
hashType: u8,
/// Platform identifier; zero if not platform binary
platform: u8,
/// log2(page size in bytes); 0 => infinite
pageSize: u8,
spare2: u32,
scatterOffset: u32,
teamOffset: u32,
spare3: u32,
codeLimit64: u64,
/// Offset of executable segment
execSegBase: u64,
/// Limit of executable segment
execSegLimit: u64,
/// Executable segment flags
execSegFlags: u64,
};
However, of particular interest are hashOffset
, identOffset
, nSpecialSlots
, and nCodeSlots
.
In order to describe each of the fields, let’s first examine how a typical code directory blob is laid out in file.
Following its header, we expect a NUL
-delimited C string containing the identifier of the program. The first element of this
string coincides with the offset stored as identOffset
in the header. The length of the string is not stored since it’s
NUL
-delimited. In our example, the identifier equals hello-55554944a708652fc7506bccf6a2b70216280825
.
{
Magic: CSMAGIC_CODEDIRECTORY(0xfade0c02)
Length: 775
Version: 0x20400
Flags: 0x2
Hash offset: 359
Ident offset: 88
Number of special slots: 7
Number of code slots: 13
Code limit: 49776
Hash size: 32
Hash type: 2
Platform: 0
Page size: 12
Reserved: 0
Scatter offset: 0
Team offset: 0
Reserved: 0
Code limit 64: 0
Offset of executable segment: 0
Limit of executable segment: 16384
Executable segment flags: 0x1
Ident: hello-55554944a708652fc7506bccf6a2b70216280825
...
Next, we expect N := nSpecialSlots
hashes of size hashSize
and type hashType
(e.g., 32
bytes long and of type
Sha256
). OK, but what are we actually hashing? Special slots encode hashes of other embedded blobs such as entitlements.
A couple important points to note here. The number of special slots has a static maximum number of 7
, however, not all slots
need to be present (if you run zacho
on a binary produced by ld64
or zld
, since the linkers only embed adhoc
code signature by default, you will notice there are no special slots present). Starting index is #1
rather than
#0
as we are normally used to. The slots have their indices in decreasing order, i.e., slot #7
is written to file
first, then #6
, etc. Indexes for each slot have a fixed meaning represented by the following constants
const CSSLOT_INFOSLOT: u32 = 1;
const CSSLOT_REQUIREMENTS: u32 = 2;
const CSSLOT_RESOURCEDIR: u32 = 3;
const CSSLOT_APPLICATION: u32 = 4;
const CSSLOT_ENTITLEMENTS: u32 = 5;
const CSSLOT_DER_ENTITLEMENTS: u32 = 7;
with one exception: index #6
encodes a signature slot which is represented by a constant const CSSLOT_SIGNATURESLOT: u32 = 0x10000
.
When populating slots, if we populate entitlements slot which happens at index #5
, we need to write out every
other slot with index smaller than #5
. If those other slots are unpopulated in the code signature, we simply write
out hashes filled with 0
s.
...
Special slot for CSSLOT_DER_ENTITLEMENTS:
8c0a93d2a721c7b7 8cde6cb284c2e798
9e9ae0dbe2f85ac1 0fc847f2ca1e0a77
Special slot for CSSLOT_SIGNATURESLOT:
0000000000000000 0000000000000000
0000000000000000 0000000000000000
Special slot for CSSLOT_ENTITLEMENTS:
e8c4dc3360497b20 568c1420ccafec93
a1511b6b10a9f6da cf9bdfd39e18afed
Special slot for CSSLOT_APPLICATION:
0000000000000000 0000000000000000
0000000000000000 0000000000000000
Special slot for CSSLOT_RESOURCEDIR:
0000000000000000 0000000000000000
0000000000000000 0000000000000000
Special slot for CSSLOT_REQUIREMENTS:
987920904eab650e 75788c054aa0b052
4e6a80bfc71aa32d f8d237a61743f986
Special slot for CSSLOT_INFOSLOT:
0000000000000000 0000000000000000
0000000000000000 0000000000000000
...
Finally, following the special slots, we should reach offset of hashOffset
which denotes the start of code hashes:
hashes of the executable file split into pages of size pageSize
. Note that the pageSize
does not have to be equal
to the actual hardware page size (e.g., 16KB
for arm64), and is typically set to 4KB
by most tooling.
zld
however sets it according to the hardware page size.
...
Code slot (0x100000000 - 0x100001000):
b0a6307466b8b198 fb4efed2bf0a035e
d11954111f2cf856 fb725b145bb91e77
Code slot (0x100001000 - 0x100002000):
ad7facb2586fc6e9 66c004d7d1d16b02
4f5805ff7cb47c7a 85dabd8b48892ca7
...
Code slot (0x10000c000 - 0x10000d000):
46fa859070a7a3b8 232b701b96750232
c34d6c25ed775969 a90932f3417526dc
}
If you recall, embedded entitlements don’t have a special structure and are encoded using GenericBlob
directly
where the contents of the entitlements file is embedded as raw bytes in the data
field
{
Magic: CSMAGIC_EMBEDDED_ENTITLEMENTS(0xfade7171)
Length: 254
Data:
3c3f786d6c207665 7273696f6e3d2231 <?xml version="1
2e302220656e636f 64696e673d225554 .0" encoding="UT
462d38223f3e0a3c 21444f4354595045 F-8"?>\x0a<!DOCTYPE
20706c6973742050 55424c494320222d plist PUBLIC "-
2f2f4170706c652f 2f44544420504c49 //Apple//DTD PLI
535420312e302f2f 454e222022687474 ST 1.0//EN" "htt
703a2f2f7777772e 6170706c652e636f p://www.apple.co
6d2f445444732f50 726f70657274794c m/DTDs/PropertyL
6973742d312e302e 647464223e0a3c70 ist-1.0.dtd">\x0a<p
6c69737420766572 73696f6e3d22312e list version="1.
30223e0a3c646963 743e0a202020203c 0">\x0a<dict>\x0a <
6b65793e636f6d2e 6170706c652e7365 key>com.apple.se
6375726974792e63 732e646562756767 curity.cs.debugg
65723c2f6b65793e 0a202020203c7472 er</key>\x0a <tr
75652f3e0a3c2f64 6963743e0a3c2f70 ue/>\x0a</dict>\x0a</p
6c6973743e0a0000 0000000000000000 list>\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
}
Believe it or not, we are now armed with everything we need to get our poc working without sudo
.
To do that, we just need to tweak the zld
linker to accept path to an entitlements file, read its contents, and save
it as a GenericBlob
(and also remember about hashing the blob and saving the hash in special slot index #5
in the code directory).
Since this morning, zld
(and Zig) accepts path to an entitlements file which is then embedded within the code
signature of the binary. The hcs-macos
branch 5 takes advantage of this and lets us build the self-hosted Zig compiler
binary with debugging tool entitlement embedded into its code signature
$ zig-stage1 build -Dentitlements=../resources/Info.plist
$ codesign --display --entitlements - zig-out/bin/zig
Executable=/Users/kubkon/dev/zig/zig-out/bin/zig
[Dict]
[Key] com.apple.security.cs.debugger
[Value]
[Bool] true
Hope you enjoy the demo without sudo this time! :-)
Demo captured on M1 MacBook Air, macOS 12.2.1, latest Zig self-hosted compiler with patch from hcs-macos
branch. 5