Skip to content

Instantly share code, notes, and snippets.

@simonlindholm
Created August 31, 2025 14:18
Show Gist options
  • Select an option

  • Save simonlindholm/3691cfdd038c19fab873ab4b59f20bb2 to your computer and use it in GitHub Desktop.

Select an option

Save simonlindholm/3691cfdd038c19fab873ab4b59f20bb2 to your computer and use it in GitHub Desktop.
// 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