├── .gitignore ├── src ├── consts.h ├── dynamic_lookup.h ├── env_parser.h ├── string_funs.h ├── logging.h ├── syscalls.h ├── dynamic_lookup.c ├── env_parser.c ├── rtld_loader.c ├── logging.c └── string_funs.c ├── replit.nix ├── scripts ├── ci_check.sh └── mypython ├── test ├── dynamic_lookup_test.c ├── string_funs_test.c ├── dynamic_lookup_test.py ├── integration_tests.py └── env_parser_test.c ├── .replit ├── .github └── workflows │ └── checks.yml ├── flake.lock ├── flake.nix ├── Makefile ├── README.md └── .clang-format /.gitignore: -------------------------------------------------------------------------------- 1 | *.generated.* 2 | *.so 3 | *.bin 4 | *.log -------------------------------------------------------------------------------- /src/consts.h: -------------------------------------------------------------------------------- 1 | #define MAX_LD_LIBRARY_PATH_LENGTH 65535 2 | #define MAX_PATH_LENGTH 4096 3 | -------------------------------------------------------------------------------- /replit.nix: -------------------------------------------------------------------------------- 1 | 2 | {pkgs}: { 3 | deps = [ 4 | pkgs.gdb 5 | pkgs.python310Full 6 | pkgs.clang 7 | ]; 8 | } -------------------------------------------------------------------------------- /scripts/ci_check.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env nix-shell 2 | #! nix-shell -i bash -p clang python310Full gnumake 3 | 4 | set -e 5 | 6 | make lint-check 7 | make test -------------------------------------------------------------------------------- /scripts/mypython: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # A python wrapper that uses LD_AUDIT with rtld loader for testing. 4 | 5 | export PYTHONPATH=${REPL_HOME}/.pythonlibs/lib/python3.10/site-packages 6 | export REPLIT_RTLD_LOG_LEVEL=2 7 | export LD_LIBRARY_PATH= 8 | export LD_AUDIT=$REPL_HOME/rtld_loader.so 9 | exec "python3" "$@" -------------------------------------------------------------------------------- /test/dynamic_lookup_test.c: -------------------------------------------------------------------------------- 1 | #include "dynamic_lookup.h" 2 | #include 3 | #include 4 | 5 | int main(int argc, char* argv[]) { 6 | if (argc < 3) { 7 | printf("Please provide libname and ld_library_path\n"); 8 | exit(1); 9 | } 10 | 11 | const char* result = dynamic_lookup(argv[1], argv[2]); 12 | if (result != NULL) { 13 | printf("%s", result); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.replit: -------------------------------------------------------------------------------- 1 | entrypoint = "main.py" 2 | 3 | # modules = ["python-3.10:v40-20240117-0bd73cd"] 4 | 5 | hidden = [".pythonlibs"] 6 | 7 | [env] 8 | PATH = "$REPL_HOME/scripts:$PATH" 9 | 10 | [nix] 11 | channel = "stable-23_11" 12 | 13 | [unitTest] 14 | language = "python3" 15 | 16 | [deployment] 17 | run = ["python3", "main.py"] 18 | deploymentTarget = "cloudrun" 19 | 20 | [[ports]] 21 | localPort = 5900 22 | externalPort = 80 23 | -------------------------------------------------------------------------------- /src/dynamic_lookup.h: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Search for a library in a sequence of directory paths, similar to how 4 | LD_LIBRARY_PATH works. 5 | 6 | * libname - the name of the library 7 | * ld_library_path - a colon-separated string of directory paths to be searched 8 | 9 | Return value 10 | a string containing the absolute path of the shared object file if found, 11 | otherwise NULL 12 | */ 13 | const char* dynamic_lookup(const char* libname, const char* ld_library_path); 14 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: checks 2 | run-name: Check for errors 3 | on: 4 | - push 5 | jobs: 6 | checks: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | with: 11 | fetch-depth: 0 12 | - name: Install Nix 13 | uses: DeterminateSystems/nix-installer-action@main 14 | - name: Run the Magic Nix Cache 15 | uses: DeterminateSystems/magic-nix-cache-action@main 16 | - run: scripts/ci_check.sh 17 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1707238373, 6 | "narHash": "sha256-WKxT0yLzWbFZwYi92lI0yWJpYtRaFSWHGX8QXzejapw=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "fb0c047e30b69696acc42e669d02452ca1b55755", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixos-23.11", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /src/env_parser.h: -------------------------------------------------------------------------------- 1 | // Parser states 2 | enum env_parser_state { 3 | PARSE_VARNAME, 4 | PARSE_IGNORED, 5 | PARSE_LD_LIBRARY_PATH, 6 | PARSE_LOG_LEVEL 7 | }; 8 | 9 | #define MAX_VARNAME_LENGTH 1024 10 | 11 | /* 12 | Parses /proc/self/environ file to set config options for the rtld loader. 13 | Which are: 14 | 15 | * REPLIT_LD_LIBRARY_PATH - colon-separated directory paths 16 | * REPLIT_RTLD_LOG_LEVEL - 0, 1, 2, or 3 for verbosity. See logging.h 17 | 18 | * fd - the file descriptor to read from. Expects it to be in 19 | format of /proc/self/environ 20 | * replit_ld_library_path_buffer - buffer to output the value of 21 | REPLIT_LD_LIBARY_PATH 22 | * log_level - location to output value of REPLIT_RTLD_LOG_LEVEL 23 | */ 24 | void parse_env(int fd, char* replit_ld_library_path_buffer, int* log_level); 25 | -------------------------------------------------------------------------------- /src/string_funs.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define MAX_DECIMAL_INT_LEN 12 4 | /* 5 | All string functions are vendored or DIY. 6 | */ 7 | 8 | // Functions copied verbatim from glibc 9 | int strcmp(const char* p1, const char* p2); 10 | int strncmp(const char* s1, const char* s2, size_t n); 11 | 12 | // Convert an int to a string. For display purposes. 13 | int itoa(int value, char* sp, int radix); 14 | 15 | // DIY versions of functions in string.h 16 | // Didn't copy them because the code is too complicated 17 | char* my_strrchr(const char* haystack, char needle); 18 | char* my_strchrnul(const char* haystack, char needle); 19 | char* my_strncpy(char* destination, const char* source, size_t num); 20 | int my_strlen(const char* str); 21 | 22 | // Convinience string functions 23 | int streql(const char* str1, const char* str2); 24 | int strneql(const char* str1, const char* str2, int n); 25 | int strstartswith(const char* prefix, const char* str); 26 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "The Replit RTLD Loader allows dynamically loaded shared libraries (.so) to work seamlessly in Repls"; 3 | inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11"; 4 | 5 | outputs = { self, nixpkgs, ... }: 6 | let pkgs = import nixpkgs { 7 | system = "x86_64-linux"; 8 | }; 9 | package = pkgs.stdenv.mkDerivation { 10 | pname = "replit_rtld_loader"; 11 | version = "1"; 12 | src = ./.; 13 | buildInputs = [pkgs.python310]; 14 | installPhase = '' 15 | mkdir $out 16 | mv rtld_loader.so $out/ 17 | ''; 18 | }; 19 | in 20 | { 21 | packages.x86_64-linux.default = package; 22 | devShells.x86_64-linux.default = pkgs.mkShell { 23 | packages = with pkgs; [ 24 | python310 25 | gnumake 26 | ]; 27 | }; 28 | overlays.default = final: prev: { 29 | replit-rtld-loader = package; 30 | }; 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: rtld_loader.so 3 | 4 | .PHONY: test 5 | test: string_funs_test.bin env_parser_test.bin dynamic_lookup_test.bin rtld_loader.so 6 | ./string_funs_test.bin 7 | ./env_parser_test.bin 8 | python3 test/dynamic_lookup_test.py 9 | python3 test/integration_tests.py 10 | 11 | rtld_loader.so: $(shell find src -type f) 12 | gcc -shared -nostdlib -fno-stack-protector -fPIC -O2 src/*.c -o rtld_loader.so 13 | 14 | string_funs_test.bin: test/string_funs_test.c src/string_funs.[ch] 15 | gcc $^ -g -o $@ -I src 16 | 17 | env_parser_test.bin: test/env_parser_test.c src/env_parser.[ch] src/string_funs.[ch] src/syscalls.h src/logging.[ch] 18 | gcc $^ -g -o $@ -I src 19 | 20 | dynamic_lookup_test.bin: test/dynamic_lookup_test.c src/dynamic_lookup.[ch] src/string_funs.[ch] src/syscalls.h src/logging.[ch] 21 | gcc $^ -g -o $@ -I src 22 | 23 | .PHONY: lint 24 | lint: src/*.[ch] test/*.[ch] 25 | clang-format -i $^ 26 | 27 | .PHONY: lint-check 28 | lint-check: src/*.[ch] test/*.[ch] 29 | clang-format --dry-run --Werror -i $^ 30 | 31 | .PHONY: clean 32 | clean: 33 | rm rtld_loader.so || true 34 | rm *.bin || true -------------------------------------------------------------------------------- /src/logging.h: -------------------------------------------------------------------------------- 1 | #define DEBUG 3 2 | #define INFO 2 3 | #define WARN 1 4 | #define OFF 0 5 | 6 | /* 7 | Initializes the logger, setting the log level. If log level == 0, 8 | no log file is created. 9 | */ 10 | void log_init(int level); 11 | 12 | /* Write message to the log with the specified level. If 13 | level is higher than the log level used to initialize the logger, 14 | the message will be discarded. 15 | */ 16 | void log_write(const char* message, int level); 17 | // Write integer to the log wit the specified level. 18 | void log_write_int(int num, int level); 19 | 20 | /* 21 | The functions below are just conviniences for log_write and log_write_int with 22 | predefined levels. 23 | */ 24 | void log_info(const char* message); 25 | void log_info_int(int num); 26 | 27 | void log_debug(const char* message); 28 | void log_debug_int(int num); 29 | 30 | void log_warn(const char* message); 31 | void log_warn_int(int num); 32 | 33 | // functions for printing to something other than the log file, for example: 34 | // 1 for stdout 35 | // 2 for stderr 36 | void fprint(int fd, const char* message); 37 | void fprint_int(int fd, int num); 38 | -------------------------------------------------------------------------------- /test/string_funs_test.c: -------------------------------------------------------------------------------- 1 | #include "string_funs.h" 2 | #include 3 | #include 4 | 5 | void test_my_strrchr() { 6 | char str[] = "zbcabc"; 7 | assert(&str[5] == my_strrchr(str, 'c')); 8 | assert(NULL == my_strrchr(str, 'e')); 9 | assert(&str[4] == my_strrchr(str, 'b')); 10 | assert(&str[3] == my_strrchr(str, 'a')); 11 | assert(&str[0] == my_strrchr(str, 'z')); 12 | } 13 | 14 | void test_my_strchrnul() { 15 | char str[] = "abc\0de"; 16 | assert(&str[0] == my_strchrnul(str, 'a')); 17 | assert(&str[1] == my_strchrnul(str, 'b')); 18 | assert(&str[2] == my_strchrnul(str, 'c')); 19 | assert(&str[3] == my_strchrnul(str, 'd')); 20 | assert(&str[3] == my_strchrnul(str, 'e')); 21 | assert(&str[3] == my_strchrnul(str, 'f')); 22 | } 23 | 24 | void test_my_strncpy() { 25 | char dest1[6] = "XXXXX"; 26 | char dest2[6] = "XXXXX"; 27 | char dest3[6] = "XXXXX"; 28 | char str[] = "abc"; 29 | 30 | my_strncpy(dest1, str, 3); 31 | assert(streql(dest1, "abcXX")); 32 | 33 | my_strncpy(dest2, str, 5); 34 | assert(streql(dest2, "abc")); 35 | assert(dest2[3] == '\0'); 36 | assert(dest2[4] == '\0'); 37 | 38 | my_strncpy(dest3, str, 2); 39 | assert(streql(dest3, "abXXX")); 40 | } 41 | 42 | void test_my_strlen() { 43 | assert(3 == my_strlen("abc")); 44 | assert(5 == my_strlen("abcde")); 45 | assert(0 == my_strlen("")); 46 | } 47 | 48 | int main() { 49 | test_my_strrchr(); 50 | test_my_strchrnul(); 51 | test_my_strncpy(); 52 | test_my_strlen(); 53 | printf("OK\n"); 54 | } 55 | -------------------------------------------------------------------------------- /test/dynamic_lookup_test.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | # Fake lib directories base dir 4 | basedir = "/tmp/dynamic_lookup_test" 5 | 6 | subprocess.run(['rm', '-fr', basedir]) 7 | subprocess.run(['mkdir', '-p', basedir + '/cairo/lib']) 8 | subprocess.run(['mkdir', '-p', basedir + '/libc++/lib']) 9 | subprocess.run(['touch', basedir + '/cairo/lib/libcairo.so']) 10 | subprocess.run(['touch', basedir + '/cairo/lib/libcairo.so.2']) 11 | subprocess.run(['touch', basedir + '/libc++/lib/libc++.so']) 12 | subprocess.run(['touch', basedir + '/libc++/lib/libc++.so.1.0']) 13 | 14 | ld_library_path = '%s/cairo/lib:%s/libc++/lib' % (basedir, basedir) 15 | 16 | output = subprocess.check_output(['./dynamic_lookup_test.bin', 'libcairo.so', ld_library_path]) 17 | assert str(output, 'UTF-8') == '%s/cairo/lib/libcairo.so' % basedir 18 | 19 | output = subprocess.check_output(['./dynamic_lookup_test.bin', 'libcairo.so.2', ld_library_path]) 20 | assert str(output, 'UTF-8') == '%s/cairo/lib/libcairo.so.2' % basedir 21 | 22 | output = subprocess.check_output(['./dynamic_lookup_test.bin', 'libc++.so', ld_library_path]) 23 | assert str(output, 'UTF-8') == '%s/libc++/lib/libc++.so' % basedir 24 | 25 | output = subprocess.check_output(['./dynamic_lookup_test.bin', 'libc++.so.1.0', ld_library_path]) 26 | assert str(output, 'UTF-8') == '%s/libc++/lib/libc++.so.1.0' % basedir 27 | 28 | output = subprocess.check_output(['./dynamic_lookup_test.bin', 'blargh.so', ld_library_path]) 29 | assert str(output, 'UTF-8') == '' 30 | 31 | output = subprocess.check_output(['./dynamic_lookup_test.bin', 'libcairo.so', '']) 32 | assert str(output, 'UTF-8') == '' 33 | 34 | print('OK') 35 | -------------------------------------------------------------------------------- /src/syscalls.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | /* 4 | Do without libc and call system calls directly. 5 | 6 | Resource for Linux syscalls: 7 | https://syscalls64.paolostivanin.com/ 8 | 9 | These stubs use the trick that function parameters in C are passed by 10 | registers, which is also how system call parameters are passed 11 | (we are lucky the Linux kernel is written in C). 12 | 13 | In each case 14 | 15 | 1. first parameter is passed in the %rdi register 16 | 2. second param in %rsi 17 | 3. third in %rdx 18 | 19 | Example: 20 | when sys_access("/path/to/file", R_OK) is called, 21 | 22 | the pointer to the string constant is put in %rdi 23 | and the value of R_OK in %rsi. Because Linux syscalls use 24 | these same registers, all we have to do 25 | is set the syscall number in %rax to let the kernel 26 | know which one - for sys_access 27 | it's 21 - and the parameters get passed through 28 | to the system call as they are. 29 | 30 | */ 31 | 32 | int sys_access(const char* path, int mode); 33 | __asm__( 34 | "sys_access:\n" 35 | " mov $21, %rax\n" 36 | " syscall\n" 37 | " ret\n"); 38 | 39 | int sys_write(int fd, const char* buf, size_t count); 40 | __asm__( 41 | "sys_write:\n" 42 | " mov $1, %rax\n" 43 | " syscall\n" 44 | " ret\n"); 45 | 46 | int sys_open(const char* path, int flags, int mode); 47 | __asm__( 48 | "sys_open:\n" 49 | " mov $2, %rax\n" 50 | " syscall\n" 51 | " ret\n"); 52 | 53 | int sys_close(int fd); 54 | __asm__( 55 | "sys_close:\n" 56 | " mov $3, %rax\n" 57 | " syscall\n" 58 | " ret\n"); 59 | 60 | int sys_read(int fd, char* buf, size_t count); 61 | __asm__( 62 | "sys_read:\n" 63 | " mov $0, %rax\n" 64 | " syscall\n" 65 | " ret\n"); 66 | -------------------------------------------------------------------------------- /src/dynamic_lookup.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "consts.h" 3 | #include "logging.h" 4 | #include "string_funs.h" 5 | #include "syscalls.h" 6 | 7 | // We cannot using malloc, so here is the buffer used to return 8 | // search results. 9 | static char search_result[MAX_PATH_LENGTH] = {0}; 10 | 11 | const char* dynamic_lookup(const char* libname, const char* ld_library_path) { 12 | int libname_len = my_strlen(libname); 13 | const char* rllp = ld_library_path; 14 | const char* next_rllp = rllp; 15 | while (*next_rllp) { 16 | rllp = next_rllp; 17 | next_rllp = my_strchrnul(rllp, ':'); 18 | 19 | size_t rllp_len = next_rllp - rllp; 20 | if (*next_rllp) { 21 | // Advance past colon 22 | next_rllp++; 23 | } 24 | 25 | char current_lib_path[MAX_PATH_LENGTH] = {0}; 26 | if (rllp_len + libname_len + 2 > MAX_PATH_LENGTH) { 27 | // We need the lib_path to be able to fit the path, the separating slash, 28 | // the filename, and the null terminator. If not, we are ignoring it 29 | continue; 30 | } 31 | my_strncpy(current_lib_path, rllp, rllp_len); 32 | char* lib_path_suffix = current_lib_path + rllp_len; 33 | lib_path_suffix[0] = '/'; 34 | my_strncpy(lib_path_suffix + 1, libname, libname_len); 35 | int lib_path_len = rllp_len + 1 + libname_len; 36 | lib_path_suffix[1 + libname_len] = '\0'; 37 | 38 | log_debug(" trying "); 39 | log_debug(current_lib_path); 40 | log_debug("\n"); 41 | // check if we can read the file (i.e. it exists and we can read it). 42 | if (sys_access(current_lib_path, R_OK) == 0) { 43 | my_strncpy(search_result, current_lib_path, lib_path_len); 44 | search_result[lib_path_len] = '\0'; 45 | return search_result; 46 | } 47 | } 48 | 49 | return NULL; 50 | } 51 | -------------------------------------------------------------------------------- /test/integration_tests.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | 4 | pwd = os.getcwd() 5 | rtld_env = { 6 | "LD_AUDIT": "%s/rtld_loader.so" % pwd, 7 | "REPLIT_RTLD_LOG_LEVEL": "2" 8 | } 9 | python_23_11 = '/nix/store/2miairdnqbrsjlcllj2vypnvmk2k9z6j-python3-3.10.13' 10 | ace = '/nix/store/mk39yjfi7n0z9qy0wv56pg696y1aj3d8-ace-7.0.8/' 11 | 12 | def realize(pkgpath): 13 | subprocess.run(['nix-store', '-r', pkgpath], stderr=subprocess.PIPE, stdout=subprocess.PIPE) 14 | 15 | realize(python_23_11) 16 | realize(ace) 17 | 18 | def test_ace_dynamic(): 19 | subprocess.run(["bash", "-c", "rm rtld_loader.log.*"]) 20 | code = "import ctypes; print(ctypes.cdll.LoadLibrary('libACE_ETCL.so'))" 21 | 22 | env = rtld_env.copy() 23 | env.update({ 24 | 'REPLIT_LD_LIBRARY_PATH': '%s/lib' % ace 25 | }) 26 | output = subprocess.check_output( 27 | ["%s/bin/python" % python_23_11, '-c', code], 28 | env = env 29 | ) 30 | 31 | print(str(output, 'UTF-8')) 32 | assert str(output, 'UTF-8').startswith(" 3 | #include "consts.h" 4 | #include "string_funs.h" 5 | #include "syscalls.h" 6 | 7 | /* 8 | Simple DFA-like parser (reads char by char; has a state) 9 | */ 10 | void parse_env(int fd, char* replit_ld_library_path_buffer, int* log_level) { 11 | char buf[1024]; 12 | char varname[MAX_VARNAME_LENGTH]; 13 | int varname_cursor = 0; 14 | int state = PARSE_VARNAME; 15 | int ld_library_path_cursor = 0; 16 | replit_ld_library_path_buffer[0] = '\0'; 17 | *log_level = 0; 18 | while (1) { 19 | int bytes = sys_read(fd, buf, sizeof(buf)); 20 | if (bytes <= 0) { 21 | break; 22 | } 23 | for (int i = 0; i < bytes; i++) { 24 | char chr = buf[i]; 25 | if (state == PARSE_VARNAME) { 26 | if (chr == '=') { 27 | if (strneql(varname, "REPLIT_LD_LIBRARY_PATH", varname_cursor)) { 28 | state = PARSE_LD_LIBRARY_PATH; 29 | ld_library_path_cursor = 0; 30 | } else if (strneql(varname, "REPLIT_RTLD_LOG_LEVEL", 31 | varname_cursor)) { 32 | state = PARSE_LOG_LEVEL; 33 | } else { 34 | state = PARSE_IGNORED; 35 | } 36 | } else { 37 | if (varname_cursor >= MAX_VARNAME_LENGTH) { 38 | continue; // truncate the varname if too long 39 | } 40 | varname[varname_cursor++] = chr; 41 | } 42 | } else if (state == PARSE_IGNORED) { 43 | if (chr == '\0') { 44 | state = PARSE_VARNAME; 45 | varname_cursor = 0; 46 | } 47 | } else if (state == PARSE_LD_LIBRARY_PATH) { 48 | if (ld_library_path_cursor >= MAX_LD_LIBRARY_PATH_LENGTH - 1) { 49 | // too long. truncate it 50 | replit_ld_library_path_buffer[MAX_LD_LIBRARY_PATH_LENGTH - 1] = '\0'; 51 | state = PARSE_IGNORED; 52 | } else if (chr == '\0') { 53 | replit_ld_library_path_buffer[ld_library_path_cursor] = '\0'; 54 | state = PARSE_VARNAME; 55 | varname_cursor = 0; 56 | } else { 57 | replit_ld_library_path_buffer[ld_library_path_cursor++] = chr; 58 | } 59 | } else if (state == PARSE_LOG_LEVEL) { 60 | if (chr >= '0' && chr <= '9') { 61 | *log_level = chr - '0'; 62 | } 63 | // Only take one character, ignore the 64 | // rest of the value. This means double 65 | // digit values will not work as expected. 66 | state = PARSE_IGNORED; 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/rtld_loader.c: -------------------------------------------------------------------------------- 1 | /* 2 | Entrypoint of the rtld loader. It uses the rtld-audit API provided by Linux: 3 | https://man7.org/linux/man-pages/man7/rtld-audit.7.html 4 | */ 5 | 6 | #define _GNU_SOURCE 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "consts.h" 13 | #include "dynamic_lookup.h" 14 | #include "env_parser.h" 15 | #include "logging.h" 16 | #include "string_funs.h" 17 | #include "syscalls.h" 18 | 19 | static char replit_ld_library_path[MAX_LD_LIBRARY_PATH_LENGTH] = {0}; 20 | 21 | __attribute__((constructor)) static void init(void) { 22 | int log_level; 23 | int fd = sys_open("/proc/self/environ", O_RDONLY, 0); 24 | parse_env(fd, replit_ld_library_path, &log_level); 25 | sys_close(fd); 26 | log_init(log_level); 27 | } 28 | 29 | unsigned int la_version(unsigned int version) { 30 | log_info("RTLD: la_version("); 31 | log_info_int(version); 32 | log_info(")\n"); 33 | return version; 34 | } 35 | 36 | char* la_objsearch(const char* name, uintptr_t* cookie, unsigned int flag) { 37 | log_debug("la_objsearch("); 38 | log_debug(name); 39 | log_debug(", "); 40 | log_debug_int(flag); 41 | log_debug(")\n"); 42 | if (flag == LA_SER_DEFAULT) { 43 | char* libname = my_strrchr(name, '/'); 44 | if (libname != NULL) { 45 | libname++; // advance past the / 46 | log_info("ld library miss for "); 47 | log_info(libname); 48 | log_info("\n searching...\n"); 49 | const char* result = NULL; 50 | if (result == NULL) { 51 | const char* search_path = replit_ld_library_path; 52 | if (strstartswith("/nix/store/", name)) { 53 | // Count slashes to find the 5th one (after /nix/store/hash-name/lib/) 54 | int slash_count = 0; 55 | int i; 56 | for (i = 0; name[i] && slash_count < 5; i++) { 57 | if (name[i] == '/') slash_count++; 58 | } 59 | // Check if it's actually a /lib/ directory 60 | if (i >= 5 && i <= MAX_LD_LIBRARY_PATH_LENGTH && strneql(name + i - 5, "/lib/", 5)) { 61 | char extended_path[MAX_LD_LIBRARY_PATH_LENGTH]; 62 | my_strncpy(extended_path, name, i); 63 | extended_path[i - 1] = ':'; 64 | my_strncpy(extended_path + i, replit_ld_library_path, MAX_LD_LIBRARY_PATH_LENGTH - i - 1); 65 | search_path = extended_path; 66 | } 67 | } 68 | result = dynamic_lookup(libname, search_path); 69 | if (result != NULL) { 70 | log_info(" found dynamically: "); 71 | log_info(result); 72 | log_info("\n"); 73 | return (char*)result; 74 | } 75 | } 76 | log_info(" not found.\n"); 77 | } 78 | } 79 | return (char*)name; 80 | } 81 | 82 | unsigned int la_objopen(struct link_map* map, Lmid_t lmid, uintptr_t* cookie) { 83 | log_debug("la_objopen("); 84 | log_debug(map->l_name); 85 | log_debug(")\n"); 86 | return 0; 87 | } 88 | 89 | void la_preinit(uintptr_t* cookie) { 90 | log_info("la_preinit()\n"); 91 | } 92 | -------------------------------------------------------------------------------- /src/logging.c: -------------------------------------------------------------------------------- 1 | #include "logging.h" 2 | #include 3 | #include "string_funs.h" 4 | #include "syscalls.h" 5 | 6 | #define LOG_FILE_PREFIX "rtld_loader.log." 7 | #define LOG_FILE_PREFIX_LENGTH 16 8 | 9 | static int audit_log_fd = -1; 10 | static int log_level; 11 | 12 | void fprint(int fd, const char* message) { 13 | int len = my_strlen(message); 14 | sys_write(fd, message, len); 15 | } 16 | 17 | void fprint_int(int fd, int num) { 18 | char int_str[MAX_DECIMAL_INT_LEN]; 19 | int len = itoa(num, int_str, 10); 20 | sys_write(fd, int_str, len); 21 | } 22 | 23 | // For troubleshooting purposes: output contents of /proc/self/cmdline to the 24 | // log file 25 | void _output_cmdline() { 26 | char buf[1024]; 27 | if (log_level < INFO) { 28 | return; 29 | } 30 | log_info("cmdline: "); 31 | int cmdline_fd = sys_open("/proc/self/cmdline", O_RDONLY, 0); 32 | while (1) { 33 | int bytes = sys_read(cmdline_fd, buf, sizeof(buf)); 34 | if (bytes <= 0) { 35 | break; 36 | } 37 | sys_write(audit_log_fd, buf, bytes); 38 | } 39 | log_info("\n"); 40 | sys_close(cmdline_fd); 41 | } 42 | 43 | void log_init(int ll) { 44 | log_level = ll; 45 | if (log_level > 0) { 46 | // Generated a log file name with a numeric suffix, i.e. rtld_loader.log.1 47 | // if the file already exists, increment the suffix 48 | char log_filename[LOG_FILE_PREFIX_LENGTH + MAX_DECIMAL_INT_LEN + 1]; 49 | my_strncpy(log_filename, LOG_FILE_PREFIX, LOG_FILE_PREFIX_LENGTH); 50 | int suffix = 1; 51 | while (1) { 52 | int suffix_len = itoa(suffix, log_filename + LOG_FILE_PREFIX_LENGTH, 10); 53 | log_filename[LOG_FILE_PREFIX_LENGTH + suffix_len] = '\0'; 54 | if (sys_access(log_filename, R_OK) == 0) { 55 | // file already exists, try the next suffix 56 | suffix++; 57 | } else { 58 | // we can use this one 59 | audit_log_fd = 60 | sys_open(log_filename, O_WRONLY | O_CREAT | O_TRUNC, 0644); 61 | break; 62 | } 63 | } 64 | 65 | _output_cmdline(); 66 | } 67 | } 68 | 69 | void log_write(const char* message, int level) { 70 | if (audit_log_fd == -1) { 71 | return; 72 | } 73 | if (level > log_level) { 74 | return; 75 | } 76 | fprint(audit_log_fd, message); 77 | } 78 | 79 | void log_write_int(int num, int level) { 80 | if (audit_log_fd == -1) { 81 | return; 82 | } 83 | if (level > log_level) { 84 | return; 85 | } 86 | fprint_int(audit_log_fd, num); 87 | } 88 | 89 | void log_info(const char* message) { 90 | log_write(message, INFO); 91 | } 92 | 93 | void log_info_int(int num) { 94 | log_write_int(num, INFO); 95 | } 96 | 97 | void log_debug(const char* message) { 98 | log_write(message, DEBUG); 99 | } 100 | 101 | void log_debug_int(int num) { 102 | log_write_int(num, DEBUG); 103 | } 104 | 105 | void log_warn(const char* message) { 106 | log_write(message, WARN); 107 | } 108 | 109 | void log_warn_int(int num) { 110 | log_write_int(num, WARN); 111 | } 112 | -------------------------------------------------------------------------------- /test/env_parser_test.c: -------------------------------------------------------------------------------- 1 | #include "env_parser.h" 2 | #include 3 | #include 4 | #include 5 | #include "consts.h" 6 | #include "logging.h" 7 | #include "string_funs.h" 8 | #include "syscalls.h" 9 | 10 | void test0() { 11 | char replit_ld_library_path[MAX_LD_LIBRARY_PATH_LENGTH] = "BLARGH"; 12 | int log_level = -1; 13 | int fd = 14 | sys_open("/tmp/fake_environ.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); 15 | sys_close(fd); 16 | 17 | fd = sys_open("/tmp/fake_environ.txt", O_RDONLY, 0); 18 | parse_env(fd, (char*)&replit_ld_library_path, &log_level); 19 | assert(streql(replit_ld_library_path, "")); 20 | assert(0 == log_level); 21 | } 22 | 23 | void test1() { 24 | char replit_ld_library_path[MAX_LD_LIBRARY_PATH_LENGTH] = {0}; 25 | int log_level = -1; 26 | char* content = "BLAH=FOOBAR\0REPLIT_LD_LIBRARY_PATH=/path/one:/path/two\0"; 27 | int fd = 28 | sys_open("/tmp/fake_environ.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); 29 | sys_write(fd, content, 55); 30 | sys_close(fd); 31 | 32 | fd = sys_open("/tmp/fake_environ.txt", O_RDONLY, 0); 33 | parse_env(fd, (char*)&replit_ld_library_path, &log_level); 34 | assert(streql(replit_ld_library_path, "/path/one:/path/two")); 35 | assert(0 == log_level); 36 | } 37 | 38 | void test2() { 39 | char replit_ld_library_path[MAX_LD_LIBRARY_PATH_LENGTH] = {0}; 40 | int log_level = -1; 41 | char* content = "BLAH=FOOBAR\0REPLIT_RTLD_LOG_LEVEL=1\0"; 42 | int fd = 43 | sys_open("/tmp/fake_environ.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); 44 | sys_write(fd, content, 36); 45 | sys_close(fd); 46 | 47 | fd = sys_open("/tmp/fake_environ.txt", O_RDONLY, 0); 48 | parse_env(fd, (char*)&replit_ld_library_path, &log_level); 49 | assert(streql(replit_ld_library_path, "")); 50 | assert(1 == log_level); 51 | } 52 | 53 | void test3() { 54 | char replit_ld_library_path[MAX_LD_LIBRARY_PATH_LENGTH] = {0}; 55 | int log_level = -1; 56 | char* content = "BLAH=FOOBAR\0REPLIT_RTLD_LOG_LEVEL=2"; 57 | int fd = 58 | sys_open("/tmp/fake_environ.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); 59 | sys_write(fd, content, 35); 60 | sys_close(fd); 61 | 62 | fd = sys_open("/tmp/fake_environ.txt", O_RDONLY, 0); 63 | parse_env(fd, (char*)&replit_ld_library_path, &log_level); 64 | assert(streql(replit_ld_library_path, "")); 65 | assert(2 == log_level); 66 | } 67 | 68 | void test4() { 69 | char replit_ld_library_path[MAX_LD_LIBRARY_PATH_LENGTH] = {0}; 70 | int log_level = -1; 71 | char* content = 72 | "BLAH=FOOBAR\0REPLIT_LD_LIBRARY_PATH=/path/one:/path/" 73 | "two\0REPLIT_RTLD_LOG_LEVEL=1"; 74 | int fd = 75 | sys_open("/tmp/fake_environ.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); 76 | sys_write(fd, content, 78); 77 | sys_close(fd); 78 | 79 | fd = sys_open("/tmp/fake_environ.txt", O_RDONLY, 0); 80 | parse_env(fd, (char*)&replit_ld_library_path, &log_level); 81 | assert(streql(replit_ld_library_path, "/path/one:/path/two")); 82 | assert(1 == log_level); 83 | } 84 | 85 | int main() { 86 | test0(); 87 | test1(); 88 | test2(); 89 | test4(); 90 | printf("OK\n"); 91 | } 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Replit RTLD Loader 2 | 3 | The Replit RTLD Loader allows dynamically loaded shared libraries (.so) 4 | to work seamlessly in Repls. It uses 5 | the [rtld-audit API](https://man7.org/linux/man-pages/man7/rtld-audit.7.html) 6 | to observe a process's library loading activities. When the native loader ld-linux 7 | fails to find a library, RTLD loader steps in resolves the desired library using 8 | directories in the `REPLIT_LD_LIBRARY_PATH` variable. It is a better alternative 9 | than using `LD_LIBRARY_PATH` because rather than overriding the default behavior 10 | of the system loader, it acts as a fallback. 11 | 12 | ## Background and Motivation 13 | 14 | At Replit we use Nix to deliver almost all our software to users. However, we noticed 15 | users experiencing programs crashing with errors like these: 16 | 17 | ``` 18 | symbol lookup error: /nix/store/dg8mpqqykmw9c7l0bgzzb5znkymlbfjw-glibc-2.37-8/lib/libc.so.6: undefined symbol: _dl_audit_symbind_alt, version GLIBC_PRIVATE 19 | ``` 20 | 21 | ``` 22 | /nix/store/dg8mpqqykmw9c7l0bgzzb5znkymlbfjw-glibc-2.37-8/lib/libm.so.6: version `GLIBC_2.38' not found (required by /nix/store/8w6mm5q1n7i7cs1933im5vkbgvjlglfn-python3-3.10.13/lib/libpython3.10.so.1.0) 23 | ``` 24 | 25 | glibc is the GNU standard C library, a fundamental library used by virtually all programs. 26 | These errors mean there is a mismatch between the version of glibc required by a library and the one that's available. 27 | This can happen when we have programs and libraries from different Nix channels interacting with each other. 28 | 29 | For example, if a Python program uses libcairo (maybe via pycairo), an entry containing the `libcairo.so` shared library would be added to the `LD_LIBRARY_PATH` variable, telling the system library loader to look for libraries there in addition to the normal places. But if Python is built from a different Nix channel from libcairo, they may depend on different versions of glibc. Python will get to choose its desired glibc version, but if it is incompatible with libcairo because its Nix channel is older than that of libcairo, the program will crash when we try to load libcairo. 30 | 31 | We found the approach of using `LD_LIBRARY_PATH` too heavy-handed: it forced programs to abide by it even if the program already knows where its required its compatible libraries are, via its own [runpath](https://amir.rachum.com/shared-libraries/). A tamer approach is called for. With the RTLD loader, we now use the `REPLIT_LD_LIBRARY_PATH` variable, which will be used to search libraries only after loader fails to find the required libraries within the program's runpath. This plus delivering our software on the latest Nix channel will help us get rid of those glibc version mis-match problems. 32 | 33 | ## How it Works 34 | 35 | 1. Activate the loader via the LD_AUDIT variable when running a program, ex: `LD_AUDIT=rtld_loader.so python main.py` 36 | 2. If the system loader fails to locate a library, say `libcairo.so`, it will search the directories within `REPLIT_LD_LIBRARY_PATH` 37 | for a library with that name. 38 | 39 | How does it tell the system loader has failed to load a library? We use a `la_objsearch` hook 40 | in the [rtld_loader API](https://man7.org/linux/man-pages/man7/rtld-audit.7.html). If the `flag` argument 41 | passed in is equal to `LA_SER_DEFAULT`, that means the system loader has failed to find the requested library 42 | from the `runpath` entries of the binary executable and is instead defaulting to searching the system library 43 | paths. RTLD loader detects this when it occurs and intercepts the request, searches for the requested library 44 | via directories listed in `REPLIT_LD_LIBRARY_PATH`, and returns the full path of the library if it is found. 45 | 46 | ## Logging 47 | 48 | You can control the loader's log level via the `REPLIT_RTLD_LOG_LEVEL` environment variable. Valid values: 49 | 50 | * 0 - off 51 | * 1 - warnings 52 | * 2 - info 53 | * 3 - debug 54 | 55 | Also see env_parser.h and logging.h 56 | 57 | ## Self Reliance 58 | 59 | We are not using libc at all to avoid any libc conflicts with 60 | the running binary. It might be possible compile libc statically into 61 | rtld_loader.so using musl or similar, but I had a hard time with musl in 62 | last attempt. So: 63 | 64 | * we call system calls directly for file system access 65 | * we vendor or DIY string functions -------------------------------------------------------------------------------- /src/string_funs.c: -------------------------------------------------------------------------------- 1 | #include "string_funs.h" 2 | 3 | /* 4 | Taken from https://github.com/lattera/glibc/blob/master/string/strcmp.c 5 | */ 6 | int strcmp(const char* p1, const char* p2) { 7 | const unsigned char* s1 = (const unsigned char*)p1; 8 | const unsigned char* s2 = (const unsigned char*)p2; 9 | unsigned char c1, c2; 10 | 11 | do { 12 | c1 = (unsigned char)*s1++; 13 | c2 = (unsigned char)*s2++; 14 | if (c1 == '\0') 15 | return c1 - c2; 16 | } while (c1 == c2); 17 | 18 | return c1 - c2; 19 | } 20 | 21 | /* 22 | Taken from https://github.com/lattera/glibc/blob/master/string/strncmp.c 23 | */ 24 | int strncmp(const char* s1, const char* s2, size_t n) { 25 | unsigned char c1 = '\0'; 26 | unsigned char c2 = '\0'; 27 | 28 | if (n >= 4) { 29 | size_t n4 = n >> 2; 30 | do { 31 | c1 = (unsigned char)*s1++; 32 | c2 = (unsigned char)*s2++; 33 | if (c1 == '\0' || c1 != c2) 34 | return c1 - c2; 35 | c1 = (unsigned char)*s1++; 36 | c2 = (unsigned char)*s2++; 37 | if (c1 == '\0' || c1 != c2) 38 | return c1 - c2; 39 | c1 = (unsigned char)*s1++; 40 | c2 = (unsigned char)*s2++; 41 | if (c1 == '\0' || c1 != c2) 42 | return c1 - c2; 43 | c1 = (unsigned char)*s1++; 44 | c2 = (unsigned char)*s2++; 45 | if (c1 == '\0' || c1 != c2) 46 | return c1 - c2; 47 | } while (--n4 > 0); 48 | n &= 3; 49 | } 50 | 51 | while (n > 0) { 52 | c1 = (unsigned char)*s1++; 53 | c2 = (unsigned char)*s2++; 54 | if (c1 == '\0' || c1 != c2) 55 | return c1 - c2; 56 | n--; 57 | } 58 | 59 | return c1 - c2; 60 | } 61 | 62 | // Taken from https://stackoverflow.com/a/12386915 63 | int itoa(int value, char* sp, int radix) { 64 | char tmp[34]; // the longest integer's string representation is 32 characters 65 | // in base 2. we also need one character for the sign and one 66 | // more for the trailing NUL. 67 | char* tp = tmp; 68 | int i; 69 | unsigned v; 70 | if (radix < 2 || radix > 36) 71 | return -1; 72 | int sign = (radix == 10 && value < 0); 73 | if (sign) 74 | v = -value; 75 | else 76 | v = (unsigned)value; 77 | 78 | while (v || tp == tmp) { 79 | i = v % radix; 80 | v /= radix; 81 | if (i < 10) 82 | *tp++ = i + '0'; 83 | else 84 | *tp++ = i + 'a' - 10; 85 | } 86 | 87 | int len = tp - tmp; 88 | 89 | if (sign) { 90 | *sp++ = '-'; 91 | len++; 92 | } 93 | 94 | while (tp > tmp) 95 | *sp++ = *--tp; 96 | 97 | return len; 98 | } 99 | 100 | /* 101 | These functions are DIY because the glibc versions are too complicated 102 | */ 103 | 104 | // https://linux.die.net/man/3/strrchr 105 | char* my_strrchr(const char* haystack, char needle) { 106 | int len = my_strlen(haystack); 107 | for (int i = len - 1; i >= 0; i--) { 108 | char chr = haystack[i]; 109 | if (chr == needle) { 110 | return (char*)&haystack[i]; 111 | } 112 | } 113 | return NULL; 114 | } 115 | 116 | // https://linux.die.net/man/3/strchrnul 117 | char* my_strchrnul(const char* haystack, char needle) { 118 | for (int i = 0;; i++) { 119 | char chr = haystack[i]; 120 | if (chr == needle) { 121 | return (char*)&haystack[i]; 122 | } 123 | if (chr == '\0') { 124 | return (char*)&haystack[i]; 125 | } 126 | } 127 | } 128 | 129 | // https://linux.die.net/man/3/strncpy 130 | char* my_strncpy(char* destination, const char* source, size_t num) { 131 | int ended = 0; 132 | for (int i = 0; i < num; i++) { 133 | char chr = source[i]; 134 | if (ended) { 135 | destination[i] = '\0'; 136 | continue; 137 | } 138 | destination[i] = chr; 139 | if (chr == '\0') { 140 | ended = 1; 141 | } 142 | } 143 | 144 | return destination; 145 | } 146 | 147 | int my_strlen(const char* str) { 148 | int i = 0; 149 | while (str[i] != '\0') { 150 | i++; 151 | } 152 | return i; 153 | } 154 | 155 | /* 156 | These are just conveniences 157 | */ 158 | 159 | int streql(const char* str1, const char* str2) { 160 | return strcmp(str1, str2) == 0; 161 | } 162 | 163 | int strneql(const char* str1, const char* str2, int n) { 164 | return strncmp(str1, str2, n) == 0; 165 | } 166 | 167 | int strstartswith(const char* prefix, const char* str) { 168 | return strneql(prefix, str, my_strlen(prefix)); 169 | } 170 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: Chromium 4 | AccessModifierOffset: -1 5 | AlignAfterOpenBracket: Align 6 | AlignArrayOfStructures: None 7 | AlignConsecutiveAssignments: 8 | Enabled: false 9 | AcrossEmptyLines: false 10 | AcrossComments: false 11 | AlignCompound: false 12 | PadOperators: true 13 | AlignConsecutiveBitFields: 14 | Enabled: false 15 | AcrossEmptyLines: false 16 | AcrossComments: false 17 | AlignCompound: false 18 | PadOperators: false 19 | AlignConsecutiveDeclarations: 20 | Enabled: false 21 | AcrossEmptyLines: false 22 | AcrossComments: false 23 | AlignCompound: false 24 | PadOperators: false 25 | AlignConsecutiveMacros: 26 | Enabled: false 27 | AcrossEmptyLines: false 28 | AcrossComments: false 29 | AlignCompound: false 30 | PadOperators: false 31 | AlignEscapedNewlines: Left 32 | AlignOperands: Align 33 | AlignTrailingComments: 34 | Kind: Always 35 | OverEmptyLines: 0 36 | AllowAllArgumentsOnNextLine: true 37 | AllowAllParametersOfDeclarationOnNextLine: false 38 | AllowShortBlocksOnASingleLine: Never 39 | AllowShortCaseLabelsOnASingleLine: false 40 | AllowShortEnumsOnASingleLine: true 41 | AllowShortFunctionsOnASingleLine: Inline 42 | AllowShortIfStatementsOnASingleLine: Never 43 | AllowShortLambdasOnASingleLine: All 44 | AllowShortLoopsOnASingleLine: false 45 | AlwaysBreakAfterDefinitionReturnType: None 46 | AlwaysBreakAfterReturnType: None 47 | AlwaysBreakBeforeMultilineStrings: true 48 | AlwaysBreakTemplateDeclarations: Yes 49 | AttributeMacros: 50 | - __capability 51 | BinPackArguments: true 52 | BinPackParameters: false 53 | BitFieldColonSpacing: Both 54 | BraceWrapping: 55 | AfterCaseLabel: false 56 | AfterClass: false 57 | AfterControlStatement: Never 58 | AfterEnum: false 59 | AfterExternBlock: false 60 | AfterFunction: false 61 | AfterNamespace: false 62 | AfterObjCDeclaration: false 63 | AfterStruct: false 64 | AfterUnion: false 65 | BeforeCatch: false 66 | BeforeElse: false 67 | BeforeLambdaBody: false 68 | BeforeWhile: false 69 | IndentBraces: false 70 | SplitEmptyFunction: true 71 | SplitEmptyRecord: true 72 | SplitEmptyNamespace: true 73 | BreakAfterAttributes: Never 74 | BreakAfterJavaFieldAnnotations: false 75 | BreakArrays: true 76 | BreakBeforeBinaryOperators: None 77 | BreakBeforeConceptDeclarations: Always 78 | BreakBeforeBraces: Attach 79 | BreakBeforeInlineASMColon: OnlyMultiline 80 | BreakBeforeTernaryOperators: true 81 | BreakConstructorInitializers: BeforeColon 82 | BreakInheritanceList: BeforeColon 83 | BreakStringLiterals: true 84 | ColumnLimit: 80 85 | CommentPragmas: '^ IWYU pragma:' 86 | CompactNamespaces: false 87 | ConstructorInitializerIndentWidth: 4 88 | ContinuationIndentWidth: 4 89 | Cpp11BracedListStyle: true 90 | DerivePointerAlignment: false 91 | DisableFormat: false 92 | EmptyLineAfterAccessModifier: Never 93 | EmptyLineBeforeAccessModifier: LogicalBlock 94 | ExperimentalAutoDetectBinPacking: false 95 | FixNamespaceComments: true 96 | ForEachMacros: 97 | - foreach 98 | - Q_FOREACH 99 | - BOOST_FOREACH 100 | IfMacros: 101 | - KJ_IF_MAYBE 102 | IncludeBlocks: Preserve 103 | IncludeCategories: 104 | - Regex: '^' 105 | Priority: 2 106 | SortPriority: 0 107 | CaseSensitive: false 108 | - Regex: '^<.*\.h>' 109 | Priority: 1 110 | SortPriority: 0 111 | CaseSensitive: false 112 | - Regex: '^<.*' 113 | Priority: 2 114 | SortPriority: 0 115 | CaseSensitive: false 116 | - Regex: '.*' 117 | Priority: 3 118 | SortPriority: 0 119 | CaseSensitive: false 120 | IncludeIsMainRegex: '([-_](test|unittest))?$' 121 | IncludeIsMainSourceRegex: '' 122 | IndentAccessModifiers: false 123 | IndentCaseBlocks: false 124 | IndentCaseLabels: true 125 | IndentExternBlock: AfterExternBlock 126 | IndentGotoLabels: true 127 | IndentPPDirectives: None 128 | IndentRequiresClause: true 129 | IndentWidth: 2 130 | IndentWrappedFunctionNames: false 131 | InsertBraces: false 132 | InsertNewlineAtEOF: true 133 | InsertTrailingCommas: None 134 | IntegerLiteralSeparator: 135 | Binary: 0 136 | BinaryMinDigits: 0 137 | Decimal: 0 138 | DecimalMinDigits: 0 139 | Hex: 0 140 | HexMinDigits: 0 141 | JavaScriptQuotes: Leave 142 | JavaScriptWrapImports: true 143 | KeepEmptyLinesAtTheStartOfBlocks: false 144 | LambdaBodyIndentation: Signature 145 | LineEnding: DeriveLF 146 | MacroBlockBegin: '' 147 | MacroBlockEnd: '' 148 | MaxEmptyLinesToKeep: 1 149 | NamespaceIndentation: None 150 | ObjCBinPackProtocolList: Never 151 | ObjCBlockIndentWidth: 2 152 | ObjCBreakBeforeNestedBlockParam: true 153 | ObjCSpaceAfterProperty: false 154 | ObjCSpaceBeforeProtocolList: true 155 | PackConstructorInitializers: NextLine 156 | PenaltyBreakAssignment: 2 157 | PenaltyBreakBeforeFirstCallParameter: 1 158 | PenaltyBreakComment: 300 159 | PenaltyBreakFirstLessLess: 120 160 | PenaltyBreakOpenParenthesis: 0 161 | PenaltyBreakString: 1000 162 | PenaltyBreakTemplateDeclaration: 10 163 | PenaltyExcessCharacter: 1000000 164 | PenaltyIndentedWhitespace: 0 165 | PenaltyReturnTypeOnItsOwnLine: 200 166 | PointerAlignment: Left 167 | PPIndentWidth: -1 168 | QualifierAlignment: Leave 169 | RawStringFormats: 170 | - Language: Cpp 171 | Delimiters: 172 | - cc 173 | - CC 174 | - cpp 175 | - Cpp 176 | - CPP 177 | - 'c++' 178 | - 'C++' 179 | CanonicalDelimiter: '' 180 | BasedOnStyle: google 181 | - Language: TextProto 182 | Delimiters: 183 | - pb 184 | - PB 185 | - proto 186 | - PROTO 187 | EnclosingFunctions: 188 | - EqualsProto 189 | - EquivToProto 190 | - PARSE_PARTIAL_TEXT_PROTO 191 | - PARSE_TEST_PROTO 192 | - PARSE_TEXT_PROTO 193 | - ParseTextOrDie 194 | - ParseTextProtoOrDie 195 | - ParseTestProto 196 | - ParsePartialTestProto 197 | CanonicalDelimiter: pb 198 | BasedOnStyle: google 199 | ReferenceAlignment: Pointer 200 | ReflowComments: true 201 | RemoveBracesLLVM: false 202 | RemoveSemicolon: false 203 | RequiresClausePosition: OwnLine 204 | RequiresExpressionIndentation: OuterScope 205 | SeparateDefinitionBlocks: Leave 206 | ShortNamespaceLines: 1 207 | SortIncludes: CaseSensitive 208 | SortJavaStaticImport: Before 209 | SortUsingDeclarations: LexicographicNumeric 210 | SpaceAfterCStyleCast: false 211 | SpaceAfterLogicalNot: false 212 | SpaceAfterTemplateKeyword: true 213 | SpaceAroundPointerQualifiers: Default 214 | SpaceBeforeAssignmentOperators: true 215 | SpaceBeforeCaseColon: false 216 | SpaceBeforeCpp11BracedList: false 217 | SpaceBeforeCtorInitializerColon: true 218 | SpaceBeforeInheritanceColon: true 219 | SpaceBeforeParens: ControlStatements 220 | SpaceBeforeParensOptions: 221 | AfterControlStatements: true 222 | AfterForeachMacros: true 223 | AfterFunctionDefinitionName: false 224 | AfterFunctionDeclarationName: false 225 | AfterIfMacros: true 226 | AfterOverloadedOperator: false 227 | AfterRequiresInClause: false 228 | AfterRequiresInExpression: false 229 | BeforeNonEmptyParentheses: false 230 | SpaceBeforeRangeBasedForLoopColon: true 231 | SpaceBeforeSquareBrackets: false 232 | SpaceInEmptyBlock: false 233 | SpaceInEmptyParentheses: false 234 | SpacesBeforeTrailingComments: 2 235 | SpacesInAngles: Never 236 | SpacesInConditionalStatement: false 237 | SpacesInContainerLiterals: true 238 | SpacesInCStyleCastParentheses: false 239 | SpacesInLineCommentPrefix: 240 | Minimum: 1 241 | Maximum: -1 242 | SpacesInParentheses: false 243 | SpacesInSquareBrackets: false 244 | Standard: Auto 245 | StatementAttributeLikeMacros: 246 | - Q_EMIT 247 | StatementMacros: 248 | - Q_UNUSED 249 | - QT_REQUIRE_VERSION 250 | TabWidth: 8 251 | UseTab: Never 252 | WhitespaceSensitiveMacros: 253 | - BOOST_PP_STRINGIZE 254 | - CF_SWIFT_NAME 255 | - NS_SWIFT_NAME 256 | - PP_STRINGIZE 257 | - STRINGIZE 258 | ... 259 | 260 | --------------------------------------------------------------------------------