source: trunk/base/src/pextlib1.0/sip_copy_proc.c @ 141420

Last change on this file since 141420 was 141420, checked in by cal@…, 4 years ago

base: Fix trace mode on El Capitan

OS X El Capitan introduces System Integrity Protection for files. Executables
with this flag set will be started in a sanitized environment by the kernel,
stripping all DYLD_* variables. This breaks trace mode, because tracing relies
on preloading to wrap file related system calls using DYLD_INSERT_LIBRARIES.

A trivial workaround for the problem is to make a copy of the affected
binaries, which will strip the flag, and then adjust the invocation of the
binary to execute the copy instead (but leaving argv[0] as-is to avoid giving
the program an indication of being run from a non-standard location).

This change implements this approach by copying the SIP-flagged binaries to
$prefix/var/macports/sip-workaround on demand iff

  • the system has the SF_RESTRICTED flag defined
  • a binary is started with DYLD_INSERT_LIBRARIES set
  • the file exists and has SF_RESTRICTED set
  • the file isn't SUID or SGID (which we could not reliably copy, and which have never preserved DYLD_* variables)

If the file to be executed is a script and has a shebang line, the checks are
run on the interpreter instead, and if necessary, the interpreter is copied.
This requires interpreting the shebang line in user space.

Copies are created on-demand and are lazy: The file modification times are
checked before overwriting an existing copy. Copies are created in a per-user
folder, which will be created on-demand in a 1777 directory (like /tmp).

Changes are also needed way before darwintrace.dylib first runs: The DYLD_*
variables are already stripped in src/pextlib1.0/system.c, where
/usr/bin/sandbox-exec and /bin/sh are run, which both have the SF_RESTRICTED
flag on 10.11 now. Consequently, the same copying approach is applied there.

Because macports build run in a sandbox, the sandbox boundaries are extended to
allow access to $prefix/var/macports/sip-workaround.

File size: 17.0 KB
Line 
1/* vim: set et sw=4 ts=4 sts=4: */
2/*
3 * sip_copy_proc.c
4 * $Id$
5 *
6 * Copyright (c) 2015 Clemens Lang <cal@macports.org>
7 * Copyright (c) 2015 The MacPorts Project
8 * All rights reserved.
9 *
10 * Redistribution and use in source and binary forms, with or without
11 * modification, are permitted provided that the following conditions
12 * are met:
13 * 1. Redistributions of source code must retain the above copyright
14 *    notice, this list of conditions and the following disclaimer.
15 * 2. Redistributions in binary form must reproduce the above copyright
16 *    notice, this list of conditions and the following disclaimer in the
17 *    documentation and/or other materials provided with the distribution.
18 * 3. Neither the name of The MacPorts Project nor the names of its contributors
19 *    may be used to endorse or promote products derived from this software
20 *    without specific prior written permission.
21 *
22 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 * POSSIBILITY OF SUCH DAMAGE.
33 */
34
35#define _DARWIN_FEATURE_64_BIT_INODE
36
37#include <errno.h>
38#include <fcntl.h>
39#include <stdbool.h>
40#include <stdio.h>
41#include <stdlib.h>
42#include <string.h>
43#include <sys/socket.h>
44#include <sys/stat.h>
45#include <sys/time.h>
46#include <sys/types.h>
47#include <sys/uio.h>
48#include <unistd.h>
49
50#include "sip_copy_proc.h"
51
52#include <config.h>
53#ifndef DARWINTRACE_SIP_WORKAROUND_PATH
54#warning No value for DARWINTRACE_SIP_WORKAROUND_PATH found in config.h, using default of /tmp/macports-sip, which will fail unless you create it with mode 01777
55#define DARWINTRACE_SIP_WORKAROUND_PATH "/tmp/macports-sip"
56#endif
57
58/**
59 * Frees an array of strings and the array itself.
60 */
61static void free_argv(char *argv[]) {
62        char **arg = argv;
63        while (arg && *arg) {
64                free(*arg);
65                *arg = NULL;
66                arg++;
67        }
68
69        free(argv);
70}
71
72typedef enum _copy_needed_return_t {
73    copy_needed_error,
74    copy_not_needed,
75    copy_is_needed
76} copy_needed_return_t;
77
78/**
79 * Helper function to determine whether the binary indicated by \a path
80 * supports library injection using DYLD_INSERT_LIBRARIES directly or needs to
81 * be copied to a temporary path to support it.
82 *
83 * The following conditions must be fulfilled for the copy to be necessary:
84 *  - \a environ needs to contain a variable that starts with
85 *    DYLD_INSERT_LIBRARIES
86 *  - If the file at \a path has a shebang, its shebang line will be read and
87 *    the following checks will be done against the interpreter binary.
88 *    Additionally, if the copy is necessary, the arguments given in \a argc
89 *    (the number of arguments) and \a argv (the arguments itself) will be
90 *    prefixed with the command and arguments from the shebang line. The
91 *    original first argument will be replaced with \a path to make sure it is
92 *    absolute.
93 *  - \a path (or the interpreter given in the shebang of path) must have the
94 *    \c SF_RESTRICTED flag set.
95 *  - \a path (or the interpreter given in the shebang of path) must not be
96 *    SUID or SGID.
97 *
98 * @param path The absolute path of the binary to be executed
99 * @param argc The number of arguments passed in \a argv
100 * @param argv The arguments to be passed to the file to be executed
101 * @param outargv Pointer to a modified array of arguments. Only valid if \c
102 *                copy_is_needed is returned. May be \c NULL, in which case no
103 *                modifications to the original \c argv were necessary. If
104 *                non-null, a dynamically allocated array of dynamically
105 *                allocated elements. The last element of the array is \c NULL,
106 *                which makes \c *outargv suitable for passing to execve(2).
107 *                Note that instead of the given \c path, you should pass \c
108 *                (*outargv)[0] to execve(2) as first argument.
109 * @param environ The environment for the program to be started. Will be
110 *                checked for the presence of a DYLD_INSERT_LIBRARIES variable.
111 * @param st Pointer to struct stat that will contain information about \c
112 *           path, or of \c outargv isn't \c NULL, about \c (*outargv)[0]. This
113 *           can be used to determine metadata of the file such as modification
114 *           time and size to avoid unnecessary copies.
115 * @return \c copy_isneeded iff a copy is required. \c copy_not_needed if a copy
116 *         is not needed. \c copy_needed_error on error, where errno will be
117 *         set.
118 */
119static copy_needed_return_t copy_needed(const char *path, char *const argv[],
120        char **outargv[], char *const environ[], struct stat *st) {
121#ifndef SF_RESTRICTED /* no system integrity protection */
122        return copy_not_needed;
123#else /* defined(SF_RESTRICTED) */
124        // check whether DYLD_INSERT_LIBRARIES is set
125        bool dyld_insert_libraries_present = false;
126        char *const *env = environ;
127        while (env && *env) {
128                if (strncmp("DYLD_INSERT_LIBRARIES=", *env, strlen("DYLD_INSERT_LIBRARIES=")) == 0) {
129                        dyld_insert_libraries_present = true;
130                        break;
131                }
132                env++;
133        }
134        // if we didn't find DYLD_INSERT_LIBRARIES, a copy isn't needed
135        if (!dyld_insert_libraries_present) {
136                return copy_not_needed;
137        }
138
139        // open file to check for shebangs
140        const char *realpath = path;
141        size_t new_argc = 0;
142        char **new_argv = NULL;
143        FILE *f = fopen(path, "r");
144        if (!f) {
145                // if opening fails we won't be able to copy anyway
146                return copy_not_needed;
147        }
148
149        /* no error checking for fgetc(3) here, because this isn't a shebang if an
150         * error occurs */
151        if (fgetc(f) == '#' && fgetc(f) == '!') {
152                /* This is an interpreted script. The interpreter's flags are what
153                 * affects whether DYLD_* is stripped, so read the interpreter's path
154                 * from the file to check that instead. Additionally, read any flags
155                 * that may be passed to the interpreter, since we'll have to do the
156                 * shebang expansion in user space if we move the interpreter. */
157                char *linep = NULL;
158                size_t linecapp = 0;
159                // read first line to get the interpreter and its arguments
160                if (getline(&linep, &linecapp, f) > 0) {
161                        char *ctxt;
162            char *word;
163            size_t idx;
164                        // do word splitting on the interpreter line and store it in new_argv
165                        for (idx = 0, word = strtok_r(linep, " \t\n", &ctxt);
166                                        word != NULL;
167                                        idx++, word = strtok_r(NULL, " \t\n", &ctxt)) {
168                                // make sure we have enough space allocated
169                                if (new_argv == NULL) {
170                                        if ((new_argv = malloc(2 * sizeof(*new_argv))) == NULL) {
171                        free(linep);
172                        return copy_needed_error;
173                                        }
174                                        new_argc = 1;
175
176                                        // new_argv[0] will be overwritten in a second
177                                        // new_argv[1] is the terminating NULL
178                                        new_argv[0] = NULL;
179                                        new_argv[1] = NULL;
180                                } else if (idx >= new_argc) {
181                                        // realloc to increase the size
182                                        char **oldargv = new_argv;
183                                        if ((new_argv = realloc(oldargv, (idx + 2) * sizeof(*new_argv))) == NULL) {
184                                                free_argv(oldargv);
185                        free(linep);
186                        return copy_needed_error;
187                                        }
188                                        new_argc = idx + 1;
189                                }
190
191                                // store a copy of the word in new_argv
192                                new_argv[idx] = strdup(word);
193                                if (!new_argv[idx]) {
194                                        free_argv(new_argv);
195                    free(linep);
196                    return copy_needed_error;
197                                }
198                new_argv[idx + 1] = NULL;
199                        }
200
201                        free(linep);
202
203                        if (new_argv && *new_argv) {
204                                // interpreter found, check that instead of given path
205                                realpath = *new_argv;
206                        }
207                }
208        }
209
210        // check whether the binary has SF_RESTRICTED and isn't SUID/SGID
211        if (-1 == stat(realpath, st)) {
212                // on error, return and let execve(2) deal with it
213                free_argv(new_argv);
214                return copy_not_needed;
215        } else {
216                if (!(st->st_flags & SF_RESTRICTED)) {
217                        // no SIP on this binary
218                        free_argv(new_argv);
219                        return copy_not_needed;
220                }
221                if ((st->st_flags & (S_ISUID | S_ISGID)) > 0) {
222                        // the binary is SUID/SGID, which would get lost when copying;
223                        // DYLD_ variables are stripped for SUID/SGID binaries anyway
224                        free_argv(new_argv);
225                        return copy_not_needed;
226                }
227        }
228
229        // prefix the shebang line to the original argv
230        if (new_argv != NULL) {
231        size_t argc = 0;
232        for (char *const *argvwalk = argv; argvwalk && *argvwalk; ++argvwalk) {
233            argc++;
234        }
235
236                // realloc to increase the size
237                char **oldargv = new_argv;
238                if ((new_argv = realloc(oldargv, (new_argc + argc + 1) * sizeof(*new_argv))) == NULL) {
239                        free_argv(oldargv);
240            return copy_needed_error;
241                }
242
243                new_argv[new_argc] = strdup(path);
244                if (!new_argv[new_argc]) {
245            free_argv(new_argv);
246            return copy_needed_error;
247                }
248        new_argv[new_argc + 1] = NULL;
249
250                for (size_t idx = 1; idx < argc; ++idx) {
251                        new_argv[new_argc + idx] = strdup(argv[idx]);
252                        if (!new_argv[new_argc + idx]) {
253                free_argv(new_argv);
254                return copy_needed_error;
255                        }
256            new_argv[new_argc + idx + 1] = NULL;
257                }
258
259                new_argc = new_argc + argc;
260
261                *outargv = new_argv;
262        }
263
264        return copy_is_needed;
265#endif /* defined(SF_RESTRICTED) */
266}
267
268static char *lazy_copy(const char *path, struct stat *in_st) {
269    char *retval = NULL;
270    uid_t euid = geteuid();
271    int outfd = -1;
272    int infd = -1;
273
274    char *target_folder = NULL;
275    char *target_path = NULL;
276    char *target_path_temp = NULL;
277    char *dir = strdup(path);
278    if (!dir) {
279        goto lazy_copy_out;
280    }
281    char *endslash = strrchr(dir, '/');
282    if (endslash) {
283        *endslash = '\0';
284    }
285
286    if (-1 == asprintf(&target_folder, "%s/%lu%s", DARWINTRACE_SIP_WORKAROUND_PATH, (unsigned long) euid, dir)) {
287        goto lazy_copy_out;
288    }
289
290    if (-1 == asprintf(&target_path, "%s/%lu%s", DARWINTRACE_SIP_WORKAROUND_PATH, (unsigned long) euid, path)) {
291        goto lazy_copy_out;
292    }
293
294    if (-1 == asprintf(&target_path_temp, "%s/%lu/.XXXXXXXXXXXXXX", DARWINTRACE_SIP_WORKAROUND_PATH, (unsigned long) euid)) {
295        goto lazy_copy_out;
296    }
297
298    // ensure directory exists
299    char *pos = target_folder + strlen(DARWINTRACE_SIP_WORKAROUND_PATH);
300    while (pos && *pos) {
301        *pos = '\0';
302        if (-1 == mkdir(target_folder, 0755) && errno != EEXIST) {
303            fprintf(stderr, "sip_copy_proc: mkdir(%s): %s\n", target_folder, strerror(errno));
304            goto lazy_copy_out;
305        }
306        *pos = '/';
307        pos++;
308        pos = strchr(pos, '/');
309    }
310    if (-1 == mkdir(target_folder, 0755) && errno != EEXIST) {
311        fprintf(stderr, "sip_copy_proc: mkdir(%s): %s\n", target_folder, strerror(errno));
312        goto lazy_copy_out;
313    }
314
315    // check whether copying is needed; it isn't if the file exists and the
316    // modification times match
317    struct stat out_st;
318    if (   -1 != stat(target_path, &out_st)
319        && in_st->st_mtimespec.tv_sec == out_st.st_mtimespec.tv_sec
320        && in_st->st_mtimespec.tv_nsec == out_st.st_mtimespec.tv_nsec) {
321        // copying not needed
322        retval = target_path;
323        goto lazy_copy_out;
324    }
325
326    // create temporary file to copy into and then later atomically replace
327    // target file
328    if (-1 == (outfd = mkstemp(target_path_temp))) {
329        fprintf(stderr, "sip_copy_proc: mkstemp(%s): %s\n", target_path_temp, strerror(errno));
330        goto lazy_copy_out;
331    }
332
333    if (-1 == (infd = open(path, O_RDONLY | O_CLOEXEC))) {
334        fprintf(stderr, "sip_copy_proc: open(%s, O_RDONLY | O_CLOEXEC): %s\n", path, strerror(errno));
335        goto lazy_copy_out;
336    }
337
338    // ensure mode is copied
339    if (-1 == fchmod(outfd, in_st->st_mode)) {
340        fprintf(stderr, "sip_copy_proc: fchmod(%s, %o): %s\n", target_path_temp, in_st->st_mode, strerror(errno));
341        goto lazy_copy_out;
342    }
343
344    char *buf = malloc(in_st->st_blksize);
345    ssize_t bytes_read = 0;
346    ssize_t bytes_written = 0;
347    bool error = false;
348    do {
349        bytes_read = read(infd, buf, in_st->st_blksize);
350        if (bytes_read < 0) {
351            if (errno == EINTR || errno == EAGAIN) {
352                continue;
353            } else {
354                error = true;
355                break;
356            }
357        }
358        if (bytes_read == 0) {
359            // EOF
360            break;
361        }
362
363        bytes_written = 0;
364        while (bytes_written < bytes_read) {
365            ssize_t written = write(outfd, buf + bytes_written, bytes_read - bytes_written);
366            if (written < 0) {
367                if (errno == EINTR || errno == EAGAIN) {
368                    continue;
369                }
370                error = true;
371                break;
372            }
373
374            bytes_written += written;
375        }
376    } while (!error);
377    if (bytes_read < 0 || bytes_written < 0) {
378        goto lazy_copy_out;
379    }
380
381    struct timeval times[2];
382    TIMESPEC_TO_TIMEVAL(&times[0], &in_st->st_mtimespec);
383    TIMESPEC_TO_TIMEVAL(&times[1], &in_st->st_mtimespec);
384    if (-1 == futimes(outfd, times)) {
385        fprintf(stderr, "sip_copy_proc: futimes(%s): %s\n", target_path_temp, strerror(errno));
386        goto lazy_copy_out;
387    }
388
389    if (-1 == rename(target_path_temp, target_path)) {
390        fprintf(stderr, "sip_copy_proc: rename(%s, %s): %s\n", target_path_temp, target_path, strerror(errno));
391        goto lazy_copy_out;
392    }
393
394    retval = target_path;
395
396lazy_copy_out:
397    {
398        int errno_save = errno;
399        close(outfd);
400        close(infd);
401        if (target_path_temp != NULL && -1 == unlink(target_path_temp) && errno != ENOENT) {
402            fprintf(stderr, "sip_copy_proc: unlink(%s): %s\n", target_path_temp, strerror(errno));
403            retval = NULL;
404        } else {
405            errno = errno_save;
406        }
407    }
408    free(dir);
409    free(target_path_temp);
410    free(target_folder);
411    if (retval != target_path) {
412        free(target_path);
413    }
414    return retval;
415}
416
417/**
418 * Behaves like execve(2), but checks whether trace mode is enabled (by
419 * checking for DYLD_INSERT_LIBRARIES in the environment) and the binary is
420 * covered by 10.11's new system integrity protection. If it is, the binary
421 * will be copied to a separate folder (or updated if already there and
422 * modification time differs) and executed from there.
423 */
424int sip_copy_execve(const char *path, char *const argv[], char *const envp[]) {
425    char **outargv = NULL;
426    struct stat st;
427
428    copy_needed_return_t need_copy = copy_needed(path, argv, &outargv, envp, &st);
429    switch (need_copy) {
430        case copy_needed_error:
431            return -1;
432            break;
433        case copy_not_needed:
434            return execve(path, argv, envp);
435            break;
436        case copy_is_needed: {
437                const char *to_be_copied = path;
438                char *const *to_be_argv = argv;
439                if (outargv) {
440                    to_be_copied = outargv[0];
441                    to_be_argv = outargv;
442                }
443
444                char *new_path = lazy_copy(to_be_copied, &st);
445                if (!new_path) {
446                    return -1;
447                }
448
449                int ret = execve(new_path, to_be_argv, envp);
450                free_argv(outargv);
451                free(new_path);
452                return ret;
453            }
454            break;
455    }
456}
457
458/**
459 * Behaves like posix_spawn(2), but checks whether trace mode is enabled (by
460 * checking for DYLD_INSERT_LIBRARIES in the environment) and the binary is
461 * covered by 10.11's new system integrity protection. If it is, the binary
462 * will be copied to a separate folder (or updated if already there and
463 * modification time differs) and executed from there.
464 */
465int sip_copy_posix_spawn(
466        pid_t *restrict pid,
467        const char *restrict path,
468        const posix_spawn_file_actions_t *file_actions,
469        const posix_spawnattr_t *restrict attrp,
470        char *const argv[restrict],
471        char *const envp[restrict]) {
472    char **outargv = NULL;
473    struct stat st;
474
475    copy_needed_return_t need_copy = copy_needed(path, argv, &outargv, envp, &st);
476    switch (need_copy) {
477        case copy_needed_error:
478            return -1;
479            break;
480        case copy_not_needed:
481            return posix_spawn(pid, path, file_actions, attrp, argv, envp);
482            break;
483        case copy_is_needed: {
484                const char *to_be_copied = path;
485                char *const *to_be_argv = argv;
486                if (outargv) {
487                    to_be_copied = outargv[0];
488                    to_be_argv = outargv;
489                }
490
491                char *new_path = lazy_copy(to_be_copied, &st);
492                if (!new_path) {
493                    return -1;
494                }
495
496                int ret = posix_spawn(pid, new_path, file_actions, attrp, to_be_argv, envp);
497                free_argv(outargv);
498                free(new_path);
499                return ret;
500            }
501            break;
502    }
503}
Note: See TracBrowser for help on using the repository browser.