/* [ʞ] nkvd.c - XDG directory enforcer
* ~ lexi hale <lexi@hale.su>
* 🄯 GNU AGPL v3
* $ cc -fPIC -pie -shared nkvd.c -Wl,-E -onkvd -ldl
* [-D_NO_GNU [-D_LIBC=…]] [-D_CONF_HOME=…]
* $ export LD_PRELOAD=nkvd.so <dissident>
*
* ! NOTE: for unknown reasons, nkvd currently only works
* when built without optimizations. it's probably that
* wrecker Trotsky's fault. i'm working on it.
* - partial solution on GCC: using #pragma to disable
* optimization around the sensitive code. unclear if
* this bug affects other compilers.
* - bizarre complication: the bug can only be reproduced
* if the -O flag itself is present; turning on the
* same individual optimization flags that -O<x> is
* supposed to represent does not trigger the problem.
* - conclusion: compiler devils
*
* ! WARNING: test this program thoroughly before you start
* exporting it from your shellrcs; if things get fucked,
* you may not be able to execute any binaries with the
* variable set, and will need to log in as root or boot
* off a rescue disk to fix the problem. LD_PRELOAD is
* not a toy. you have been warned.
*
* ! WARNING: while the code *should* theoretically work
* with non-GNU libcs, i don't have access to any
* computers running under such a configuration, so i was
* only able to test that the compilation process doesn't
* explode. there are likely to be bugs. reports & merge
* requests especially are, as always, welcome.
*
* ? tired of programs disrespecting your XDG configuration
* and shitting all over home sweet ~ ? fear no more!
* nkvd is your very own digital thug to whip those
* dissidents into line like the counterrevolutionary
* scum they are. simply command LD to preload nkvd.so,
* and it will wiretap startup, then intercept any calls
* to open(2) (not just fopen) to make sure they
* point where they're supposed to. nkvd's behavior is
* controlled by a number of environment variables:
*
* - XDG_CONFIG_HOME specifies the redirection target,
* to, but can be overridden with nkvd_gulag. if
* neither are set, $HOME/.config will be used
* instead.
*
* - nkvd_hq specifies which directory to protect from
* cyberkulaks. it defaults to your home directory.
* attempts to access dotfiles or dotfolders with
* appropriate names in this directory will be
* redirected.
*
* - nkvd_subversives tells nkvd which programs to
* interdict. it is a colon-separated list of names.
* to determine if a program is on the List, nkvd
* will check the value of basename(argv[0]) against
* its members. if nkvd_subversives is unset, nkvd
* will interdict all programs except for system
* utilities like rm and ls. the first character of
* nkvd_subversives must be either "+", in which case
* it functions as a blacklist, or a "-", in which
* case it functions as a whitelist.
*
* - nkvd_interdict_all tells nkvd which dotfile
* accesses to redirect. by default, it will
* redirect access to files or folders whose paths
* begin with the string $HOME/.(basename(argv[0])),
* instead returning fds to $XDG_CONFIG_HOME/$1. if
* nkvd_interdict_all is set, it will redirect ALL
* accesses to files whose paths begin with
* $HOME/. this is a particularly extreme mode
* to operate nkvd in and it is liable to cause
* serious problems, in particular for any programs
* attempting to access files in ~/.config
* or ~/.local, another XDG directory. it is
* recommended that nkvd_interdict_all should only be
* used with a carefully selected blacklist!
*
* NOTE: if you are compiling nkvd.c for a libc other than
* glibc, be sure to pass the flag -D_NO_GNU to the
* compiler to ensure appropriate behavior at runtime.
*
* TODO: extend nkvd to enforce other XDG directories?
*
* TODO: build labor power, arm the workers, and smash the
* bourgeoisie to impose a dictatorship of the
* proletariat.
*
* TODO: figure out a way to deal with {open,fstat,unlink}at(2)
*
* TODO: do a better job of canonicalizing paths, in case
* anyone tries to be tricky. (alas, realpath() et al
* are not viable for these purposes as they only work
* for files already in existence.)
*
* TODO: -pie allows the program to be run as a standalone
* binary. exploit this; allow it to launch other
* binaries with itself as LD_PRELOAD? detecting this
* state of affairs will likely only be possible
* through checking if LD_PRELOAD is empty. due to the
* difficulty of determining a binary's path on linux,
* this may not be feasible. switch back to -shared
* and delete -Wl,-E if so.
*
* TODO: support a mappings file/variable for those programs
* too special to simply name their config file
* "~/.(argv[0])".
*
* TODO: exempt xdg dirs beginning with ~/. from proscription
* in nkvd_interdict_all=1 mode
*
* TODO: instead of function passthrough, alter environment
* to delete LD_PRELOAD and re-exec whitelisted apps
* without nkvd loading at all
*/
#include <stdarg.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <limits.h>
#include <stdlib.h>
#include <pwd.h>
#ifdef _NO_GNU
/* the POSIX version of basename is a goddamn mess and
* it's better to use the glibc version where possible. */
# include <libgen.h>
# define SYMSRC hnd
#else
/* glibc's basename() is imported from <string.h> via
* the following directive. i have no idea how this
* magic works unless they're abusing asm declarations */
# define _GNU_SOURCE /* for basename() */
# define __USE_GNU /* for RTLD_NEXT */
# ifdef _USE_RTLD_NEXT
# define SYMSRC RTLD_NEXT
# else
# define SYMSRC hnd
# endif
#endif
#include <string.h>
#include <dlfcn.h>
#ifndef _LIBC
# define _LIBC "libc.so.6"
#endif
#ifndef _CONF_HOME
# define _CONF_HOME ".config"
#endif
#define pstr(x) x,sizeof x
#define hl(txt) "\x1b[1;31m" txt "\x1b[m"
#define bold(txt) "\x1b[1m" txt "\x1b[21m"
#define e_fatal hl("(nkvd fatal)") " "
#define fail(code, err) { write(2,pstr(e_fatal " " err)); _exit(code); }
#define dbg(x) write(2,pstr(bold("(nkvd)") " " x))
typedef _Bool bool;
enum { false = 0, true = 1 };
static bool intercept;
static int wiretap_argc = 0;
static const char** wiretap_argv = NULL;
static const char* redir_prefix;
static const char* redir_to;
static size_t redir_len;
static const char* hq;
static size_t hq_len;
static void
configdirs(void) {
const char* nkvd = getenv("nkvd_gulag");
if (nkvd != NULL) redir_to = nkvd, redir_len = strlen(nkvd);
const char* xdg = getenv("XDG_CONFIG_HOME");
if (xdg != NULL) redir_to = xdg, redir_len = strlen(xdg);
const char* home = getenv("HOME");
if (home == NULL) {
const char* user = getenv("USER");
struct passwd* p;
if (user == NULL) {
uid_t u = geteuid();
p = getpwuid(u);
} else
p = getpwnam(user);
if(p == NULL) fail(-5, "XDG_CONFIG_HOME, HOME, and USER "
"environment variables are all unset, and no home "
"directory can be identified for your user ID. "
"something is seriously wrong with your system, "
"comrade. bailing.");
home = p -> pw_dir;
}
size_t homelen = strlen(home);
const char* h = getenv("nkvd_hq");
if (h != NULL) hq = h, hq_len = strlen(h);
else hq = strdup(home), hq_len = homelen;
# define CONFDIR "/" _CONF_HOME "/"
char* buf = malloc(homelen + sizeof CONFDIR);
/* since this will be used throughout the lifetime of the
* program, we don't need to free it, and it would actually
* just slow down the exit process to do so. */
strcpy(buf, home);
strcpy(buf + homelen, CONFDIR);
redir_len = homelen + sizeof CONFDIR;
# undef CONFDIR
redir_to = buf;
}
static bool
checkpath(const char* path, size_t len, char* gulag) {
char c[hq_len + len + 4];
strncpy(c, hq, hq_len);
c[hq_len] = '/';
c[hq_len+1] = '.';
c[hq_len+2] = 0;
const char* all = getenv("nkvd_interdict_all");
size_t clen;
if (all == NULL || !(all[0] == '1' && all[1] == 0)) {
strcpy(c + hq_len + 2, wiretap_argv[0]);
clen = strlen(c);
c[clen] = '/'; c[clen+1] = 0;
if (len > clen) ++clen;
} else {
clen = strlen(c);
}
if (strncmp(path, c, clen) == 0) {
/* guilty as sin, your honor */
strncpy(gulag,redir_to,redir_len);
strncpy(gulag + redir_len - 1,
path + hq_len + 2,
len - (hq_len + 1));
return true;
} else {
/* no further questions, citizen */
return false;
}
}
static bool
interrogate(const char* path, size_t plen, char* gulag) {
/* papers, please */
if (path[0] != '/') for (size_t i = 64; i<PATH_MAX; i << 1) {
char cwd[i + plen + 1];
if (getcwd(cwd, i) == NULL) continue;
size_t cwdlen = strlen(cwd);
cwd[cwdlen] = '/';
strncpy(cwd + cwdlen + 1, path, plen + 1);
return checkpath(cwd,cwdlen + plen, gulag);
}
return checkpath(path, plen, gulag);
}
static bool
shitlist(const char* name) {
# ifdef _NO_GNU /* avoid POSIX weirdness */
{ char* name = strdup(name); /* yay shadowing! */
# endif
const char* base = basename(name);
const char* list = getenv("nkvd_subversives");
if (list == NULL) return true;
if (list[0] != '+' && list[0] != '-')
fail(-4, "the variable $nkvd_subversives needs to "
"begin with either a + to indicate blacklist "
"mode, or a - to indicate whitelist mode.");
const enum { blacklist, whitelist } mode =
(list[0] == '+' ? blacklist : whitelist);
const char* p = list + 1;
for(;;) {
# ifndef _NO_GNU
const char* end = strchrnul(p, ':');
# else
const char* end = strchr(p, ':');
if (end == NULL) end = p + strlen(p);
# endif
if(strncmp(base,p,end-p) == 0) return (mode == blacklist);
if (*end == 0) break;
else p = end + 1;
}
# ifdef _NO_GNU
free(name); }
# endif
return (mode == whitelist);
}
#pragma GCC push_options
#pragma GCC optimize ("O0")
#define STAT_PARAMS const char* volatile path, struct stat* volatile statbuf
#define XSTAT_PARAMS int volatile ver, const char* volatile path, struct stat* volatile statbuf
#define decl_stat_ptr(nm) static int (*libc_##nm) (STAT_PARAMS)
#define decl_xstat_ptr(nm) static int (*libc_##nm) (XSTAT_PARAMS)
static int (*libc_open) (const char* volatile,int,...);
static int (*libc_unlink) (const char* volatile);
static int (*libc_access) (const char* volatile,int);
static int (*libc_eaccess)(const char* volatile,int);
static int (*libc_rename) (const char* volatile,const char* volatile);
decl_stat_ptr(stat);
decl_stat_ptr(stat64);
decl_stat_ptr(lstat);
decl_stat_ptr(lstat64);
#ifndef _NO_GNU
decl_xstat_ptr(xstat);
decl_xstat_ptr(xstat64);
decl_xstat_ptr(lxstat);
decl_xstat_ptr(lxstat64);
#endif
static int
nkvd_stat(int (*volatile fn)(STAT_PARAMS), STAT_PARAMS) {
if (intercept) {
size_t plen = strlen(path);
char gulag[redir_len + plen];
if (interrogate(path, plen, gulag)) {
return (*fn)(gulag,statbuf);
}
}
return (*fn)(path,statbuf);
}
static int
nkvd_xstat(int (*volatile fn)(XSTAT_PARAMS), XSTAT_PARAMS) {
if (intercept) {
size_t plen = strlen(path);
char gulag[redir_len + plen];
if (interrogate(path, plen, gulag)) {
return (*fn)(ver,gulag,statbuf);
}
}
return (*fn)(ver,path,statbuf);
}
#define def_stat_fn(nm,fn) int nm(STAT_PARAMS) { return nkvd_stat(libc_##fn,path,statbuf); }
#define def_xstat_fn(nm,fn) int nm(XSTAT_PARAMS) { return nkvd_xstat(libc_##fn,ver,path,statbuf); }
def_stat_fn(stat,stat);
def_stat_fn(stat64,stat64);
def_stat_fn(lstat,lstat);
def_stat_fn(lstat64,lstat64);
#ifndef _NO_GNU
def_xstat_fn(__xstat,xstat);
def_xstat_fn(__xstat64,xstat64);
def_xstat_fn(__lxstat,lxstat);
def_xstat_fn(__lxstat64,lxstat64);
#endif
#pragma GCC pop_options
int unlink(const char* volatile path) {
if (intercept) {
size_t plen = strlen(path);
char gulag[redir_len + plen];
if (interrogate(path, plen, gulag)) {
return (*libc_unlink)(gulag);
}
}
return (*libc_unlink)(path);
};
typedef int(*intfn)(const char*,int);
static int
nkvd_intcall(intfn fn, const char* volatile path, int mode) {
if (intercept) {
size_t plen = strlen(path);
char gulag[redir_len + plen];
if (interrogate(path, plen, gulag)) {
return (*fn)(gulag,mode);
}
}
return (*fn)(path,mode);
};
int access (const char* volatile path, int mode)
{ nkvd_intcall(libc_access,path,mode); }
int eaccess (const char* volatile path, int mode)
{ nkvd_intcall(libc_eaccess,path,mode); }
#define max(a,b) ((a) > (b) ? (a) : (b))
int rename(const char* volatile from,
const char* volatile to) {
if (intercept) {
const size_t fromlen = strlen(from),
tolen = strlen(from);
char fromgulag[redir_len + tolen ],
togulag[redir_len + fromlen];
const char* fromfinal,
* tofinal;
if (interrogate(from, fromlen, fromgulag))
fromfinal = fromgulag;
else fromfinal = from;
if (interrogate(to, tolen, togulag))
tofinal = togulag;
else tofinal = to;
return (*libc_rename)(fromfinal,tofinal);
}
return (*libc_rename)(from,to);
};
int open(const char* volatile path, int flags, ...) {
/* fuck you for using varargs, asshole */
va_list v; va_start(v,flags);
mode_t m;
if(flags & O_CREAT) m = va_arg(v, mode_t);
va_end(v);
if (intercept) {
size_t plen = strlen(path);
char gulag[redir_len + plen];
if (interrogate(path, plen, gulag)) {
return (*libc_open)(gulag, flags, m);
}
}
return (*libc_open)(path, flags, m);
}
#include <stdio.h>
int nkvd_init(int argc, const char** argv) {
wiretap_argc = argc;
wiretap_argv = argv;
# ifndef _USE_RTLD_NEXT
/* RTLD_NEXT is buggy as hell so we avoid it by default */
void* SYMSRC = dlopen(_LIBC,RTLD_LAZY);
if (SYMSRC == NULL)
fail(-2,"compiled in non-RTLD_NEXT mode and cannot load "
"libc from " bold(_LIBC) "; please recompile "
" and specify your libc with -D_LIBC= on the "
"compile command line.");
# endif
# define load_fn(fn,pfx) libc_##fn = dlsym(SYMSRC, #pfx #fn)
load_fn(open,); load_fn(unlink,);
load_fn(access,); load_fn(eaccess,);
load_fn(rename,);
load_fn(stat,); load_fn(lstat,);
load_fn(stat64,); load_fn(lstat64,);
# ifndef _NO_GNU
load_fn(xstat,__); load_fn(lxstat,__);
load_fn(xstat64,__); load_fn(lxstat64,__);
# endif
# undef load_fn
# define dump(v) printf("-- " #v ": %p", libc_##v)
dump(lstat);
dump(lstat64);
dump(lxstat);
dump(lxstat64);
/* have enough stat fns been found for us to proceed? */
bool statfns = ( (libc_stat && libc_lstat)
|| (libc_stat64 && libc_lstat64)
#ifndef _NO_GNU
|| (libc_xstat && libc_lxstat)
|| (libc_xstat64 && libc_lxstat64)
#endif
);
if (! (libc_open && statfns && libc_unlink &&
libc_access && libc_eaccess && libc_rename))
fail(-3, "your libc is defective. bailing.");
intercept = shitlist(argv[0]);
if (intercept) {
configdirs();
}
return 0; /* ?? why is this int, not void */
}
__attribute__((section(".init_array"))) static void* nkvd_constructor = &nkvd_init;
int main() { return 0; } /* stub for -pie */