Repository navigation
Expand file tree
/
Copy pathpatch.c
More file actions
1062 lines (976 loc) · 33.9 KB
/
Copy pathpatch.c
File metadata and controls
1062 lines (976 loc) · 33.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2026 HackintARM contributors
/*
* gatehouse/src/patch.c - deliberate deviations from the shipped XNU image.
*
* Gatehouse owns exactly one place where the kernel is *changed* on the way in:
* here. Every entry is a named, auditable fixup with a comment explaining why
* it is needed and why it is safe, so that the loader stays honest about the
* fact that it is not running the kernel byte-for-byte as shipped.
*
* Load *completion* is not a deviation and does not belong here: resolving
* LC_DYLD_CHAINED_FIXUPS (what iBoot/dyld do for every Mach-O) lives in
* fixups.c, and copying the segments lives in macho.c. If you want to know
* how Gatehouse departs from Apple's build, this file is the whole answer.
*/
#include "gh.h"
#define ARM64_NOP 0xd503201fu
/* arm64e function furniture and a couple of one-instruction bodies. */
#define ARM64_BTI_C 0xd503245fu /* "bti c": only a direct branch may land */
#define ARM64_PACIBSP 0xd503237fu /* sign LR with the B key and SP */
#define ARM64_RETAB 0xd65f0fffu /* authenticate LR (B key, SP) and return */
#define ARM64_MOV_W0_0 0x52800000u /* "mov w0, #0" */
#define PAGE_MASK (~(u64)0xfff) /* 4 KiB page base (~int would truncate) */
static u32 rd32(const u8 *p)
{
return (u32)p[0] | ((u32)p[1] << 8) | ((u32)p[2] << 16) | ((u32)p[3] << 24);
}
static void wr32(u8 *p, u32 v)
{
p[0] = (u8)(v & 0xff);
p[1] = (u8)((v >> 8) & 0xff);
p[2] = (u8)((v >> 16) & 0xff);
p[3] = (u8)((v >> 24) & 0xff);
}
/* Linear 4-byte-aligned search for a little-endian instruction pattern. */
static u64 find_pattern(const u8 *base, u64 size, const u32 *pat, u32 n)
{
u64 i;
if (size < (u64)n * 4) {
return (u64)-1;
}
for (i = 0; i + (u64)n * 4 <= size; i += 4) {
u32 j;
for (j = 0; j < n; j++) {
if (rd32(base + i + j * 4) != pat[j]) {
break;
}
}
if (j == n) {
return i;
}
}
return (u64)-1;
}
/*
* Apple VM platform hypercalls on a machine that does not answer them.
*
* The vmapple kernel calls up to EL2 with hvc #0 to ask its host for things
* only a virtualisation host can give it. The ids live in Apple's own
* 0xc100____ space:
*
* 0xc1000000 PAC_INIT_KEY_STATE (once, at the top of common_start:
* the host picks the kernel's
* pointer-signing keys)
* 0xc1000001 default ROP/JOP pids (inlined at every site that reads
* them)
* 0xc1000003/4 publish a vcpu's state to the host
* 0xc1000005 guest feature query
*
* and every one of them treats "no answer" as fatal by branching to itself:
*
* hvc #0
* cbnz x0, . ; spin forever unless the host accepted
*
* EL2 on this machine is PSCI, which answers every function id it does not
* know with NOT_SUPPORTED (-1), so all 19 sites wedge -- common_start never
* even reaches the point where the kernel starts. None of the values carried
* here has to be agreed with anyone: the kernel primes its own keys, caches
* them in its own globals and then only ever signs and authenticates its own
* pointers with them, so their reset values are self-consistent; the
* context/feature calls are "tell the host" calls whose only consequence for
* the guest is a cached copy of what it said.
*
* We therefore neutralise exactly the wedge -- the branch to self after the
* trap -- and leave the hvc in place: the trap costs nothing, the call sites
* stay recognisable, and a machine that does have an AVP host still reaches
* it.
*
* The rule is deliberately narrow: an hvc whose x0 is built from a 0xc100
* halfword within the four instructions before it, followed by a branch to
* itself. PSCI's CPU_OFF and CPU_DEFAULT_SUSPEND (0x84000002/0xc400000e)
* also end in a parking loop, but there the CPU is meant never to come back,
* which is correct behaviour and must not be touched.
*/
#define ARM64_HVC0 0xd4000002u
/* "movz|movk wN|xN, #imm16, lsl #shift"? */
static int mov_imm(u32 insn, u32 *imm16, u32 *shift)
{
if ((insn & 0xff800000u) != 0x52800000u && /* movz w */
(insn & 0xff800000u) != 0x72800000u && /* movk w */
(insn & 0xff800000u) != 0xd2800000u && /* movz x */
(insn & 0xff800000u) != 0xf2800000u) { /* movk x */
return 0;
}
*imm16 = (insn >> 5) & 0xffffu;
*shift = ((insn >> 21) & 3u) * 16u;
return 1;
}
/* Does one of the four instructions before this hvc build an 0xc100 id? */
static int avp_call_id(const u8 *img, u64 at)
{
u32 k;
for (k = 1; k <= 4; k++) {
u32 imm16, shift;
if (mov_imm(rd32(img + at - 4 * k), &imm16, &shift) &&
imm16 == 0xc100u) {
return 1;
}
}
return 0;
}
/*
* A branch to itself: "b ." (B with a zero displacement) or "cbz/cbnz
* wN|xN, ." (conditional, zero 19-bit displacement). Nothing can leave such
* a loop, which is what makes these sites wedges rather than retries.
*/
static int is_self_branch(u32 insn)
{
if ((insn & 0xfc000000u) == 0x14000000u) { /* B */
return (insn & 0x03ffffffu) == 0u;
}
if ((insn & 0x7f000000u) == 0x34000000u || /* CBZ w/x */
(insn & 0x7f000000u) == 0x35000000u) { /* CBNZ w/x */
return ((insn >> 5) & 0x7ffffu) == 0u;
}
return 0;
}
static int patch_apple_hypercalls(u8 *img, u64 span)
{
u64 i;
u32 sites = 0;
for (i = 16; i + 8 <= span; i += 4) {
if (rd32(img + i) != ARM64_HVC0) {
continue;
}
if (!avp_call_id(img, i)) {
continue;
}
if (!is_self_branch(rd32(img + i + 4))) {
continue;
}
wr32(img + i + 4, ARM64_NOP);
sites++;
}
gh_loghex("vm platform hypercall wedges neutralised: ", sites);
return (int)sites;
}
/*
* IOPCIDevice::extendedFindPCICapability() capability-search fast path.
*
* macOS 27 answers PCI capability lookups for a device out of a per-device
* capability dictionary that IOPCIBridge::constructCapabilitiesDict() builds.
* The dictionary is keyed by capability *symbol*, and every capability a
* virtio-pci device exposes shares one symbol -- all five carry capability ID
* 0x09 (common / notify / isr / device / pci-cfg) -- so the dictionary collapses
* to a single entry and the fast path hands back the same capability whatever
* offset it is given. AppleVirtIOPCITransport::start() walks a device's vendor
* capabilities with extendedFindPCICapability(9, &offset) until the call
* reports none left, so the walk never terminates: the transport never
* registers, no IOMedia appears and there is no root device.
*
* This is not specific to virtio, nor to any machine -- any device that exposes
* more than one capability with the same ID trips it -- so the deviation lives
* here, in the loader, and every target gets it.
*
* The site is three instructions:
*
* ldr x8, [x21, #160] ; IOPCIDevice::reserved
* ldr x9, [x8, #472] ; reserved->capability dictionary (may be NULL)
* cbz x9, <search> ; no dictionary: take the reference search
*
* Replacing the conditional branch with an unconditional one sends every
* lookup through the reference config-space search -- the path Apple itself
* takes whenever the dictionary is absent -- which walks the PCI capability
* list in config space, iterates correctly and terminates.
*/
static int patch_apple_pci_capability_search(u8 *img, u64 span)
{
static const u32 site[] = { 0xf94052a8u, 0xf940ed09u };
u64 off = find_pattern(img, span, site, 2);
u32 cbz, imm;
if (off == (u64)-1) {
return 0;
}
/* The next instruction must be "cbz x9, <search>". */
cbz = rd32(img + off + 8);
if ((cbz & 0xff00001fu) != 0xb4000009u) {
return 0;
}
/*
* cbz reaches +/-1 MiB with a 19-bit immediate and b reaches +/-128 MiB
* with a 26-bit one, both scaled by 4 and measured from the same
* address, so the branch displacement carries over unchanged.
*/
imm = (cbz >> 5) & 0x7ffffu;
if (imm & 0x40000u) {
imm |= 0xfff80000u; /* sign-extend 19 -> 32 */
}
wr32(img + off + 8, 0x14000000u | (imm & 0x03ffffffu)); /* b <search> */
gh_loghex("PCI capability search on the reference path: ", 1);
return 1;
}
/*
* Root-volume authentication when the root was not attested by the host.
*
* macOS 27 on Apple Virtual platforms treats the root filesystem as an
* *authenticated* volume. iBoot forwards the sealed root hash to the kernel
* in the device tree (system-volume-auth-blob, root-snapshot-name) and
* bsd_init() insists on it:
*
* IOSecureBSDRoot(rootdevice); bsd_init.c:916
* ...
* require_rootauth = !imageboot_desired();
* if (require_rootauth) {
* autherr = VNOP_IOCTL(rootvnode, FSIOC_KERNEL_ROOTAUTH,
* NULL, 0, vfs_context_kernel());
* if (autherr) panic("rootvp not authenticated after mounting");
* }
*
* Both halves of that contract are missing here for the same reason -- the
* root did not come from Apple's boot chain -- so both deviations live here
* and every target gets them.
*
* - IOSecureBSDRoot() asks the platform expert to vouch for the root with
* callPlatformFunction("SecureRootName", ...). That is a *host* service:
* on Apple's own hypervisor the host attests the root volume it handed
* the guest. Nothing on the guest side can answer it, the dispatch never
* returns, and the boot stops in bsd_init() without printing anything.
*
* - APFS answers FSIOC_KERNEL_ROOTAUTH out of is_root_hash_
* authentication_required(), which is true for any release-build System
* volume ("Root Volume, root hash authentication is required"). With no
* iBoot-forwarded root hash and no ARV snapshot on the medium, nothing
* could ever satisfy it: the volume mounts and the kernel panics.
*
* So: the single "bl <IOSecureBSDRoot>" in bsd_init() becomes a nop, and
* is_root_hash_authentication_required() is made to return false -- the same
* statement the kernel logs ("root hash authentication is required"), negated,
* because without host attestation there is no root hash to check. The
* consequence to be honest about: a kernel patched this way cannot detect a
* tampered root volume.
*
* Neither edit hardcodes an address. Both functions are found by a C string
* they build ("SecureRootName", "is_root_hash_authentication_required"), then
* the adrp+add pair that materialises it, then the enclosing function entry,
* and the locators verify the shape of everything they touch.
*/
/*
* A kernel collection is loaded as a flat 1:1 image of its VA window, so a
* file offset and the address the code will run at differ by one constant
* (segment vmaddr - fileoff). Gatehouse never relies on that, but the
* locators below must decode pc-relative operands, which needs a real
* address. Return the constant only if every segment agrees on it, so an
* image with a different layout is left alone rather than mis-patched.
*/
static u64 kc_link_delta(const gh_macho *m)
{
u64 delta = (u64)-1;
u32 i;
for (i = 0; i < m->nsegs; i++) {
const gh_segment *s = &m->segs[i];
u64 d;
if (s->filesize == 0) {
continue; /* __bss-like */
}
d = s->vmaddr - s->fileoff;
if (delta == (u64)-1) {
delta = d;
} else if (delta != d) {
return (u64)-1;
}
}
return delta;
}
/* Offset of a NUL-terminated string in the image, or (u64)-1. */
static u64 find_cstring(const u8 *img, u64 span, const char *s)
{
u64 n = gh_strlen(s);
u64 i;
for (i = 0; i + n < span; i++) {
if (img[i] != (u8)s[0]) {
continue;
}
if (gh_memcmp(img + i, s, n) == 0 && img[i + n] == 0) {
return i;
}
}
return (u64)-1;
}
/*
* Find the "adrp xN, <page>; add xN, xN, #<off>" pair that materialises a
* given address. Apple's compiler emits the two back to back, which is what
* makes a string reference a usable function locator.
*/
static u64 find_adrp_add(const u8 *img, u64 span, u64 delta, u64 target)
{
u64 i;
for (i = 0; i + 8 <= span; i += 4) {
u32 insn = rd32(img + i);
i64 imm;
u64 page;
u32 add, imm12;
if ((insn & 0x9f000000u) != 0x90000000u) { /* ADRP */
continue;
}
imm = (i64)((((u64)(insn >> 5) & 0x7ffffu) << 2) |
((insn >> 29) & 3u));
if (imm & (1 << 20)) {
imm -= (1 << 21); /* sign-extend */
}
page = ((delta + i) & PAGE_MASK) + (u64)(imm << 12);
if (page != (target & PAGE_MASK)) {
continue;
}
add = rd32(img + i + 4);
if ((add & 0x7f000000u) != 0x11000000u || /* ADD (immediate) */
(add & 0x80000000u) == 0u || /* 64-bit form */
((add >> 22) & 3u) != 0u || /* no shift */
(add & 0x1fu) != ((add >> 5) & 0x1fu)) { /* same register */
continue;
}
imm12 = (add >> 10) & 0xfffu;
if (page + imm12 == target) {
return i;
}
}
return (u64)-1;
}
/*
* Walk back from a reference to the enclosing function's entry: the nearest
* "pacibsp" (every function in the collection is arm64e and opens with the
* B-key pair), including a preceding "bti c" when it has one.
*/
static u64 find_function_entry(const u8 *img, u64 ref)
{
u64 e;
if (ref < 4) {
return (u64)-1;
}
for (e = ref; e >= 4; e -= 4) {
if (rd32(img + e) == ARM64_PACIBSP) {
break;
}
}
if (e < 4) {
return (u64)-1;
}
if (rd32(img + e - 4) == ARM64_BTI_C) {
e -= 4;
}
return e;
}
/* Count "bl" sites that land on a function; record up to `max` of them. */
static u32 find_bl_callers(const u8 *img, u64 span, u64 delta, u64 func,
u64 *out, u32 max)
{
u64 i;
u32 n = 0;
for (i = 0; i + 4 <= span; i += 4) {
u32 insn = rd32(img + i);
i64 imm;
if ((insn & 0xfc000000u) != 0x94000000u) { /* BL */
continue;
}
imm = (i64)(insn & 0x03ffffffu);
if (imm & (1 << 25)) {
imm -= (1 << 26); /* sign-extend */
}
if ((u64)((i64)(delta + i) + (imm << 2)) == func) {
if (out && n < max) {
out[n] = i;
}
n++;
}
}
return n;
}
/*
* Replace a predicate's body with "return false", leaving its BTI landing pad
* in place. The stub re-signs the link register with the same B-key/SP pair
* the real prologue would have used, so it can return through the caller's
* frame without ever touching SP.
*/
static void write_pretend_false(u8 *img, u64 entry)
{
u64 at = entry;
if (rd32(img + at) == ARM64_BTI_C) {
at += 4;
}
wr32(img + at, ARM64_MOV_W0_0);
wr32(img + at + 4, ARM64_PACIBSP);
wr32(img + at + 8, ARM64_RETAB);
}
static int patch_apple_root_auth(u8 *img, u64 span, const gh_macho *m)
{
u64 delta = kc_link_delta(m);
u64 str, ref, fn, calls[4];
int applied = 0;
if (delta == (u64)-1) {
return 0;
}
/* 1. bsd_init()'s call to IOSecureBSDRoot(). */
str = find_cstring(img, span, "SecureRootName");
if (str != (u64)-1) {
ref = find_adrp_add(img, span, delta, delta + str);
fn = (ref == (u64)-1) ? (u64)-1 : find_function_entry(img, ref);
if (fn != (u64)-1 &&
find_bl_callers(img, span, delta, delta + fn, calls, 4) == 1) {
wr32(img + calls[0], ARM64_NOP);
gh_loghex("host root attestation bypassed: ", 1);
applied++;
}
}
/* 2. APFS's root-hash requirement, answered "no". */
str = find_cstring(img, span, "is_root_hash_authentication_required");
if (str != (u64)-1) {
ref = find_adrp_add(img, span, delta, delta + str);
fn = (ref == (u64)-1) ? (u64)-1 : find_function_entry(img, ref);
if (fn != (u64)-1 && rd32(img + fn) == ARM64_BTI_C &&
rd32(img + fn + 4) == ARM64_PACIBSP) {
write_pretend_false(img, fn);
gh_loghex("root hash authentication relaxed: ", 1);
applied++;
}
}
return applied;
}
/*
* Diagnostic handover: run a different program as PID 1.
*
* XNU's load_init_program() execs one path -- "/sbin/launchd" in a release
* kernel -- and no boot-arg or device tree property changes it. On a machine
* whose root volume is not (yet) a complete macOS install, that leaves the
* boot wherever launchd stops, with no way to look around, and the first thing
* a bring-up needs is a shell. Linux has init= for exactly this; Gatehouse
* only edits the image when the caller asks it to (main.c's gh_init=<path>),
* and never touches the shipped default.
*
* The locator proves the byte string it rewrites is the one
* load_init_program() is about to exec, without hardcoding an address:
*
* 1. "load_init_program: attempting to load %s" is the format string of
* the printf() immediately before that exec, and the compiler
* materialises both it and the path argument with the usual
* "adrp xN, <page>; add xN, xN, #<off>" pair, back to back:
*
* adrp x20, <page> ; the path
* add x20, x20, #<off>
* str x20, [sp] ; argv[0]
* adrp x0, <page> ; the format string
* add x0, x0, #<off>
* bl printf
* mov x0, x20
* bl load_init_program_at_path
*
* so the path is the adrp+add pair eight bytes before the format
* string's, and the format string itself must be referenced exactly
* once (a second reference means the shape is not what we expect).
* 2. what that pair resolves to must look like the thing we are about to
* overwrite: a NUL-terminated absolute path in the image.
* 3. the replacement must fit in the space the shipped path occupies
* (14 bytes plus its NUL), so the strings that follow it -- the kernel
* uses the neighbours too -- are never disturbed.
*
* Any deviation from that shape and the image is left alone rather than
* half-patched.
*/
static const char init_fmt[] =
"load_init_program: attempting to load %s\n";
/* Decode "adrp xN, <page>"; return the page address, or (u64)-1. */
static u64 adrp_page(const u8 *img, u64 at, u64 delta, u32 *reg)
{
u32 insn = rd32(img + at);
i64 imm;
if ((insn & 0x9f000000u) != 0x90000000u) {
return (u64)-1;
}
imm = (i64)((((u64)(insn >> 5) & 0x7ffffu) << 2) | ((insn >> 29) & 3u));
if (imm & (1 << 20)) {
imm -= (1 << 21);
}
*reg = insn & 0x1fu;
return ((delta + at) & PAGE_MASK) + (u64)(imm << 12);
}
/* Decode the "add xN, xN, #imm" that pairs with it; return the address. */
static u64 adrp_add_target(const u8 *img, u64 at, u64 page, u32 reg)
{
u32 insn = rd32(img + at);
if ((insn & 0x7f000000u) != 0x11000000u || /* ADD (immediate) */
(insn & 0x80000000u) == 0u || /* 64-bit form */
((insn >> 22) & 3u) != 0u || /* no shift */
(insn & 0x1fu) != reg || /* destination ... */
((insn >> 5) & 0x1fu) != reg) { /* ... and source */
return (u64)-1;
}
return page + ((insn >> 10) & 0xfffu);
}
int gh_patch_init_program(u8 *img, u64 span, const gh_macho *m,
const char *path)
{
u64 delta = kc_link_delta(m);
u64 fstr, first = (u64)-1, page, str = (u64)-1;
u64 i, k, n, orig, hits;
u32 refs = 0, reg;
if (delta == (u64)-1 || path == 0 || path[0] != '/') {
return 0;
}
n = gh_strlen(path);
/* The format string must exist and be referenced exactly once. */
fstr = find_cstring(img, span, init_fmt);
if (fstr == (u64)-1) {
return 0;
}
for (i = 0; i + 8 <= span; i += 4) {
u32 r;
page = adrp_page(img, i, delta, &r);
if (page == (u64)-1) {
continue;
}
if (adrp_add_target(img, i + 4, page, r) == delta + fstr) {
refs++;
if (first == (u64)-1) {
first = i;
}
}
}
if (refs != 1) {
return 0;
}
/*
* The path is materialised by its own adrp+add just ahead of the
* format string's -- with the argv[0] store between the two pairs --
* so walk back over the instructions preceding the format reference
* and keep the pair that really does resolve to a NUL-terminated
* absolute path. Exactly one of them may, or the shape is not the one
* this patch was written against.
*/
for (hits = 0, k = 1; k <= 6 && first >= 4 * k; k++) {
u64 at = first - 4 * k, cand, off;
page = adrp_page(img, at, delta, ®);
if (page == (u64)-1) {
continue;
}
cand = adrp_add_target(img, at + 4, page, reg);
if (cand == (u64)-1 || cand < delta) {
continue;
}
off = cand - delta;
if (off >= span || img[off] != '/') {
continue;
}
for (orig = 0; off + orig < span && img[off + orig] != 0 &&
orig < 256; orig++) {
}
if (orig == 0 || orig == 256) {
continue;
}
hits++;
str = cand;
}
if (hits != 1) {
return 0;
}
i = str - delta; /* image offset of the path */
for (orig = 0; i + orig < span && img[i + orig] != 0 &&
orig < 256; orig++) {
}
/* The replacement has to fit in the space the shipped path occupies,
* so the strings that follow it are never disturbed. */
if (n > orig) {
return 0;
}
gh_memcpy(img + i, path, n + 1);
return 1;
}
/*
* gh_patch_init_argument - choose what PID 1 gets as its one argument.
*
* load_init_program_at_path() copies the init program's path to the scratch
* page as argv[0], and -- only when RB_SINGLE is set -- a second string as
* argv[1]. XNU ships that second string as "-s" and bounds it to three
* characters ("cmp xN, #4"), so a shell starts with nowhere to read from:
* PID 1 is created without file descriptors, and "-s" means "read a script
* from stdin".
*
* A rescue shell wants a path there instead. "sh /dev/console" makes the
* shell open the console itself and read its commands from it, which is the
* only way to reach a prompt without a descriptor of our own; the first line
* typed can then be "exec >/dev/console 2>&1" and the rest of the session is
* ordinary. So re-point the argument's adrp+add at the replacement string
* and widen the bound to fit it. Nothing else about init changes.
*
* Locator: the "add xN, x0, #1; cmp xN, #4" pair that bounds the argument is
* unique in the whole collection (it is the only place that bounds a strlen
* result to four). That comparison is not a bespoke length check: it is the
* inlined __copyout_chk() test for the copyout() of the "-s" literal, whose
* compile-time object size is three. The string about to be copied is the
* adrp+add pair in the window just before it, and must read "-s"; anything
* else is a shape this patch does not understand, and it refuses.
*
* The replacement cannot be one of the kernel's own strings: the image has no
* NUL-terminated "/dev/console" (the one that exists is inside a packed
* string table), so the caller reserves a slot for it in the loaded window,
* outside the image, and passes its offset. This writes the bytes too, so
* there is a single place that decides what PID 1 receives.
*/
int gh_patch_init_argument(u8 *img, u64 span, u64 win, const gh_macho *m,
const char *arg, u64 arg_off)
{
u64 delta = kc_link_delta(m);
u64 site = (u64)-1, pair = (u64)-1, i, n;
u32 reg = 0, pair_reg = 0, w;
int found = 0;
if (delta == (u64)-1 || arg == 0 || arg[0] != '/') {
return 0;
}
n = gh_strlen(arg);
if (n == 0 || n + 1 > 0xfffu) {
return 0;
}
/* The string goes after the image and stays inside the window. */
if (arg_off < span || arg_off + n + 1 > win) {
return 0;
}
/* The one "add xN, x0, #1; cmp xN, #4" in the image. */
for (i = 0; i + 8 <= span; i += 4) {
u32 a = rd32(img + i);
u32 c = rd32(img + i + 4);
u32 r;
if ((a & 0xffc00000u) != 0x91000000u || /* add (immediate) */
((a >> 5) & 0x1fu) != 0u || /* from x0 ... */
((a >> 10) & 0xfffu) != 1u) { /* ... plus one */
continue;
}
r = a & 0x1fu;
if (r == 31 || (c & 0xffc00000u) != 0xf1000000u || /* cmp */
(c & 0x1fu) != 31u || /* xzr */
((c >> 5) & 0x1fu) != r ||
((c >> 10) & 0xfffu) != 4u) {
continue;
}
if (site != (u64)-1) {
return 0;
}
site = i;
}
if (site == (u64)-1) {
return 0;
}
/* ... and the "-s" its result is about to be bounds-checked against. */
i = site;
while (i >= 4 && site - i < 96) {
u64 page, t, hit;
i -= 4;
page = adrp_page(img, i, delta, ®);
if (page == (u64)-1) {
continue;
}
t = adrp_add_target(img, i + 4, page, reg);
if (t == (u64)-1 || t < delta) {
continue;
}
hit = t - delta;
if (hit + 3 > span || img[hit] != '-' || img[hit + 1] != 's' ||
img[hit + 2] != 0) {
continue;
}
if (found) {
return 0;
}
found = 1;
pair = i;
pair_reg = reg;
}
if (!found) {
return 0;
}
/* The replacement has to be in the image, and unambiguous. */
gh_memcpy(img + arg_off, arg, n + 1);
/* Re-point "adrp xN, <page>; add xN, xN, #<off>" at it. */
{
i64 off = (i64)((delta + arg_off) & PAGE_MASK) -
(i64)((delta + pair) & PAGE_MASK);
u32 pages;
if (off & 0xfff) {
return 0;
}
off >>= 12;
if (off < -(1 << 20) || off >= (1 << 20)) {
return 0;
}
pages = (u32)(off & 0x1fffff);
w = 0x90000000u | (((pages >> 2) & 0x7ffffu) << 5) |
((pages & 3u) << 29) | pair_reg;
wr32(img + pair, w);
w = 0x91000000u | (((u32)((delta + arg_off) & 0xfff)) << 10) |
(pair_reg << 5) | pair_reg;
wr32(img + pair + 4, w);
}
/*
* Widen the bound to the replacement's length plus its NUL, plus one:
* this is the inlined __copyout_chk() test, which panics when
* "chk_size < len", and the compiler turned that into
* "cmp len, #chk_size + 1; b.hs panic".
*/
w = rd32(img + site + 4);
w = (w & ~0xffc00u) | ((u32)(n + 2) << 10);
wr32(img + site + 4, w);
return 1;
}
/*
* image4 "ignition blob" sysctl.
*
* macOS 27 runs the Darwin Ignition Sequence before the first user process
* reaches main(). Its configuration step opens with
*
* sysctlbyname("security.mac.img4.ignition_blob", ...)
*
* which AppleImage4 answers by asking the SEP for an Image4 blob and waiting
* for the answer (_darwin_trap_ignition_get_blob spins on a completion flag
* before it can copy the blob out). There is no SEP behind this platform, so
* the answer never comes: the sysctl never returns, and since PID 1 is the only
* user process, userland stops dead before launchd ever runs. The same call is
* the first thing every later process makes, so every one of them hangs there
* too.
*
* A machine where the blob genuinely is not available returns ENOENT from the
* handler, and dyld has an explicit path for that: it logs "ignition blob not
* available" and continues with the reduced attestation this platform can offer
* anyway. We reach that path by taking the leaf out of the sysctl tree, which
* makes the name lookup itself fail with ENOENT. The name is a single string
* literal referenced exactly once, by the sysctl_oid that registers the node, so
* renaming it cannot disturb the handler, the Mach trap that serves the same
* blob, or any other sysctl.
*/
static int patch_apple_image4_ignition_sysctl(u8 *img, u64 span)
{
static const char name[] = "ignition_blob";
const u64 n = sizeof(name) - 1;
u64 hits = 0, off = 0, i;
for (i = 1; i + n + 1 <= span; i++) {
if (img[i - 1] != 0 || img[i + n] != 0) {
continue;
}
if (gh_memcmp(img + i, name, n) != 0) {
continue;
}
hits++;
off = i;
}
if (hits != 1) {
gh_log("ignition blob: sysctl leaf not unique, left alone\n");
return 0;
}
img[off + n - 1] = 'x'; /* "ignition_blob" -> "ignition_blox" */
gh_loghex("image4 ignition-blob sysctl masked at ", off);
return 1;
}
/*
* Boot probes for the display swap path.
*
* The guest has a paravirtual display but never presents a frame. The first
* question is whether WindowServer submits a swap at all, and if so how far
* the guest's IOMobileFramebuffer/AppleParavirtDisplay path gets before it
* stops. Each site below is a prologue `mov` whose only effect is a register
* copy; replacing it with `hvc #0x4d0N` traps to Gatehouse (lib.c emulates
* the copy and resumes). A probe changes nothing observable to the guest and
* can be removed once the path is understood.
*/
static int patch_probe(u8 *img, u64 span, const char *name,
const u32 *pat, u32 n, u32 target_off, u32 expected,
u32 imm)
{
u64 off = find_pattern(img, span, pat, n);
u64 at;
if (off == (u64)-1) {
gh_uart_puts("probe: ");
gh_uart_puts(name);
gh_uart_puts(" pattern not found\n");
return 0;
}
at = off + target_off;
if (at + 4 > span) {
gh_uart_puts("probe: ");
gh_uart_puts(name);
gh_uart_puts(" outside image\n");
return 0;
}
if (rd32(img + at) != expected) {
gh_uart_puts("probe: ");
gh_uart_puts(name);
gh_uart_puts(" unexpected instruction, left alone\n");
return 0;
}
wr32(img + at, 0xd4000002u | ((imm & 0xffffu) << 5));
gh_uart_puts("probe: ");
gh_uart_puts(name);
gh_uart_puts(" armed\n");
return 1;
}
static int patch_swap_probes(u8 *img, u64 span)
{
/* IOMobileFramebufferLegacy::swap_submit */
static const u32 swap_submit[] = {
0xd503245fu, 0xd503237fu, 0xd10303ffu, 0xa9066ffcu,
0xa90767fau, 0xa9085ff8u, 0xa90957f6u, 0xa90a4ff4u,
0xa90b7bfdu, 0x9102c3fdu, 0xaa0103f6u, 0xaa0003f5u,
};
/* IOMobileFramebufferLegacy::swap_queue */
static const u32 swap_queue[] = {
0xd503245fu, 0xd503237fu, 0xd10543ffu, 0xa90f6ffcu,
0xa91067fau, 0xa9115ff8u, 0xa91257f6u, 0xa9134ff4u,
0xa9147bfdu, 0x910503fdu, 0xaa0103f4u, 0xaa0003f3u,
0xb0ff49c8u,
};
/* AppleParavirtDisplay::swap_begin_gated */
static const u32 swap_begin[] = {
0xd503245fu, 0xd503237fu, 0xa9be4ff4u, 0xa9017bfdu,
0x910043fdu, 0xaa0003f3u, 0xf9561400u, 0xf9400010u,
};
/* IOMobileFramebufferLegacy::swap_complete_gated */
static const u32 swap_complete[] = {
0xd503245fu, 0xd503237fu, 0xd10303ffu, 0xa9066ffcu,
0xa90767fau, 0xa9085ff8u, 0xa90957f6u, 0xa90a4ff4u,
0xa90b7bfdu, 0x9102c3fdu, 0xaa0103f6u, 0xaa0003f3u,
0xb0ff49e8u,
};
/* IOMobileFramebufferUserClient::swap_start */
static const u32 uc_swap_start[] = {
0xd503245fu, 0xd503237fu, 0xa9be4ff4u, 0xa9017bfdu,
0x910043fdu, 0xaa0103f4u, 0xaa0003f3u, 0xb940f008u,
};
/* IOMobileFramebufferUserClient::swap_submit */
static const u32 uc_swap_submit[] = {
0xd503245fu, 0xd503237fu, 0xd10103ffu, 0xa90157f6u,
0xa9024ff4u, 0xa9037bfdu, 0x9100c3fdu, 0xaa0103f3u,
0xaa0003f4u, 0x711b805fu,
};
/* IOMobileFramebufferLegacy::note_vbl_ISR */
static const u32 note_vbl[] = {
0xd503245fu, 0xd503237fu, 0xd10103ffu, 0xa90157f6u,
0xa9024ff4u, 0xa9037bfdu, 0x9100c3fdu, 0xaa0103f5u,
0xaa0003f3u, 0xf9400010u, 0xaa0003f1u, 0xf2f9b431u,
};
/* IOMobileFramebufferLegacy::swapping_client_did_start */
static const u32 swapping_did_start[] = {
0xd503245fu, 0xd503237fu, 0xa9be4ff4u, 0xa9017bfdu,
0x910043fdu, 0xaa0003f3u, 0xf94ea000u, 0x94003b96u,
0x52838708u, 0x8b080260u, 0x94003bcfu, 0x35000720u,
};
/* IOMobileFramebufferLegacy::swap_start */
static const u32 iomfb_swap_start[] = {
0xd503245fu, 0xd503237fu, 0xa9be4ff4u, 0xa9017bfdu,
0x910043fdu, 0xaa0103f3u, 0xaa0003f4u, 0xaa0203e0u,
0x97ffd879u, 0xb40003a0u, 0xaa0003e3u, 0xf94da680u,
};
/* IOMobileFramebufferUserClient::externalMethod */
static const u32 uc_external_method[] = {
0xd503245fu, 0xd503237fu, 0xa9bd57f6u, 0xa9014ff4u,
0xa9027bfdu, 0x910083fdu, 0xaa0203f4u, 0xaa0103f5u,
0xaa0003f3u, 0xf9406c00u, 0xf9400010u, 0xaa0003f1u,
0xf2f9b431u, 0xdac11a30u, 0xd2812011u, 0x8b110210u,
};
/* AppleParavirtDisplay::start_hardware block_invoke (lambda 0) */
static const u32 pvg_start_hw_block[] = {
0xd503245fu, 0xd503237fu, 0xd10143ffu, 0xa9034ff4u,
0xa9047bfdu, 0x910103fdu, 0xaa0003f4u, 0xf9401413u,
0xaa1303e0u, 0x97ffec3cu, 0x34000120u, 0x52838188u,
};
/* AppleParavirtDisplay::program_hardware_swap_gated */
static const u32 program_hw_swap[] = {
0xd503245fu, 0xd503237fu, 0xd10243ffu, 0x6d0223e9u,
0xa9036ffcu, 0xa90467fau, 0xa9055ff8u, 0xa90657f6u,
0xa9074ff4u, 0xa9087bfdu, 0x910203fdu, 0xb4005581u,
0xaa0103f4u, 0xaa0003f3u,
};
int applied = 0;
applied += patch_probe(img, span, "swap_submit", swap_submit,
(u32)(sizeof(swap_submit) / 4), 0x28,
0xaa0103f6u, 0x4d01u);
applied += patch_probe(img, span, "swap_queue", swap_queue,
(u32)(sizeof(swap_queue) / 4), 0x2c,
0xaa0003f3u, 0x4d02u);
applied += patch_probe(img, span, "swap_begin_gated", swap_begin,
(u32)(sizeof(swap_begin) / 4), 0x14,
0xaa0003f3u, 0x4d03u);
applied += patch_probe(img, span, "swap_complete_gated", swap_complete,
(u32)(sizeof(swap_complete) / 4), 0x2c,
0xaa0003f3u, 0x4d04u);
applied += patch_probe(img, span, "program_hardware_swap_gated",
program_hw_swap,
(u32)(sizeof(program_hw_swap) / 4), 0x34,
0xaa0003f3u, 0x4d05u);
applied += patch_probe(img, span, "uc_swap_start", uc_swap_start,
(u32)(sizeof(uc_swap_start) / 4), 0x18,
0xaa0003f3u, 0x4d06u);
applied += patch_probe(img, span, "uc_swap_submit", uc_swap_submit,
(u32)(sizeof(uc_swap_submit) / 4), 0x20,
0xaa0003f4u, 0x4d07u);
applied += patch_probe(img, span, "note_vbl_ISR", note_vbl,
(u32)(sizeof(note_vbl) / 4), 0x1c,
0xaa0103f5u, 0x4d08u);
applied += patch_probe(img, span, "swapping_client_did_start",
swapping_did_start,
(u32)(sizeof(swapping_did_start) / 4), 0x14,
0xaa0003f3u, 0x4d09u);
applied += patch_probe(img, span, "iomfb_swap_start", iomfb_swap_start,
(u32)(sizeof(iomfb_swap_start) / 4), 0x14,
0xaa0103f3u, 0x4d0au);
applied += patch_probe(img, span, "uc_externalMethod",
uc_external_method,
(u32)(sizeof(uc_external_method) / 4), 0x18,
0xaa0203f4u, 0x4d0bu);
applied += patch_probe(img, span, "pvg_start_hw_block",
pvg_start_hw_block,
(u32)(sizeof(pvg_start_hw_block) / 4), 0x18,
0xaa0003f4u, 0x4d0cu);