为什么在设备 compiler/toolchain 上使用 iOS 时,各种项目的构建会因“不允许操作”而失败?

Why do builds for various projects fail with ‘Operation not permitted’ using iOS on-device compiler/toolchain?

我是一名中等水平的 Linux/Unix 用户,试图在(越狱)iPad 上为 iPad 编译软件。

许多构建(例如,make 和 tex-live)因一些 Operation not permitted 错误而失败。这看起来像 Can't exec "blah": Operation not permittedexecvp: blah: Operation not permitted,其中 blahaclocalconfigure 脚本、libtool 或任何东西。奇怪的是,在 Makefileconfigure 脚本中找到有问题的行并在其前面加上 sudo -u mobile -E 将解决该行的错误,只是为了它会在后面的行或另一行中重新出现文件。由于我 运行 构建脚本为 mobile,我不明白这怎么可能解决问题,但确实如此。我已经确认进行这些更改实际上允许脚本成功运行到那时。 运行 带有 sudosudo -u mobile -E and/or 运行 整个构建的构建脚本 root 没有解决问题;无论是哪种,我仍然必须编辑构建脚本以添加 sudo

我想知道为什么会这样,如果可能的话,我想知道如何在不编辑构建脚本的情况下解决这个问题。关于这些类型的错误的任何信息对我来说都是有趣的,即使它们不能解决我的问题。我知道 permissions/security/entitlements 系统在 iOS 上很不寻常,我想了解更多关于它是如何工作的。

我在越狱的 iOS 13.5 上使用 iPad Pro 4,使用来自 sbingner 和 MCApollo 的 repos 的构建工具(repo.bingner.com 和 mcapollo.github。io/Public).特别是,我使用的是 LLVM 5(从 sbingner 的旧 deb 手动安装)、Clang 10、Darwin CC 工具 927 和 GNU Make 4.2.1 的构建。我已将 CCCXXCFLAGS 等设置为指向 clang-10 和我的 iOS 13.5 SDK 和 -isysroot 并已确认这些设置正在工作。我想用更新的版本替换它们,但由于这个问题和其他一些问题,我还不能为自己构建这些工具。如有必要,我确实可以访问 Mac 进行交叉编译,但我宁愿只使用我的 iPad,因为我喜欢挑战。

我可以附上任何必要的日志或提供更多有用的信息;我对这个问题了解不够,不知道哪些信息有用。在此先感谢您对我的帮助!

对于最终需要在没有解决此问题的越狱时解决此问题的任何人,我已经编写(粘贴在下面)一个基于来自源的 posix_spawn 实现的用户态挂钩Apple 的 xnu 内核。

使用 Theos 编译它,并通过将环境变量 DYLD_INSERT_LIBRARIES 设置为生成的 dylib 的路径,将其注入到您的 shell 生成的所有进程中。注意:一些调整注入器(即 libhooker,参见 here)重置 DYLD_INSERT_LIBRARIES,因此如果您注意到此行为,请务必仅注入您的库。

因为 iOS 调用 posix_spawn 中的 exec 系统调用的实现,此挂钩修复了我 运行 迄今为止遇到的所有与 exec 相关的问题。

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <spawn.h>

// Copied from bsd/kern/kern_exec.c
#define IS_WHITESPACE(ch) ((ch == ' ') || (ch == '\t'))
#define IS_EOL(ch) ((ch == '#') || (ch == '\n'))
// Copied from bsd/sys/imgact.h
#define IMG_SHSIZE 512

// Here, we provide an alternate implementation of posix_spawn which correctly handles #!.
// This is based on the implementation of posix_spawn in bsd/kern/kern_exec.c from Apple's xnu source.
// Thus, I am fairly confident that this posix_spawn has correct behavior relative to macOS.
%hookf(int, posix_spawn, pid_t *pid, const char *orig_path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const orig_argv[], char *const envp[]) {
    
    // Call orig before checking for anything.
    // This mirrors the standard implementation of posix_spawn because it first checks if we are spawning a binary.
    int err = %orig;
    
    // %orig returns EPERM when spawning a script.
    // Thus, if err is anything other than EPERM, we can just return like normal.
    if (err != EPERM) 
        return err;
    
    // At this point, we do not need to check for exec permissions or anything like that.
    //  because posix_spawn would have returned that error instead of EPERM.

    // Now we open the file for reading so that we can check if it's a script.
    // If it turns out not to be a script, the EPERM must be from something else
    //  so we just return err.

    FILE *file = fopen(orig_path, "r");
    if (file == NULL) {
        return err;
    }
    if (fseek(file, 0, SEEK_SET)) {
        return err;
    }   

    // In exec_activate_image, the data buffer is filled with the first PAGE_SIZE bytes of the file.
    // However, in exec_shell_imgact, only the first IMG_SHSIZE bytes are used.
    // Thus, we read IMG_SHSIZE bytes out of our file.
    // The buffer is filled with newlines so that if the file is not IMG_SHSIZE bytes,
    //  the logic reads an IS_EOL.
    char vdata[IMG_SHSIZE] = {'\n'};
    if (fread(vdata, 1, IMG_SHSIZE, file) < 2) { // If we couldn't read at least two bytes, it's not a script.
        fclose(file);
        return err;
    }

    // Now that we've filled the buffer, we don't need the file anymore.
    fclose(file);
    
    // Now we follow exec_shell_imgact.
    // The point of this is to confirm we have a script 
    //  and extract the usable part of the interpreter+arg string.
    // Where they return -1, we don't have a shell script, so we return err.
    // Where they return an error, we return that same error.
    // We don't bother doing any SUID stuff because SUID scripts should be disabled anyway.
        char *ihp;
        char *line_startp, *line_endp;
        
    // Make sure we have a shell script.
        if (vdata[0] != '#' || vdata[1] != '!') {
        return err;
    }
        
        // Try to find the first non-whitespace character
        for (ihp = &vdata[2]; ihp < &vdata[IMG_SHSIZE]; ihp++) {
                if (IS_EOL(*ihp)) {
                        // Did not find interpreter, "#!\n"
                        return ENOEXEC;
                } else if (IS_WHITESPACE(*ihp)) {
                        // Whitespace, like "#!    /bin/sh\n", keep going.
                } else {
                        // Found start of interpreter
                        break;
                }
        }
        
        if (ihp == &vdata[IMG_SHSIZE]) {
                // All whitespace, like "#!           "
                return ENOEXEC;
        }

        line_startp = ihp;
        
        // Try to find the end of the interpreter+args string
        for (; ihp < &vdata[IMG_SHSIZE]; ihp++) {
                if (IS_EOL(*ihp)) {
                        // Got it
                        break;
                } else {
                        // Still part of interpreter or args
                }
        }
        
        if (ihp == &vdata[IMG_SHSIZE]) {
                // A long line, like "#! blah blah blah" without end
                return ENOEXEC;
        }
        
        // Backtrack until we find the last non-whitespace
        while (IS_EOL(*ihp) || IS_WHITESPACE(*ihp)) {
                ihp--;
        }
        
        // The character after the last non-whitespace is our logical end of line
        line_endp = ihp + 1;
        
        /*
         * Now we have pointers to the usable part of:
         *
         * "#!  /usr/bin/int first    second   third    \n"
         *      ^ line_startp                       ^ line_endp
         */

    // Now, exec_shell_imgact copies the interpreter into another buffer and then null-terminates it.
    // Then, it copies the entire interpreter+args into another buffer and null-terminates it for later processing into argv.
    // This processing is done in exec_extract_strings, which goes through and null-terminates each argument.
    // We will just do this all at once since that's much easier.
    
    // Keep track of how many arguments we have.
    int i_argc = 0;

    ihp = line_startp;
    while (true) {
        // ihp is on the start of an argument.
        i_argc++;
        // Scan to the end of the argument.
        for (; ihp < line_endp; ihp++) {
            if (IS_WHITESPACE(*ihp)) {
                // Found the end of the argument
                break;
            } else {
                // Keep going
            } 
        }
        // Null terminate the argument
        *ihp = '[=10=]';
        // Scan to the beginning of the next argument.
        for (; ihp < line_endp; ihp++) {
            if (!IS_WHITESPACE(*ihp)) {
                // Found the next argument
                break;
            } else {
                // Keep going
            }
        }
        if (ihp == line_endp) {
            // We've reached the end of the arg string
            break;
        }
        // If we are here, ihp is the start of an argument.
    }
    // Now line_startp is a bunch of null-terminated arguments possibly padded by whitespace.
    // i_argc is now the count of the interpreter arguments.
       
    // Our new argv should look like i_argv[0], i_argv[1], i_argv[2], ..., orig_path, orig_argv[1], orig_argv[2], ..., NULL
    //  where i_argv is the arguments to be extracted from line_startp;
    // To allocate our new argv, we need to know orig_argc.
    int orig_argc = 0;
    while (orig_argv[orig_argc] != NULL) {
        orig_argc++;
    }
    
    // We need space for i_argc + 1 + (orig_argc - 1) + 1 char*'s
    char *argv[i_argc + orig_argc + 1];
    
    // Copy i_argv into argv
    int i = 0;
    ihp = line_startp;
    for (; i < i_argc; i++) {
        // ihp is on the start of an argument
        argv[i] = ihp;
        // Scan to the next null-terminator
        for (; ihp < line_endp; ihp++) {
            if (*ihp == '[=10=]') {
                // Found it
                break;
            } else {
                // Keep going
            }
        }
        // Go to the next character
        ihp++;
        // Then scan to the next argument. 
        // There must be another argument because we already counted i_argc.
        for (; ihp < line_endp; ihp++) {
            if (!IS_WHITESPACE(*ihp)) {
                // Found it
                break;
            } else {
                // Keep going
            }
        }
        // ihp is on the start of an argument.
    }

    // Then, copy orig_path into into argv.
    // We need to make a copy of orig_path to avoid issues with const.
    char orig_path_copy[strlen(orig_path)+1];
    strcpy(orig_path_copy, orig_path);  
    argv[i] = orig_path_copy;
    i++;

    // Now, copy orig_argv[1...] into argv.
    for (int j = 1; j < orig_argc; i++, j++) {
        argv[i] = orig_argv[j];
    }
    // Finally, add the null.
    argv[i] = NULL;
    // Now, our argv is setup correctly. 

    // Now, we can call out to posix_spawn again.
    // The interpeter is in argv[0], so we use that for the path.
    return %orig(pid, argv[0], file_actions, attrp, argv, envp);
}