Created
August 31, 2025 14:18
-
-
Save simonlindholm/3691cfdd038c19fab873ab4b59f20bb2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // gcc -shared -fPIC -O2 pathguard.c -o libpathguard.so -ldl | |
| // ALLOW_PATHS="/proc:/dev/null:/usr:." LD_PRELOAD="$PWD/libpathguard.so" ./program | |
| #define _GNU_SOURCE | |
| #include <dlfcn.h> | |
| #include <errno.h> | |
| #include <fcntl.h> | |
| #include <limits.h> | |
| #include <pthread.h> | |
| #include <stdarg.h> | |
| #include <stdbool.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <sys/stat.h> | |
| #include <sys/types.h> | |
| #include <unistd.h> | |
| static int (*real_open)(const char *, int, ...) = NULL; | |
| static int (*real_open64)(const char *, int, ...) = NULL; | |
| static int (*real_openat)(int, const char *, int, ...) = NULL; | |
| static int (*real_openat64)(int, const char *, int, ...) = NULL; | |
| static FILE *(*real_fopen)(const char *, const char *) = NULL; | |
| static FILE *(*real_fopen64)(const char *, const char *) = NULL; | |
| static pthread_once_t init_once = PTHREAD_ONCE_INIT; | |
| static __thread int reentry = 0; // thread-local reentry guard | |
| static int silent = 0; | |
| // --- Utilities -------------------------------------------------------------- | |
| static void init_syms(void) { | |
| real_open = dlsym(RTLD_NEXT, "open"); | |
| real_openat = dlsym(RTLD_NEXT, "openat"); | |
| real_fopen = dlsym(RTLD_NEXT, "fopen"); | |
| // Optional variants (may be NULL depending on libc) | |
| real_open64 = dlsym(RTLD_NEXT, "open64"); | |
| real_openat64 = dlsym(RTLD_NEXT, "openat64"); | |
| real_fopen64 = dlsym(RTLD_NEXT, "fopen64"); | |
| const char *s = getenv("ALLOW_SILENT"); | |
| silent = (s && *s && strcmp(s, "0") != 0) ? 1 : 0; | |
| } | |
| static inline void ensure_init(void) { | |
| pthread_once(&init_once, init_syms); | |
| } | |
| static void log_block(const char *op, const char *path) { | |
| if (!silent && !reentry) { | |
| dprintf(STDERR_FILENO, "[pathguard] blocked %s: %s (EACCES)\n", op, path ? path : "(null)"); | |
| } | |
| } | |
| // Join base + "/" + leaf into out (size outsz). Returns out or NULL. | |
| static char *join_path(char *out, size_t outsz, const char *base, const char *leaf) { | |
| if (!base || !leaf) return NULL; | |
| size_t bl = strlen(base); | |
| if (bl == 0) return NULL; | |
| int need_slash = (base[bl - 1] != '/'); | |
| if (snprintf(out, outsz, "%s%s%s", base, need_slash ? "/" : "", leaf) >= (int)outsz) | |
| return NULL; | |
| return out; | |
| } | |
| // Get absolute path for dirfd (directory) into buf using /proc/self/fd/<n>. | |
| // Returns buf on success, else NULL. | |
| static char *dirfd_abspath(int dirfd, char *buf, size_t bufsz) { | |
| if (dirfd == AT_FDCWD) { | |
| return getcwd(buf, bufsz); | |
| } | |
| char linkpath[64]; | |
| snprintf(linkpath, sizeof(linkpath), "/proc/self/fd/%d", dirfd); | |
| ssize_t n = readlink(linkpath, buf, bufsz - 1); | |
| if (n < 0) return NULL; | |
| buf[n] = '\0'; | |
| return buf; | |
| } | |
| // Best-effort canonicalization: | |
| // - If path exists, use realpath() fully. | |
| // - If it doesn't, try to realpath() the parent dir and then append the last component. | |
| // Writes result into out (size outsz). Returns out or NULL. | |
| static char *canonicalize(const char *in, char *out, size_t outsz) { | |
| if (!in || !*in) return NULL; | |
| // Guard re-entrant libc file ops that realpath might do. | |
| reentry++; | |
| char tmp[PATH_MAX]; | |
| if (realpath(in, tmp) != NULL) { | |
| reentry--; | |
| if (strlen(tmp) >= outsz) return NULL; | |
| strcpy(out, tmp); | |
| return out; | |
| } | |
| reentry--; | |
| // If we’re here, the full path may not exist. Resolve parent. | |
| char work[PATH_MAX]; | |
| strncpy(work, in, sizeof(work) - 1); | |
| work[sizeof(work) - 1] = '\0'; | |
| char *slash = strrchr(work, '/'); | |
| if (!slash) { | |
| // No slash => relative single component; resolve CWD. | |
| char cwd[PATH_MAX]; | |
| if (!getcwd(cwd, sizeof(cwd))) return NULL; | |
| if (!join_path(out, outsz, cwd, in)) return NULL; | |
| return out; | |
| } | |
| // Separate parent and leaf. | |
| *slash = '\0'; | |
| const char *leaf = slash + 1; | |
| reentry++; | |
| char parent[PATH_MAX]; | |
| if (realpath(work, parent) == NULL) { | |
| // Parent could also be missing; just stitch original back. | |
| reentry--; | |
| if (strlen(in) >= outsz) return NULL; | |
| strcpy(out, in); | |
| return out; | |
| } | |
| reentry--; | |
| if (!join_path(out, outsz, parent, leaf)) return NULL; | |
| return out; | |
| } | |
| static bool path_allowed(const char *abs_path) { | |
| // dprintf(STDERR_FILENO, "[pathguard] checking %s\n", abs_path); | |
| const char *allow = getenv("ALLOW_PATHS"); | |
| if (!allow || !*allow) { | |
| // If unset, default to DENY ALL (safer). | |
| return false; | |
| } | |
| // Iterate colon-separated prefixes. | |
| const char *p = allow; | |
| while (*p) { | |
| // Extract next token | |
| const char *colon = strchr(p, ':'); | |
| size_t len = colon ? (size_t)(colon - p) : strlen(p); | |
| if (len > 0) { | |
| // Copy token to buf | |
| char pref[PATH_MAX]; | |
| if (len >= sizeof(pref)) len = sizeof(pref) - 1; | |
| memcpy(pref, p, len); | |
| pref[len] = '\0'; | |
| // If prefix isn't canonical, canonicalize it so symlinks don't bypass. | |
| char pref_canon[PATH_MAX]; | |
| const char *pref_cmp = pref; | |
| if (canonicalize(pref, pref_canon, sizeof(pref_canon))) { | |
| pref_cmp = pref_canon; | |
| } | |
| size_t pl = strlen(pref_cmp); | |
| // Ensure boundary: "/"-aware prefix match (so "/foo" doesn't match "/foobar") | |
| if (pl == 1 && pref_cmp[0] == '/') { | |
| // root => allow everything (probably not desired, but supported) | |
| return true; | |
| } | |
| if (strncmp(abs_path, pref_cmp, pl) == 0 && | |
| (abs_path[pl] == '/' || abs_path[pl] == '\0')) { | |
| return true; | |
| } | |
| } | |
| p = colon ? colon + 1 : p + len; | |
| if (!colon) break; | |
| } | |
| return false; | |
| } | |
| // Normalize a path relative to dirfd (if needed) into absolute form. | |
| // Returns out or NULL. `opname` only used for logging on failure. | |
| static char *resolve_against_dirfd(int dirfd, const char *path, char *out, size_t outsz, const char *opname) { | |
| if (!path) return NULL; | |
| if (path[0] == '/') { | |
| // Absolute already | |
| return canonicalize(path, out, outsz); | |
| } | |
| char base[PATH_MAX]; | |
| if (!dirfd_abspath(dirfd, base, sizeof(base))) { | |
| if (!silent) dprintf(STDERR_FILENO, "[pathguard] WARN: %s failed to resolve base dir; denying\n", opname); | |
| return NULL; | |
| } | |
| char joined[PATH_MAX]; | |
| if (!join_path(joined, sizeof(joined), base, path)) return NULL; | |
| return canonicalize(joined, out, outsz); | |
| } | |
| static bool decide_allow(int dirfd, const char *path, const char *opname) { | |
| if (!path) return true; // let libc handle NULL errors | |
| char absbuf[PATH_MAX]; | |
| if (!resolve_against_dirfd(dirfd, path, absbuf, sizeof(absbuf), opname)) { | |
| // If we cannot resolve, be conservative: deny | |
| return false; | |
| } | |
| return path_allowed(absbuf); | |
| } | |
| // --- Hooked functions ------------------------------------------------------- | |
| int open(const char *pathname, int flags, ...) { | |
| ensure_init(); | |
| mode_t mode = 0; | |
| if (flags & O_CREAT) { | |
| va_list ap; | |
| va_start(ap, flags); | |
| mode = va_arg(ap, int); | |
| va_end(ap); | |
| } | |
| if (!reentry && !decide_allow(AT_FDCWD, pathname, "open")) { | |
| log_block("open", pathname); | |
| errno = EACCES; | |
| return -1; | |
| } | |
| reentry++; | |
| int rc = (real_open ? real_open : ((int (*)(const char *, int, ...))dlsym(RTLD_NEXT, "open")))(pathname, flags, mode); | |
| reentry--; | |
| return rc; | |
| } | |
| int openat(int dirfd, const char *pathname, int flags, ...) { | |
| ensure_init(); | |
| mode_t mode = 0; | |
| if (flags & O_CREAT) { | |
| va_list ap; | |
| va_start(ap, flags); | |
| mode = va_arg(ap, int); | |
| va_end(ap); | |
| } | |
| if (!reentry && !decide_allow(dirfd, pathname, "openat")) { | |
| log_block("openat", pathname); | |
| errno = EACCES; | |
| return -1; | |
| } | |
| reentry++; | |
| int rc = (real_openat ? real_openat : ((int (*)(int, const char *, int, ...))dlsym(RTLD_NEXT, "openat")))(dirfd, pathname, flags, mode); | |
| reentry--; | |
| return rc; | |
| } | |
| FILE *fopen(const char *pathname, const char *mode) { | |
| ensure_init(); | |
| if (!reentry && !decide_allow(AT_FDCWD, pathname, "fopen")) { | |
| log_block("fopen", pathname); | |
| errno = EACCES; | |
| return NULL; | |
| } | |
| reentry++; | |
| FILE *f = (real_fopen ? real_fopen : ((FILE *(*)(const char *, const char *))dlsym(RTLD_NEXT, "fopen")))(pathname, mode); | |
| reentry--; | |
| return f; | |
| } | |
| // --- Optional 64-bit/glibc variants (compile-time present on many systems) --- | |
| int open64(const char *pathname, int flags, ...) { | |
| ensure_init(); | |
| mode_t mode = 0; | |
| if (flags & O_CREAT) { | |
| va_list ap; | |
| va_start(ap, flags); | |
| mode = va_arg(ap, int); | |
| va_end(ap); | |
| } | |
| if (!reentry && !decide_allow(AT_FDCWD, pathname, "open64")) { | |
| log_block("open64", pathname); | |
| errno = EACCES; | |
| return -1; | |
| } | |
| reentry++; | |
| int rc = (real_open64 ? real_open64 : ((int (*)(const char *, int, ...))dlsym(RTLD_NEXT, "open64")))(pathname, flags, mode); | |
| reentry--; | |
| return rc; | |
| } | |
| int openat64(int dirfd, const char *pathname, int flags, ...) { | |
| ensure_init(); | |
| mode_t mode = 0; | |
| if (flags & O_CREAT) { | |
| va_list ap; | |
| va_start(ap, flags); | |
| mode = va_arg(ap, int); | |
| va_end(ap); | |
| } | |
| if (!reentry && !decide_allow(dirfd, pathname, "openat64")) { | |
| log_block("openat64", pathname); | |
| errno = EACCES; | |
| return -1; | |
| } | |
| reentry++; | |
| int rc = (real_openat64 ? real_openat64 : ((int (*)(int, const char *, int, ...))dlsym(RTLD_NEXT, "openat64")))(dirfd, pathname, flags, mode); | |
| reentry--; | |
| return rc; | |
| } | |
| FILE *fopen64(const char *pathname, const char *mode) { | |
| ensure_init(); | |
| if (!reentry && !decide_allow(AT_FDCWD, pathname, "fopen64")) { | |
| log_block("fopen64", pathname); | |
| errno = EACCES; | |
| return NULL; | |
| } | |
| reentry++; | |
| FILE *f = (real_fopen64 ? real_fopen64 : ((FILE *(*)(const char *, const char *))dlsym(RTLD_NEXT, "fopen64")))(pathname, mode); | |
| reentry--; | |
| return f; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment