├── .gitignore ├── copy_to_theos.sh ├── LICENSE.md ├── Makefile ├── .github └── workflows │ └── build.yml ├── src ├── libroot.h └── dyn.c └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build 3 | libroot_dyn_*.a -------------------------------------------------------------------------------- /copy_to_theos.sh: -------------------------------------------------------------------------------- 1 | cp libroot_dyn_iphoneos-arm.a $THEOS/vendor/lib/libroot.a 2 | cp libroot_dyn_iphoneos-arm.oldabi.a $THEOS/vendor/lib/libroot_oldabi.a 3 | cp libroot_dyn_iphoneos-arm64.a $THEOS/vendor/lib/iphone/rootless/libroot.a 4 | cp libroot_dyn_iphoneos-arm64.oldabi.a $THEOS/vendor/lib/iphone/rootless/libroot_oldabi.a -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Lars Fröder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TOOLCHAIN_PREFIX ?= 2 | CC = $(TOOLCHAIN_PREFIX)clang 3 | LIBTOOL = $(TOOLCHAIN_PREFIX)libtool 4 | 5 | SRC_FILES := $(wildcard src/*.c) 6 | 7 | IPHONEOS_ARM_BUILD_DIR := build/iphoneos_arm 8 | IPHONEOS_ARM_OBJ_FILES := $(patsubst src/%.c,$(IPHONEOS_ARM_BUILD_DIR)/%.o,$(SRC_FILES)) 9 | 10 | IPHONEOS_ARM64_BUILD_DIR := build/iphoneos_arm64 11 | IPHONEOS_ARM64_OBJ_FILES := $(patsubst src/%.c,$(IPHONEOS_ARM64_BUILD_DIR)/%.o,$(SRC_FILES)) 12 | 13 | CFLAGS = -I../../BaseBin/.include -Isrc -isysroot $(shell xcrun --sdk iphoneos --show-sdk-path) -arch armv6 -arch armv7 -arch armv7s -arch arm64 -arch arm64e -miphoneos-version-min=7.0 -fobjc-arc -O2 14 | LDFLAGS = 15 | 16 | all: libroot_dyn_iphoneos-arm.a libroot_dyn_iphoneos-arm64.a 17 | 18 | libroot_dyn_iphoneos-arm.a: $(IPHONEOS_ARM_OBJ_FILES) 19 | @$(LIBTOOL) $^ -o $@ 20 | 21 | libroot_dyn_iphoneos-arm64.a: $(IPHONEOS_ARM64_OBJ_FILES) 22 | @$(LIBTOOL) $^ -o $@ 23 | 24 | $(IPHONEOS_ARM_BUILD_DIR)/%.o: src/%.c 25 | @mkdir -p $(dir $@) 26 | $(CC) $(CFLAGS) $(LDFLAGS) -c $< -o $@ 27 | 28 | $(IPHONEOS_ARM64_BUILD_DIR)/%.o: src/%.c 29 | @mkdir -p $(dir $@) 30 | $(CC) $(CFLAGS) -DIPHONEOS_ARM64 $(LDFLAGS) -c $< -o $@ 31 | 32 | clean: 33 | @rm -rf build 34 | 35 | clean-artifacts: clean 36 | @rm -f libroot_dyn_*.a -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Publish artifacts 2 | 3 | on: 4 | - push 5 | - workflow_dispatch 6 | 7 | jobs: 8 | build-v0: 9 | name: Build for arm64e unversioned ABI 10 | permissions: 11 | id-token: write 12 | contents: read 13 | attestations: write 14 | runs-on: macos-11 15 | env: 16 | DEVELOPER_DIR: /Applications/Xcode_11.7.app 17 | steps: 18 | - name: Checkout repo 19 | uses: actions/checkout@v4 20 | 21 | - name: Build 22 | run: | 23 | make 24 | # adjust names 25 | mv libroot_dyn_iphoneos-arm.a libroot_oldabi.a 26 | mv libroot_dyn_iphoneos-arm64.a rootless-libroot_oldabi.a 27 | # archive so we can keep permissions 28 | tar cvf libroot_oldabi.tar libroot_oldabi.a rootless-libroot_oldabi.a 29 | 30 | - name: Attest default libroot 31 | uses: actions/attest-build-provenance@v1 32 | with: 33 | subject-path: libroot_oldabi.a 34 | 35 | - name: Attest rootless libroot 36 | uses: actions/attest-build-provenance@v1 37 | with: 38 | subject-path: rootless-libroot_oldabi.a 39 | 40 | - name: Upload artifact 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: libroot_oldabi 44 | path: libroot_oldabi.tar 45 | if-no-files-found: error 46 | 47 | build-v1: 48 | name: Build for arm64e v1 ABI 49 | permissions: 50 | id-token: write 51 | contents: read 52 | attestations: write 53 | runs-on: macos-14 54 | env: 55 | DEVELOPER_DIR: /Applications/Xcode_15.1.app 56 | steps: 57 | - name: Checkout repo 58 | uses: actions/checkout@v4 59 | 60 | - name: Build 61 | run: | 62 | make 63 | # adjust names 64 | mv libroot_dyn_iphoneos-arm.a libroot.a 65 | mv libroot_dyn_iphoneos-arm64.a rootless-libroot.a 66 | # archive so we can keep permissions 67 | tar cvf libroot.tar libroot.a rootless-libroot.a 68 | 69 | - name: Attest default libroot 70 | uses: actions/attest-build-provenance@v1 71 | with: 72 | subject-path: libroot.a 73 | 74 | - name: Attest rootless libroot 75 | uses: actions/attest-build-provenance@v1 76 | with: 77 | subject-path: rootless-libroot.a 78 | 79 | - name: Upload artifact 80 | uses: actions/upload-artifact@v4 81 | with: 82 | name: libroot 83 | path: libroot.tar 84 | if-no-files-found: error 85 | -------------------------------------------------------------------------------- /src/libroot.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | __BEGIN_DECLS 4 | 5 | _Pragma("GCC visibility push(hidden)") 6 | 7 | const char *_Nonnull libroot_dyn_get_root_prefix(void); 8 | const char *_Nonnull libroot_dyn_get_jbroot_prefix(void); 9 | const char *_Nonnull libroot_dyn_get_boot_uuid(void); 10 | char *_Nullable libroot_dyn_rootfspath(const char *_Nullable path, char *_Nullable resolvedPath); 11 | char *_Nullable libroot_dyn_jbrootpath(const char *_Nullable path, char *_Nullable resolvedPath); 12 | 13 | _Pragma("GCC visibility pop") 14 | 15 | __END_DECLS 16 | 17 | #define __CONVERT_PATH_CSTRING(converter, path) ({ \ 18 | static char outPath[PATH_MAX]; \ 19 | converter(path, outPath); \ 20 | }) 21 | 22 | #define JBROOT_PATH_CSTRING(path) __CONVERT_PATH_CSTRING(libroot_dyn_jbrootpath, path) 23 | #define ROOTFS_PATH_CSTRING(path) __CONVERT_PATH_CSTRING(libroot_dyn_rootfspath, path) 24 | 25 | #if __has_attribute(overloadable) 26 | __attribute__((__overloadable__)) 27 | static inline const char *_Nullable __libroot_convert_path(char *_Nullable (*_Nonnull converter)(const char *_Nonnull, char *_Nullable), const char *_Nullable path, char *_Nonnull buf) { 28 | return converter(path, buf); 29 | } 30 | #endif /* __has_attribute(overloadable) */ 31 | 32 | #ifdef __OBJC__ 33 | 34 | #import 35 | 36 | #define __CONVERT_PATH_NSSTRING(converter, path) ({ \ 37 | char tmpBuf[PATH_MAX]; \ 38 | const char *converted = converter(path.fileSystemRepresentation, tmpBuf); \ 39 | converted ? [NSString stringWithUTF8String:converted] : nil; \ 40 | }) 41 | 42 | #define JBROOT_PATH_NSSTRING(path) __CONVERT_PATH_NSSTRING(libroot_dyn_jbrootpath, path) 43 | #define ROOTFS_PATH_NSSTRING(path) __CONVERT_PATH_NSSTRING(libroot_dyn_rootfspath, path) 44 | 45 | #if __has_attribute(overloadable) 46 | __attribute__((__overloadable__)) 47 | static inline NSString *_Nullable __libroot_convert_path(char *_Nullable (*_Nonnull converter)(const char *_Nonnull, char *_Nullable), NSString *_Nullable path, void *_Nullable const __unused buf) { 48 | return __CONVERT_PATH_NSSTRING(converter, path); 49 | } 50 | #endif /* __has_attribute(overloadable) */ 51 | 52 | #endif /* __OBJC__ */ 53 | 54 | #if __has_attribute(overloadable) 55 | 56 | #define __BUFFER_FOR_CHAR_P(x) \ 57 | __builtin_choose_expr( \ 58 | __builtin_types_compatible_p(__typeof__(*(x)), char), \ 59 | ({ static char buf[PATH_MAX]; buf; }), \ 60 | NULL \ 61 | ) 62 | 63 | # define JBROOT_PATH(path) __libroot_convert_path(libroot_dyn_jbrootpath, (path), __BUFFER_FOR_CHAR_P(path)) 64 | # define ROOTFS_PATH(path) __libroot_convert_path(libroot_dyn_rootfspath, (path), __BUFFER_FOR_CHAR_P(path)) 65 | #else 66 | # define JBROOT_PATH(path) _Pragma("GCC error \"JBROOT_PATH is not supported with this compiler, use JBROOT_PATH_CSTRING or JBROOT_PATH_NSSTRING\"") path 67 | # define ROOTFS_PATH(path) _Pragma("GCC error \"ROOTFS_PATH is not supported with this compiler, use ROOTFS_PATH_CSTRING or ROOTFS_PATH_NSSTRING\"") path 68 | #endif /* __has_attribute(overloadable) */ 69 | 70 | #define JBRAND libroot_dyn_get_boot_uuid() 71 | -------------------------------------------------------------------------------- /src/dyn.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "libroot.h" 9 | 10 | static const char *(*dyn_get_root_prefix)(void) = NULL; 11 | static const char *(*dyn_get_jbroot_prefix)(void) = NULL; 12 | static const char *(*dyn_get_boot_uuid)(void) = NULL; 13 | static char *(*dyn_jbrootpath)(const char *path, char *resolvedPath) = NULL; 14 | static char *(*dyn_rootfspath)(const char *path, char *resolvedPath) = NULL; 15 | 16 | #ifdef IPHONEOS_ARM64 17 | 18 | static const char *libroot_get_root_prefix_fallback(void) 19 | { 20 | return ""; 21 | } 22 | 23 | static const char *libroot_get_jbroot_prefix_fallback(void) 24 | { 25 | return "/var/jb"; 26 | } 27 | 28 | #else 29 | 30 | static const char *libroot_get_root_prefix_fallback(void) 31 | { 32 | return ""; 33 | } 34 | 35 | static const char *libroot_get_jbroot_prefix_fallback(void) 36 | { 37 | if (access("/var/LIY", F_OK) == 0) { 38 | // Legacy support for XinaA15 1.x (For those two people still using it) 39 | // Technically this should be deprecated, but with the libroot solution it's not the hardest thing in the world to maintain 40 | // So I decided to leave it in 41 | return "/var/jb"; 42 | } 43 | else { 44 | return ""; 45 | } 46 | } 47 | 48 | #endif 49 | 50 | static const char *libroot_get_boot_uuid_fallback(void) 51 | { 52 | return "00000000-0000-0000-0000-000000000000"; 53 | } 54 | 55 | static char *libroot_rootfspath_fallback(const char *path, char *resolvedPath) 56 | { 57 | if (!path) return NULL; 58 | if (!resolvedPath) resolvedPath = malloc(PATH_MAX); 59 | 60 | const char *prefix = libroot_dyn_get_root_prefix(); 61 | const char *jbRootPrefix = libroot_dyn_get_jbroot_prefix(); 62 | size_t jbRootPrefixLen = strlen(jbRootPrefix); 63 | 64 | if (path[0] == '/') { 65 | // This function has two different purposes 66 | // If what we have is a subpath of the jailbreak root, strip the jailbreak root prefix 67 | // Else, add the rootfs prefix 68 | if (!strncmp(path, jbRootPrefix, jbRootPrefixLen)) { 69 | strlcpy(resolvedPath, &path[jbRootPrefixLen], PATH_MAX); 70 | } 71 | else { 72 | strlcpy(resolvedPath, prefix, PATH_MAX); 73 | strlcat(resolvedPath, path, PATH_MAX); 74 | } 75 | } 76 | else { 77 | // Don't modify relative paths 78 | strlcpy(resolvedPath, path, PATH_MAX); 79 | } 80 | 81 | return resolvedPath; 82 | } 83 | 84 | static char *libroot_jbrootpath_fallback(const char *path, char *resolvedPath) 85 | { 86 | if (!path) return NULL; 87 | if (!resolvedPath) resolvedPath = malloc(PATH_MAX); 88 | 89 | const char *prefix = libroot_dyn_get_jbroot_prefix(); 90 | bool skipRedirection = path[0] != '/'; // Don't redirect relative paths 91 | 92 | #ifndef IPHONEOS_ARM64 93 | // Special case 94 | // On XinaA15 v1: Don't redirect /var/mobile paths to /var/jb/var/mobile 95 | if (!skipRedirection) { 96 | if (access("/var/LIY", F_OK) == 0) { 97 | skipRedirection = strncmp(path, "/var/mobile", 11) == 0; 98 | } 99 | } 100 | #endif 101 | 102 | if (!skipRedirection) { 103 | strlcpy(resolvedPath, prefix, PATH_MAX); 104 | strlcat(resolvedPath, path, PATH_MAX); 105 | } 106 | else { 107 | strlcpy(resolvedPath, path, PATH_MAX); 108 | } 109 | 110 | return resolvedPath; 111 | } 112 | 113 | static void libroot_load(void) 114 | { 115 | static dispatch_once_t onceToken; 116 | dispatch_once (&onceToken, ^{ 117 | void *handle = dlopen("@rpath/libroot.dylib", RTLD_NOW); 118 | if (handle) { 119 | dyn_get_root_prefix = dlsym(handle, "libroot_get_root_prefix"); 120 | dyn_get_jbroot_prefix = dlsym(handle, "libroot_get_jbroot_prefix"); 121 | dyn_get_boot_uuid = dlsym(handle, "libroot_get_boot_uuid"); 122 | dyn_jbrootpath = dlsym(handle, "libroot_jbrootpath"); 123 | dyn_rootfspath = dlsym(handle, "libroot_rootfspath"); 124 | } 125 | if (!dyn_get_root_prefix) dyn_get_root_prefix = libroot_get_root_prefix_fallback; 126 | if (!dyn_get_jbroot_prefix) dyn_get_jbroot_prefix = libroot_get_jbroot_prefix_fallback; 127 | if (!dyn_get_boot_uuid) dyn_get_boot_uuid = libroot_get_boot_uuid_fallback; 128 | if (!dyn_jbrootpath) dyn_jbrootpath = libroot_jbrootpath_fallback; 129 | if (!dyn_rootfspath) dyn_rootfspath = libroot_rootfspath_fallback; 130 | }); 131 | } 132 | 133 | const char *libroot_dyn_get_root_prefix(void) 134 | { 135 | libroot_load(); 136 | return dyn_get_root_prefix(); 137 | } 138 | 139 | const char *libroot_dyn_get_jbroot_prefix(void) 140 | { 141 | libroot_load(); 142 | return dyn_get_jbroot_prefix(); 143 | } 144 | 145 | const char *libroot_dyn_get_boot_uuid(void) 146 | { 147 | libroot_load(); 148 | return dyn_get_boot_uuid(); 149 | } 150 | 151 | char *libroot_dyn_rootfspath(const char *path, char *resolvedPath) 152 | { 153 | libroot_load(); 154 | return dyn_rootfspath(path, resolvedPath); 155 | } 156 | 157 | char *libroot_dyn_jbrootpath(const char *path, char *resolvedPath) 158 | { 159 | libroot_load(); 160 | return dyn_jbrootpath(path, resolvedPath); 161 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libroot 2 | 3 | Library for dynamically obtaining the jailbreak-root and rootfs paths and converting paths around. 4 | Part of "Rootless v2", an API that allows third party packages to run on any jailbreak, no matter where the jailbreaks bootstrap is installed to. 5 | Designed to be backwards compatible with all jailbreaks, even those that have not adopted the API. 6 | Intended to be flexible enough to support undetectable jailbreaks with randomized paths, while providing a compatibility layer on other jailbreaks to avoid community fragmentation of third party packages (Like what was seen with "`iphoneos-arm64e`"). 7 | 8 | ## Adapting libroot in a jailbreak 9 | 10 | ### Providing paths 11 | 12 | Jailbreaks supporting rootless V2 are supposed to ship a package in their jailbreak that contains the `/usr/lib/libroot.dylib` file. 13 | This library can implement the following functions, which will be used by third party packages that access files. 14 | If this library does not exist, `libroot_dyn.a` will use a fallback implementation that will make callers use the following paths: 15 | | | iphoneos-arm64 | XinaA15 1.x | iphoneos-arm | 16 | | ------------- | -------------- | -------------- | -------------- | 17 | | rootfs | `/` | `/` | `/` | 18 | | jbroot | `/var/jb` | `/var/jb` | `/` | 19 | 20 | ```c 21 | const char *libroot_get_root_prefix(void); 22 | ``` 23 | This function shall return a static buffer containing the prefix that the bootstrap of the jailbreak considers the rootfs, which is read-only and only contains the files shipped by the operating system. For most jailbreaks this will be `""`, the only exception is when a jailbreak uses something like scheme4 to make the bootstrap think it's in a different path. For example, roothide uses `"/rootfs"` for this, as it's bootstrap considers that to correspond to the rootfs, so that `"/jbroot"` can correspond to the jailbreak root. 24 | 25 | 26 | ```c 27 | const char *libroot_get_jbroot_prefix(void); 28 | ``` 29 | This function shall return a static buffer containing the prefix of the path where the bootstrap of the jailbreak is installed to, this path is read-write and contains all jailbreak related files. For most jailbreaks this will be `"/var/jb"` or what that symlink points to, but if the jailbreak (like roothide) wants to be undetectable, this needs to be a randomized path. 30 | 31 | 32 | ```c 33 | const char *libroot_get_boot_uuid(void); 34 | ``` 35 | This function shall return a static buffer containing a per boot UUID that identifies the current userspace boot. This UUID is expected to be 37 characters long, including the last byte being NULL. This merely exists for allowing third party packages to randomize certain tokens to avoid jailbreak detection. If your jailbreak is not supposed to be undetectable, you can return `"00000000-0000-0000-0000-000000000000"` here or avoid implementing it alltogether. A more detailed explanation can be found in the "Using libroot in a project" section. 36 | 37 | 38 | ### Custom path conversion logic 39 | 40 | The `libroot_dyn` static library already provides logic for converting a relative path to a rootfs or jbroot path. If your jailbreak wants to provide custom conversion logic though, it can do so by implementing the following two functions. 41 | 42 | ```c 43 | char *libroot_rootfspath(const char *path, char *resolvedPath); 44 | ``` 45 | This function is supposed to provide the logic for converting a path to be a rootfs path. Sample input: `"/sbin/launchd"`, sample output: `"/sbin/launchd"` (sample output, roothide: `/rootfs/sbin/launchd`). 46 | Additionally it is supposed to provide logic for stripping the jbroot prefix from a jbroot path, if the path passed to it is inside that. Sample input: `/var/jb/Library/Application Support/CCSupport.bundle`, sample output: `/Library/Application Support/CCSupport.bundle`. If `resolvedPath` is not NULL, it should be treated as a buffer of `PATH_MAX` characters and returned upon success, if it is NULL, the function is supposed to allocate a buffer of `PATH_MAX` bytes by itself. Unless you need custom logic, it is recommended to leave this function unimplemented. 47 | 48 | 49 | ```c 50 | char *libroot_jbrootpath(const char *path, char *resolvedPath); 51 | ``` 52 | This function is supposed to provide the logic for converting a path to be a jbroot path. Sample input: `"/Library/Application Support/CCSupport.bundle"`, sample output: `"/var/jb/Library/Application Support/CCSupport.bundle"`. If `resolvedPath` is not NULL, it should be treated as a buffer of `PATH_MAX` characters and returned upon success, if it is NULL, the function is supposed to allocate a buffer of `PATH_MAX` bytes by itself. Unless you need custom logic, it is recommended to leave this function unimplemented. 53 | 54 | ## Using libroot in a project 55 | 56 | First of all, link the corresponding .a file `libroot_dyn_iphoneos-arm64.a` or `libroot_dyn_iphoneos-arm.a` to your project (if you're using a recent theos version, this will be done automatically) and include the libroot.h header. 57 | This header contains 3 macros to make it easy to interact with libroot: 58 | 59 | 60 | ```c 61 | JBROOT_PATH_CSTRING(path) 62 | JBROOT_PATH_NSSTRING(path) 63 | ``` 64 | These macros can be used to convert a path to the jailbreak root. 65 | 66 | 67 | ```c 68 | ROOTFS_PATH_CSTRING(path) 69 | ROOTFS_PATH_NSSTRING(path) 70 | ``` 71 | These macros can be used to convert a path to what the bootstrap considers the rootfs (NOTE: This is only rarely neccessary if you need to interact with bootstrap binaries, for example if you want to make the bootstrap's `cp` binary copy a file from the rootfs to the jbroot, you would do so as follows: `JBROOT_PATH("/bin/cp") ROOTFS_PATH("/sbin/launchd") JBROOT_PATH("/sbin/launchd")`). If you want to access a file on the rootfs from the context of your own process, you do NOT want to use this macro. This macro can also be used to convert a jbroot path to a normal path (As explained under `libroot_jbrootpath`). 72 | 73 | ```c 74 | JBRAND 75 | ``` 76 | This macro is used to get a per userspace boot 37-char long UUID in the following format: `"00000000-0000-0000-0000-000000000000"`, the main use case for this to avoid jailbreak detection of tweaks. One example would be when working with distributed notifications, apps could listen for these to know when you're jailbroken, so you can prefix them with `JBRAND` on both the sender and the receivers end to avoid this, as apps cannot predict this UUID. 77 | 78 | Additionally it is also possible to call 79 | ```c 80 | char *libroot_dyn_rootfspath(const char *path, char *resolvedPath); 81 | char *libroot_dyn_jbrootpath(const char *path, char *resolvedPath); 82 | ``` 83 | directly for more control over the arguments, the function signature is similar to [realpath](https://man7.org/linux/man-pages/man3/realpath.3.html). 84 | --------------------------------------------------------------------------------