aboutsummaryrefslogtreecommitdiff
path: root/darwin-bootstrap/unrestrict.c
blob: c2f13bb3d3474eccfc2eb5c926bc4886025f8ec1 (plain) (blame)
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
/* This is an iOS executable spawned from posixspawn-hook.dylib, which accesses
 * a process and changes the name of any __RESTRICT,__restrict sections in the
 * main executable in memory.  Doing so prevents dyld from refusing to honor
 * DYLD_INSERT_LIBRARIES (which is a pretty dumb protection mechanism in the
 * first place, even on OS X).
 *
 * It exists as a separate executable because (a) such processes may be
 * launched with POSIX_SPAWN_SETEXEC, which makes posix_spawn act like exec and
 * replace the current process, and (b) if they're not, launchd (into which
 * posixspawn-hook is injected) still can't task_for_pid the child process
 * itself, because it doesn't have the right entitlements.
*/

#define IB_LOG_NAME "unrestrict"
#include "ib-log.h"
#include "darwin/mach-decls.h"
#include "substitute.h"
#include "substitute-internal.h"
#include <stdlib.h>
#include <syslog.h>
#include <signal.h>
#include <errno.h>
#include <stdio.h>
#include <mach/mach.h>
#include <mach-o/loader.h>

#define PROC_PIDFDVNODEINFO 1
#define PROC_PIDFDVNODEINFO_SIZE 176
int proc_pidfdinfo(int, int, int, void *, int);

static bool unrestrict_macho_header(void *header, size_t size) {
    struct mach_header *mh = header;
    if (mh->magic != MH_MAGIC && mh->magic != MH_MAGIC_64) {
        ib_log("bad mach-o magic");
        return false;
    }
    bool did_modify = false;
    size_t off = mh->magic == MH_MAGIC_64 ? sizeof(struct mach_header_64)
                                          : sizeof(struct mach_header);
    for (uint32_t i = 0; i < mh->ncmds; i++) {
        if (off > size || size - off < sizeof(struct load_command))
            break; /* whatever */
        struct load_command *lc = header + off;
        if (lc->cmdsize > size - off)
            break;
        #define CASES(code...) \
            if (lc->cmd == LC_SEGMENT) { \
                typedef struct segment_command segment_command_y; \
                typedef struct section section_y; \
                code \
            } else if (lc->cmd == LC_SEGMENT_64) { \
                typedef struct segment_command_64 segment_command_y; \
                typedef struct section_64 section_y; \
                code \
            }
        CASES(
            segment_command_y *sc = (void *) lc;
            if (lc->cmdsize < sizeof(*sc) ||
                sc->nsects > (lc->cmdsize - sizeof(*sc)) / sizeof(struct section)) {
                ib_log("bad segment_command");
                return false;
            }
            if (!strncmp(sc->segname, "__RESTRICT", 16)) {
                section_y *sect = (void *) (sc + 1);
                for (uint32_t i = 0; i < sc->nsects; i++, sect++) {
                    if (!strncmp(sect->sectname, "__restrict", 16)) {
                        strcpy(sect->sectname, "\xf0\x9f\x92\xa9");
                        did_modify = true;
                    }
                }
            }
        )
        #undef CASES

        if (off + lc->cmdsize < off) {
            ib_log("overflowing lc->cmdsize");
            return false;
        }
        off += lc->cmdsize;
    }
    return did_modify;
}

static void unrestrict(task_t task) {
    vm_address_t header_addr = 0;
    kern_return_t kr;

    /* alrighty then, let's look at the damage.  find the first readable
     * segment */
setback:;
    mach_vm_address_t segm_addr = 0;
    mach_vm_size_t segm_size = 0;
    vm_region_basic_info_data_64_t info;
    mach_msg_type_number_t info_count = VM_REGION_BASIC_INFO_COUNT_64;
    mach_port_t object_name;
    while (1) {
        kr = mach_vm_region(task, &segm_addr, &segm_size, VM_REGION_BASIC_INFO_64,
                            (vm_region_info_t) &info, &info_count, &object_name);
        if (kr == KERN_INVALID_ADDRESS) {
            /* nothing! maybe it's not there *yet*? this actually is possible */
            usleep(10);
            goto setback;
        } else if (kr) {
            ib_log("mach_vm_region(%lx): %x", (long) segm_addr, kr);
            return;
        }
        if (info.protection)
            break;

        segm_addr++;
    }

    size_t toread = 0x4000;
    if (segm_size < toread)
        toread = segm_size;
    if ((kr = vm_allocate(mach_task_self(), &header_addr, toread,
                          VM_FLAGS_ANYWHERE))) {
        ib_log("vm_allocate(%zx): %x", toread, kr);
        return;
    }
    mach_vm_size_t actual = toread;
    kr = mach_vm_read_overwrite(task, segm_addr, toread, header_addr,
                                &actual);
    if (kr || actual != toread) {
        ib_log("mach_vm_read_overwrite: %x", kr);
        return;
    }

    bool did_modify = unrestrict_macho_header((void *) header_addr, toread);

    if (did_modify) {
        if ((kr = vm_protect(mach_task_self(), header_addr, toread,
                             FALSE, info.protection))) {
            ib_log("vm_protect(%lx=>%d): %x",
                   (long) header_addr, info.protection, kr);
            return;
        }
        vm_prot_t cur, max;
        if ((kr = mach_vm_remap(task, &segm_addr, toread, 0,
                                VM_FLAGS_OVERWRITE,
                                mach_task_self(), header_addr, FALSE,
                                &cur, &max, info.inheritance))) {
            ib_log("mach_vm_remap(%lx=>%lx size=%zx): %x",
                   (long) header_addr, (long) segm_addr, toread, kr);
            return;
        }
    }
}



int main(int argc, char **argv) {
    if (argc != 4) {
        ib_log("wrong number of args");
        return 1;
    }

    const char *pids = argv[1];
    char *end;
    long pid = strtol(pids, &end, 10);
    if (!pids[0] || *end) {
        ib_log("pid not an integer");
        return 1;
    }

    const char *should_resume = argv[2];
    if (strcmp(should_resume, "0") && strcmp(should_resume, "1")) {
        ib_log("should_resume not 0 or 1");
        return 1;
    }

    const char *is_exec = argv[3];
    if (strcmp(is_exec, "0") && strcmp(is_exec, "1")) {
        ib_log("is_exec not 0 or 1");
        return 1;
    }

    /* double fork to avoid zombies */
    int ret = fork();
    if (ret == -1) {
        ib_log("fork: %s", strerror(errno));
        return 1;
    } else if (ret) {
        return 0;
    }

    if (IB_VERBOSE) {
        ib_log("unrestricting %ld (sr=%s, ie=%s)", pid,
               should_resume, is_exec);
    }

    int ec = 1;

    mach_port_t task;
    kern_return_t kr = task_for_pid(mach_task_self(), (pid_t) pid, &task);
    if (kr) {
        ib_log("TFP fail: %d", kr);
        goto fail;
    }

    if (is_exec[0] == '1') {
        int retries = 0;
        int wait_us = 1;
        while (1) {
            /* The process might not have transitioned yet.  We set up a dummy fd
             * 255 in the parent process which was marked CLOEXEC, so test if that
             * still exists.  AFAICT, Substrate's equivalent to this is not
             * actually correct.
             * TODO cleanup
             */
            char buf[PROC_PIDFDVNODEINFO_SIZE];
            /* A bug in proc_pidfdinfo makes it never return -1.  Yuck. */
            errno = 0;
            proc_pidfdinfo(pid, 255, PROC_PIDFDVNODEINFO, buf, sizeof(buf));
            if (errno == EBADF) {
                break;
            } else if (errno) {
                ib_log("proc_pidfdinfo: %s", strerror(errno));
                goto fail;
            }

            if (retries++ == 20) {
                ib_log("still in parent process after 20 retries");
                goto fail;
            }
            wait_us *= 2;
            if (wait_us > 200000)
                wait_us = 200000;
            while (usleep(wait_us))
                ;
        }
    }

    unrestrict(task);

fail:
    if (should_resume[0] == '1') {
        if ((kill(pid, SIGCONT))) {
            ib_log("kill SIGCONT: %s", strerror(errno));
            return 1;
        }
    }

    return ec;
}