commit 78e0e5994fd7e9d71aabefd532b856c3d6edfe51
parent d6c117fce77f008779a609f3887ad89b44937278
Author: Richard Ipsum <richardipsum@fastmail.co.uk>
Date: Sun, 6 Jul 2025 15:19:21 +0100
feat: revive and modernize core logic (WIP branch since 2024 :D)
This commit includes a large set of changes aimed at improving or restoring the
usefulness of the project after a long hiatus. Main changes (to the best of my recollection):
- handle "new" statx call which is being used by e.g. GNU ls
- improve coverage of existing calls, now supporting: stat, lstat,
fstat, fstatat, chown, lchown, fchown, fchownat, chmod
- Fix bug where we were not correctly sending file descriptors between
processes (and replaced read/write loop on socket with sendmsg)
- General cleanup
- Completely remove the use of syscall(). This was required because of
OpenBSD making it a requirement to only call syscalls through their
libc functions (
https://marc.info/?l=openbsd-tech&m=169841790407370&w=2 ) but it also
is just a lot simpler and means there is no longer limit of only
supporting amd64 because of hardcoded syscall numbers.
- Add tests to ensure we are covering listed calls
NOTE: This commit revives work started in 2024 and contains accumulated
work-in-progress changes. Some areas may need cleanup or review,
but I am hoping if anyone stumbles on this it is more useful with these
changes pushed than without!
Diffstat:
18 files changed, 796 insertions(+), 227 deletions(-)
diff --git a/Makefile b/Makefile
@@ -1,8 +1,15 @@
PREFIX ?= /usr/local
+# I'm assuming most Linux systems have the "new" statx call now,
+# https://www.man7.org/linux/man-pages/man2/statx.2.html
+# but if you're compiling on Linux and get errors about statx
+# set HAVE_STATX = 0
+HAVE_STATX = 1
+
all: libsfakeroot.so sfakeroot
-CFLAGS=-Wall -Wextra -Werror -pedantic -std=c99 -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE -D_BSD_SOURCE -DSFAKEROOT_LIBDIR=\"$(DESTDIR)$(PREFIX)/lib\"
+CC=gcc
+CFLAGS=-Wall -Wextra -pedantic -std=c99 -D_DEFAULT_SOURCE -DSFAKEROOT_LIBDIR=\"$(DESTDIR)$(PREFIX)/lib\" -D_GNU_SOURCE -DHAVE_STATX=$(HAVE_STATX) -g
LDFLAGS=-L .
libsfakeroot.o: libsfakeroot.c
@@ -15,7 +22,9 @@ libsfakeroot.so: libsfakeroot.o strlcpy.o
$(CC) -fPIC $(CFLAGS) $(LDFLAGS) -shared -o $@ libsfakeroot.o strlcpy.o
sfakeroot: sfakeroot.o strlcpy.o
- $(CC) $(CFLAGS) $(LDFLAGS) -o $@ sfakeroot.o strlcpy.o -l sfakeroot
+ $(CC) $(CFLAGS) $(LDFLAGS) -o $@ sfakeroot.o strlcpy.o
+
+sfakeroot.o: sfakeroot.c sfakeroot.h
install:
install sfakeroot $(DESTDIR)$(PREFIX)/bin
diff --git a/README b/README
@@ -1,25 +1,43 @@
sfakeroot
=========
-sfakeroot is a minimalist fakeroot[1]. It uses the same LD_PRELOAD tricks
-as fakeroot to provide an environment in which stat() and related calls
+sfakeroot is a minimalist fakeroot[1]. It uses LD_PRELOAD
+to provide an environment in which stat() and related calls
return uid/gid 0, so that files appear to be owned by root even when they're
not. Inside the fakeroot environment permissions can be modified as if the
current user is root, though ownership of the underlying files is never
really changed. Any permissions set within the fakeroot environment will
last only for the duration of the session, once sfakeroot exits all changes
-are lost.
+are lost. If you need to preserve session state between calls you can tell sfakeroot to save the session to a file, which can be loaded again when sfakeroot runs. See man page for details.
Why?
----
sfakeroot was written because there is no OpenBSD compatible fakeroot
equivalent, I considered porting fakeroot but it seemed too difficult.
+On Linux you will probably just use docker, but this could be a useful
+tool if docker is not an option.
+
+Currently supported calls
+--------------------------
+
+This is a list of calls that will currently be intercepted by sfakeroot:
+
+* stat
+* lstat
+* fstat
+* fstatat
+* chown
+* lchown
+* fchown
+* fchownat
+* chmod
+* statx
Installation
------------
-To build just run:
+To build run:
$ make
@@ -61,3 +79,9 @@ To make better use of sfakeroot on OpenBSD you may wish to use it with sbase[2].
[1]: http://freshmeat.sourceforge.net/projects/fakeroot
[2]: https://git.suckless.org/sbase/
+
+TODO
+----
+
+* Support fts and ftw functions.
+* Support fchmod fchmodat
+\ No newline at end of file
diff --git a/libsfakeroot.c b/libsfakeroot.c
@@ -1,6 +1,6 @@
/* sfakeroot
*
- * Copyright © 2020 Richard Ipsum
+ * Copyright © 2020 - 2025 Richard Ipsum
*
* This file is part of sfakeroot.
*
@@ -31,126 +31,33 @@
#include <sys/un.h>
#include "sfakeroot.h"
-
-int sfakeroot_readwriten(int fd, char bytes[], size_t len, bool is_write)
-{
- ssize_t n;
- size_t remaining = len;
-
- while (remaining > 0) {
- n = is_write ? write(fd, bytes, remaining) : read(fd, bytes, remaining);
- switch (n) {
- case 0:
- if (is_write) {
- /* not necessarily an error if we're writing... */
- continue;
- }
- fprintf(stderr, "sfakeroot: %s: data truncated, remaining: %zu\n",
- is_write ? "write" : "read", remaining);
- return -1;
- case -1:
- if (errno == EINTR) {
- continue;
- }
- fprintf(stderr, "sfakeroot: %s: %s\n",
- is_write ? "write" : "read", strerror(errno));
- return -1;
- default:
- remaining -= n;
- bytes += n;
- }
- }
-
- return len - remaining;
-}
-
-int sfakeroot_recvmsg(int fd, struct sfakeroot_msg *m)
-{
- if (sfakeroot_readwriten(fd, (char *) m, sizeof (*m), false) != sizeof (*m)) {
- fprintf(stderr, "sfakeroot_recvmsg: error reading message\n");
- return -1;
- }
-
- return 0;
-}
-
-int sfakeroot_sendmsg(int fd, struct sfakeroot_msg *m)
-{
- if (sfakeroot_readwriten(fd, (char *) m, sizeof (*m), true) != sizeof (*m)) {
- fprintf(stderr, "sfakeroot_sendmsg: error writing message\n");
- return -1;
- }
-
- return 0;
-}
-
-static int sfakeroot__session_open_internal(bool session_expected)
-{
- struct sockaddr_un sa = {.sun_family = AF_UNIX};
- int sockfd;
- socklen_t socklen;
- char *sockpath = getenv("SFAKEROOT_SOCKET_PATH");
-
- if (sockpath == NULL) {
- fprintf(stderr, "environment variable SFAKEROOT_SOCKET_PATH not set!\n");
- return -1;
- }
-
- sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
- if (sockfd == -1) {
- fprintf(stderr, "socket: %s\n", strerror(errno));
- return -1;
- }
-
- strlcpy(sa.sun_path, sockpath, sizeof (sa.sun_path));
- socklen = strlen(sa.sun_path) + 1 + sizeof (sa.sun_family);
-
- if (connect(sockfd, (struct sockaddr *) &sa, socklen) == -1) {
- if (session_expected) {
- fprintf(stderr, "connect: %s\n", strerror(errno));
- }
- return -1;
- }
-
- return sockfd;
-}
-
-int sfakeroot_session_open(void)
-{
- return sfakeroot__session_open_internal(true);
-}
-
-bool sfakeroot_daemon_running(void)
-{
- int sockfd = sfakeroot__session_open_internal(false);
- close(sockfd);
- return sockfd != -1;
-}
+#include "sfakeroot_inline.h"
static int sfakeroot__call(struct sfakeroot_msg *m)
{
int sockfd;
+ int sendfds[1];
char *wd;
sockfd = sfakeroot_session_open();
if (sockfd == -1) {
return -1;
}
-
if ((wd = getcwd(NULL, 0)) == NULL) {
return -1;
}
strlcpy(m->working_dir, wd, sizeof (m->working_dir));
free(wd);
-
- if (sfakeroot_sendmsg(sockfd, m) == -1) {
+ if (m->fd >= 0) {
+ sendfds[0] = m->fd;
+ }
+ if (sfakeroot_sendmsg(sockfd, m, m->fd >= 0 ? sendfds : NULL, m->fd >= 0 ? 1 : 0) == -1) {
return -1;
}
if (sfakeroot_recvmsg(sockfd, m) == -1) {
return -1;
}
-
close(sockfd);
errno = m->reterrno;
return m->retcode;
@@ -158,7 +65,7 @@ static int sfakeroot__call(struct sfakeroot_msg *m)
int chmod(const char *path, mode_t mode)
{
- struct sfakeroot_msg m = {.type = SFAKEROOT_MSG_CHMOD, .mode = mode};
+ struct sfakeroot_msg m = {.type = SFAKEROOT_MSG_CHMOD, .mode = mode, .fd = -1};
debug("chmod %s %o\n", path, mode);
strlcpy(m.path, path, sizeof (m.path));
return sfakeroot__call(&m);
@@ -166,7 +73,7 @@ int chmod(const char *path, mode_t mode)
int chown(const char *path, uid_t uid, gid_t gid)
{
- struct sfakeroot_msg m = {.type = SFAKEROOT_MSG_CHOWN, .uid = uid, .gid = gid};
+ struct sfakeroot_msg m = {.type = SFAKEROOT_MSG_CHOWN, .uid = uid, .gid = gid, .fd = -1};
strlcpy(m.path, path, sizeof (m.path));
debug("chown: path: %s, uid: %d, gid: %d\n", path, (int) uid, (int) gid);
return sfakeroot__call(&m);
@@ -174,7 +81,7 @@ int chown(const char *path, uid_t uid, gid_t gid)
int lchown(const char *path, uid_t uid, gid_t gid)
{
- struct sfakeroot_msg m = {.type = SFAKEROOT_MSG_LCHOWN, .uid = uid, .gid = gid};
+ struct sfakeroot_msg m = {.type = SFAKEROOT_MSG_LCHOWN, .uid = uid, .gid = gid, .fd = -1};
strlcpy(m.path, path, sizeof (m.path));
debug("lchown: path: %s, uid: %d, gid: %d\n", path, (int) uid, (int) gid);
return sfakeroot__call(&m);
@@ -217,7 +124,7 @@ static int sfakeroot__stat(struct sfakeroot_msg *m, struct stat *s)
int stat(const char *path, struct stat *s)
{
- struct sfakeroot_msg m = {.type = SFAKEROOT_MSG_STAT};
+ struct sfakeroot_msg m = {.type = SFAKEROOT_MSG_STAT, .fd = -1};
debug("stat\n");
strlcpy(m.path, path, sizeof (m.path));
return sfakeroot__stat(&m, s);
@@ -231,7 +138,7 @@ int __xstat(int ver, const char *path, struct stat *s)
int lstat(const char *path, struct stat *s)
{
- struct sfakeroot_msg m = {.type = SFAKEROOT_MSG_LSTAT};
+ struct sfakeroot_msg m = {.type = SFAKEROOT_MSG_LSTAT, .fd = -1};
debug("lstat\n");
strlcpy(m.path, path, sizeof (m.path));
return sfakeroot__stat(&m, s);
@@ -266,6 +173,26 @@ int fstatat(int fd, const char *path, struct stat *s, int flag)
return sfakeroot__stat(&m, s);
}
+#if USE_STATX
+int statx(int dirfd, const char *path, int flags, unsigned int mask,
+ struct statx *statxbuf)
+{
+ struct sfakeroot_msg m = {
+ .type = SFAKEROOT_MSG_STATX,
+ .fd = dirfd,
+ .flag = flags,
+ .mask = mask
+ };
+ int ret;
+ strlcpy(m.path, path, sizeof (m.path));
+ if ((ret = sfakeroot__call(&m)) == -1) {
+ return -1;
+ }
+ *statxbuf = m.statxbuf;
+ return ret;
+}
+#endif
+
#undef __xstat
#undef __fxstat
#undef __lxstat
diff --git a/sfakeroot.c b/sfakeroot.c
@@ -1,6 +1,6 @@
/* sfakeroot: manipulate files faking root privileges
*
- * Copyright © 2020 Richard Ipsum
+ * Copyright © 2020 - 2025 Richard Ipsum
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,6 +16,9 @@
*
*/
+#include <syslog.h>
+#include <stdarg.h>
+
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
@@ -31,6 +34,7 @@
#include <sys/un.h>
#include <signal.h>
+#include "sfakeroot_inline.h"
#include "sfakeroot.h"
#include <sys/syscall.h>
@@ -39,11 +43,6 @@
#define SONAME "libsfakeroot.so"
#ifdef __linux__
-#ifndef __x86_64__
-#error "Architectures other than amd64 are not supported on Linux"
-#endif
-/*./x86_64-linux-gnu/asm/unistd_64.h:#define __NR_newfstatat 262 */
-#define SYS_fstatat 262
#define SESSION_FILE_LINE_FMT "%lu,%u,%u,%u\n"
#else
#define SESSION_FILE_LINE_FMT "%llu,%u,%u,%u\n"
@@ -69,13 +68,6 @@ struct cmdline_arguments {
size_t args_len;
};
-static void xsend(int sockfd, struct sfakeroot_msg *m)
-{
- if (sfakeroot_sendmsg(sockfd, m) == -1) {
- exit(1);
- }
-}
-
static struct sfakeroot_ent *lookupent(struct sfakeroot_list *list, ino_t st_ino)
{
for (struct sfakeroot_ent *e = list->first; e != NULL; e = e->next) {
@@ -85,6 +77,7 @@ static struct sfakeroot_ent *lookupent(struct sfakeroot_list *list, ino_t st_ino
}
}
+ debug("lookupent returning NULL\n");
return NULL;
}
@@ -98,7 +91,7 @@ static struct sfakeroot_ent *addent(struct sfakeroot_list *list,
return NULL;
}
- debug("add ent: %llu\n", s->st_ino);
+ debug("add ent: %llu has mode %o\n", s->st_ino, s->st_mode);
ent->stale = stale;
ent->next = NULL;
@@ -118,102 +111,76 @@ out:
return ent;
}
-static int real_stat_internal(int fd, const char *path, struct stat *sb,
- int flag, const char *working_dir, int *errno_out,
- int sysno)
+static void handle_stat(struct sfakeroot_msg *m)
{
- int cwdfd, ret;
+ struct sfakeroot_ent *ent;
+ int errno_sv = 0;
+ int cwdfd;
+ ino_t inode;
DIR *cwd = opendir(".");
if (cwd == NULL) {
- *errno_out = errno;
- return -1;
+ m->retcode = -1;
+ return;
}
-
- debug("chdir %s\n", working_dir);
cwdfd = dirfd(cwd);
- if (chdir(working_dir) == -1) {
- *errno_out = errno;
+ if (chdir(m->working_dir) == -1) {
closedir(cwd);
- return -1;
- }
-
- debug("do syscall\n");
- switch (sysno) {
- case SYS_fstatat:
- ret = syscall(sysno, fd, path, sb, flag);
- break;
- case SYS_stat:
- case SYS_lstat:
- ret = syscall(sysno, path, sb);
- break;
- default:
- ret = -1;
- errno = EINVAL;
- break;
+ m->retcode = -1;
+ return;
}
- *errno_out = errno;
- fchdir(cwdfd);
- closedir(cwd);
- return ret;
-}
-
-static int real_stat(const char *path, struct stat *sb, bool islstat,
- char *working_dir, int *errno_out)
-{
- int sysno = islstat ? SYS_lstat : SYS_stat;
- return real_stat_internal(-1, path, sb, 0, working_dir, errno_out, sysno);
-}
-
-static int real_fstatat(int fd, const char *path, struct stat *sb, int flag,
- const char *working_dir, int *errno_out)
-{
- return real_stat_internal(fd, path, sb, flag, working_dir,
- errno_out, SYS_fstatat);
-}
-
-static int real_fstat(int fd, struct stat *sb, int *errno_out)
-{
- int ret = syscall(SYS_fstat, fd, sb);
- *errno_out = errno;
- return ret;
-}
-
-static void handle_stat(struct sfakeroot_msg *m)
-{
- struct sfakeroot_ent *ent;
- int errno_sv = 0;
-
- debug("handle_fstat\n");
-
switch (m->type) {
case SFAKEROOT_MSG_FSTAT:
- m->retcode = real_fstat(m->fd, &m->st, &errno_sv);
+ syslog(LOG_WARNING, "handle_fstat fd: %d working dir: %s\n", m->fd, m->working_dir);
+ m->retcode = fstat(m->fd, &m->st);
+ inode = m->st.st_ino;
+ errno_sv = errno;
break;
case SFAKEROOT_MSG_STAT:
+ syslog(LOG_WARNING, "handle_stat path: %s, working dir: %s\n", m->path, m->working_dir);
+ m->retcode = stat(m->path, &m->st);
+ inode = m->st.st_ino;
+ errno_sv = errno;
+ break;
case SFAKEROOT_MSG_LSTAT:
- m->retcode = real_stat(m->path, &m->st,
- m->type == SFAKEROOT_MSG_LSTAT,
- m->working_dir, &errno_sv);
+ syslog(LOG_WARNING, "handle_lstat path: %s, working dir: %s\n", m->path, m->working_dir);
+ m->retcode = lstat(m->path, &m->st);
+ inode = m->st.st_ino;
+ errno_sv = errno;
break;
case SFAKEROOT_MSG_FSTATAT:
- m->retcode = real_fstatat(m->fd, m->path, &m->st, m->flag,
- m->working_dir, &errno_sv);
+ m->retcode = fstatat(m->fd, m->path, &m->st, m->flag);
+ inode = m->st.st_ino;
+ errno_sv = errno;
break;
+#if USE_STATX
+ case SFAKEROOT_MSG_STATX:
+ syslog(LOG_WARNING, "handle_statx path: %s, working dir: %s\n", m->path, m->working_dir);
+ m->retcode = statx(m->fd, m->path, m->flag, m->mask, &m->statxbuf);
+ inode = m->statxbuf.stx_ino;
+ errno_sv = errno;
+ break;
+#endif
default:
debug("non stat message in handle_stat\n");
- return;
+ goto cleanup;
}
if (m->retcode != 0) {
m->reterrno = errno_sv;
- return;
+ goto cleanup;
}
- if ((ent = lookupent(&ents, m->st.st_ino)) != NULL) {
+ if ((ent = lookupent(&ents, inode)) != NULL) {
+ debug("lookupent returned ent for inode %d, has st_mode: %o\n", inode, ent->st.st_mode);
if (ent->stale) {
/* update the entry with the new stat data */
+#if USE_STATX
+ m->statxbuf.stx_uid = ent->st.st_uid;
+ m->statxbuf.stx_gid = ent->st.st_gid;
+ m->statxbuf.stx_mode = ent->st.st_mode;
+#endif
m->st.st_uid = ent->st.st_uid;
m->st.st_gid = ent->st.st_gid;
m->st.st_mode = ent->st.st_mode;
@@ -221,80 +188,120 @@ static void handle_stat(struct sfakeroot_msg *m)
ent->stale = false;
}
else {
+#if USE_STATX
+ m->statxbuf.stx_uid = ent->st.st_uid;
+ m->statxbuf.stx_gid = ent->st.st_gid;
+ m->statxbuf.stx_mode = ent->st.st_mode;
+#endif
m->st = ent->st;
}
if ((int) m->st.st_uid == -1) {
m->st.st_uid = 0;
+#if USE_STATX
+ m->statxbuf.stx_uid = 0;
+#endif
}
if ((int) m->st.st_gid == -1) {
m->st.st_gid = 0;
+#if USE_STATX
+ m->statxbuf.stx_gid = 0;
+#endif
}
- return;
+
+ goto cleanup;
}
m->st.st_uid = 0;
m->st.st_gid = 0;
+#if USE_STATX
+ m->statxbuf.stx_uid = 0;
+ m->statxbuf.stx_gid = 0;
+#endif
+
+cleanup:
+ fchdir(cwdfd);
+ closedir(cwd);
}
static void handle_perms_change(struct sfakeroot_msg *m)
{
- struct stat s;
+ struct stat s = {0};
int errno_sv;
+ int cwdfd;
struct sfakeroot_ent *ent;
+ DIR *cwd = opendir(".");
+
+ if (cwd == NULL) {
+ m->retcode = -1;
+ return;
+ }
+ cwdfd = dirfd(cwd);
+ if (chdir(m->working_dir) == -1) {
+ closedir(cwd);
+ m->retcode = -1;
+ return;
+ }
debug("handle_perms_change: m->path: %s, m->working_dir: %s\n",
m->path, m->working_dir);
switch (m->type) {
case SFAKEROOT_MSG_LCHOWN:
- m->retcode = real_stat(m->path, &s, true, m->working_dir, &errno_sv);
+ m->retcode = lstat(m->path, &s);
+ errno_sv = errno;
break;
case SFAKEROOT_MSG_CHOWN:
case SFAKEROOT_MSG_CHMOD:
- m->retcode = real_stat(m->path, &s, false, m->working_dir, &errno_sv);
+ m->retcode = stat(m->path, &s);
+ errno_sv = errno;
break;
case SFAKEROOT_MSG_FCHOWNAT:
- m->retcode = real_fstatat(m->fd, m->path, &s, m->flag,
- m->working_dir, &errno_sv);
+ m->retcode = fstatat(m->fd, m->path, &s, m->flag);
+ errno_sv = errno;
break;
case SFAKEROOT_MSG_FCHOWN:
- m->retcode = real_fstat(m->fd, &s, &errno_sv);
+ m->retcode = fstat(m->fd, &s);
+ errno_sv = errno;
break;
default:
debug("non chown/chmod message in handle_perms_change\n");
- return;
+ goto cleanup;
}
if (m->retcode == -1) {
debug("stat returned error\n");
m->reterrno = errno_sv;
- return;
+ goto cleanup;
}
/* path exists */
switch (m->type) {
case SFAKEROOT_MSG_LCHOWN:
case SFAKEROOT_MSG_CHOWN:
+ case SFAKEROOT_MSG_FCHOWN:
case SFAKEROOT_MSG_FCHOWNAT:
s.st_uid = m->uid;
s.st_gid = m->gid;
break;
case SFAKEROOT_MSG_CHMOD:
+ debug("setting mode %o\n", m->mode);
s.st_mode = (s.st_mode & S_IFMT) | m->mode;
break;
default:
debug("non chown/chmod message in handle_perms_change\n");
- return;
+ goto cleanup;
}
if ((ent = lookupent(&ents, s.st_ino)) != NULL) {
uid_t u = ent->st.st_uid;
gid_t g = ent->st.st_gid;
+ mode_t md = ent->st.st_mode;
ent->st = s;
ent->st.st_uid = ((int) m->uid == -1) ? u : m->uid;
ent->st.st_gid = ((int) m->gid == -1) ? g : m->gid;
+ ent->st.st_mode = m->mode ? m->mode : md;
m->retcode = 0;
- return;
+ goto cleanup;
}
s.st_uid = m->uid;
@@ -303,10 +310,14 @@ static void handle_perms_change(struct sfakeroot_msg *m)
if (addent(&ents, &s, false) == NULL) {
m->reterrno = ENOMEM;
m->retcode = -1;
- return;
+ goto cleanup;
}
m->retcode = 0;
+
+cleanup:
+ fchdir(cwdfd);
+ closedir(cwd);
}
/* listen for new connections */
@@ -454,6 +465,13 @@ static int sfakeroot_load_session_from_file(const struct cmdline_arguments *args
return 0;
}
+bool sfakeroot_daemon_running(void)
+{
+ int sockfd = sfakeroot__session_open_internal(false);
+ close(sockfd);
+ return sockfd != -1;
+}
+
static void sfakeroot_server(int pipewfd, const char *session_socket_path,
const struct cmdline_arguments *args)
{
@@ -476,22 +494,30 @@ static void sfakeroot_server(int pipewfd, const char *session_socket_path,
for (;;) {
struct sfakeroot_msg m;
+ debug("waiting for connection...\n");
+ debug("%d\n", getpid());
+ debug("getenv(LD_PRELOAD): ");
sock = accept(listen_sock, (struct sockaddr *) &sa, &namelen);
if (sock == -1) {
fprintf(stderr, "%s: accept: %s\n", argv0, strerror(errno));
exit(1);
}
+ debug("connection accepted\n");
+ debug("waiting for message\n");
if (sfakeroot_recvmsg(sock, &m) == -1) {
fprintf(stderr, "eof?\n");
continue;
}
+ syslog(LOG_WARNING, "message type: %u\n", m.type);
switch (m.type) {
case SFAKEROOT_MSG_FSTAT:
case SFAKEROOT_MSG_LSTAT:
case SFAKEROOT_MSG_STAT:
case SFAKEROOT_MSG_FSTATAT:
+ case SFAKEROOT_MSG_STATX:
+ syslog(LOG_WARNING, "doing the fake stat\n");
handle_stat(&m);
break;
case SFAKEROOT_MSG_CHOWN:
@@ -509,7 +535,9 @@ static void sfakeroot_server(int pipewfd, const char *session_socket_path,
}
exit(0);
}
- xsend(sock, &m);
+ if (sfakeroot_sendmsg(sock, &m, NULL, 0) == -1) {
+ exit(1);
+ }
close(sock);
}
}
@@ -531,6 +559,7 @@ static int sfakeroot_daemon(const char *session_socket_path,
case 0:
/* child */
close(pipefds[0]); /* close read end */
+ debug("starting daemon...\n");
if (daemon(1, 1) == -1) {
fprintf(stderr, "%s: failed to daemonise: %s\n",
argv0, strerror(errno));
@@ -568,7 +597,7 @@ static int sfakeroot_daemon(const char *session_socket_path,
int sfakeroot_session_open(void);
-static int setenvvars(const char *session_socket_path)
+static int setpreload(void)
{
char *wd = SFAKEROOT_LIBDIR, *path = NULL;
size_t len;
@@ -576,24 +605,18 @@ static int setenvvars(const char *session_socket_path)
len = snprintf(NULL, 0, "%s/%s", wd, SONAME);
path = malloc(len + 1);
if (path == NULL) {
- goto error;
+ fprintf(stderr, "%s: malloc: %s\n", argv0, strerror(errno));
+ return -1;
}
snprintf(path, len + 1, "%s/%s", wd, SONAME);
if (setenv("LD_PRELOAD", path, 1) == -1) {
- goto error;
+ fprintf(stderr, "%s: setenv: %s\n", argv0, strerror(errno));
+ free(path);
+ return -1;
}
free(path);
-
- if (setenv("SFAKEROOT_SOCKET_PATH", session_socket_path, 1) == -1) {
- goto error;
- }
-
return 0;
-
-error:
- fprintf(stderr, "%s: setenv: %s\n", argv0, strerror(errno));
- return -1;
}
static void usage(void)
@@ -660,22 +683,29 @@ int main(int argc, char *argv[])
exit_status = 1;
goto cleanup;
}
-
- if (setenvvars(session_socket_path) == -1) {
+ if (setenv("SFAKEROOT_SOCKET_PATH", session_socket_path, 1) == -1) {
+ fprintf(stderr, "%s: setenv: %s\n", argv0, strerror(errno));
exit_status = 1;
goto cleanup;
}
-
if (!sfakeroot_daemon_running()) {
if (sfakeroot_daemon(session_socket_path, &args) == -1) {
exit_status = 1;
goto cleanup;
}
}
-
exec_argvp = args.args_len > 0 ? args.args : sargv;
switch (pid = fork()) {
case 0:
+ // This child process is going to be our session
+ // i.e. shell or whatever program we were asked to run
+ // we need to set LD_PRELOAD for this process so that
+ // we can intercept the 'stat' calls and redirect them
+ // to the sfakeroot daemon.
+ if (setpreload() == -1) {
+ exit_status = 1;
+ goto cleanup;
+ }
execvp(exec_argvp[0], exec_argvp);
fprintf(stderr, "%s: exec `%s': %s\n",
argv0, exec_argvp[0], strerror(errno));
@@ -704,7 +734,7 @@ int main(int argc, char *argv[])
exit_status = 1;
goto cleanup;
}
- if (sfakeroot_sendmsg(sockfd, &m) == -1) {
+ if (sfakeroot_sendmsg(sockfd, &m, NULL, 0) == -1) {
exit_status = 1;
goto cleanup;
}
diff --git a/sfakeroot.h b/sfakeroot.h
@@ -1,6 +1,6 @@
/* sfakeroot
*
- * Copyright © 2020 Richard Ipsum
+ * Copyright © 2020 - 2025 Richard Ipsum
*
* This file is part of sfakeroot.
*
@@ -20,11 +20,18 @@
#pragma once
#include <stdarg.h>
+#include <stdbool.h>
#include <sys/stat.h>
#define SOCK_PATH ".sfakeroot_socket"
#define MAX_PATHLEN 8192
+#if defined(HAVE_STATX) && HAVE_STATX && defined(__linux__)
+ #define USE_STATX 1
+#else
+ #define USE_STATX 0
+#endif
+
static void debug(const char *fmt, ...)
{
#ifdef DEBUG
@@ -32,6 +39,7 @@ static void debug(const char *fmt, ...)
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
+ //puts((USE_STATX == 1) ? "STATX ENABLED" : "STATX DISABLED");
#else
(void) fmt;
#endif
@@ -53,7 +61,8 @@ enum sfakeroot_msg_type {
SFAKEROOT_MSG_FCHOWN,
SFAKEROOT_MSG_FCHOWNAT,
SFAKEROOT_MSG_CHMOD,
- SFAKEROOT_MSG_FINISH
+ SFAKEROOT_MSG_FINISH,
+ SFAKEROOT_MSG_STATX
};
struct sfakeroot_msg {
@@ -67,6 +76,7 @@ struct sfakeroot_msg {
char path[MAX_PATHLEN];
int fd;
int flag;
+ unsigned int mask;
struct stat st;
uid_t uid;
gid_t gid;
@@ -75,10 +85,14 @@ struct sfakeroot_msg {
/* system call return data */
int retcode;
int reterrno;
+
+#if USE_STATX
+ struct statx statxbuf;
+#endif
};
int sfakeroot_recvmsg(int fd, struct sfakeroot_msg *m);
-int sfakeroot_sendmsg(int fd, struct sfakeroot_msg *m);
+int sfakeroot_sendmsg(int fd, struct sfakeroot_msg *m, int send_fds[], int send_fds_len);
int sfakeroot_stat(const char *path, struct stat *s, bool lstat);
bool sfakeroot_daemon_running(void);
diff --git a/sfakeroot_inline.h b/sfakeroot_inline.h
@@ -0,0 +1,111 @@
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#include "sfakeroot.h"
+
+static int sfakeroot__session_open_internal(bool session_expected)
+{
+ struct sockaddr_un sa = {.sun_family = AF_UNIX};
+ int sockfd;
+ socklen_t socklen;
+ char *sockpath = getenv("SFAKEROOT_SOCKET_PATH");
+
+ if (sockpath == NULL) {
+ fprintf(stderr, "environment variable SFAKEROOT_SOCKET_PATH not set!\n");
+ return -1;
+ }
+
+ sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (sockfd == -1) {
+ fprintf(stderr, "socket: %s\n", strerror(errno));
+ return -1;
+ }
+
+ strlcpy(sa.sun_path, sockpath, sizeof (sa.sun_path));
+ socklen = strlen(sa.sun_path) + 1 + sizeof (sa.sun_family);
+
+ if (connect(sockfd, (struct sockaddr *) &sa, socklen) == -1) {
+ if (session_expected) {
+ fprintf(stderr, "connect: %s\n", strerror(errno));
+ }
+ return -1;
+ }
+
+ return sockfd;
+}
+
+int sfakeroot_session_open(void)
+{
+ return sfakeroot__session_open_internal(true);
+}
+
+int sfakeroot_recvmsg(int fd, struct sfakeroot_msg *m)
+{
+ union {
+ char buf[CMSG_SPACE(253)]; // 253 SCM_MAX_FD
+ struct cmsghdr align;
+ } u;
+ struct iovec io = {
+ .iov_base = m,
+ .iov_len = sizeof (*m)
+ };
+ struct msghdr msg = {
+ .msg_iov = &io,
+ .msg_iovlen = 1,
+ .msg_control = u.buf,
+ .msg_controllen = sizeof (u.buf)
+ };
+ if (recvmsg(fd, &msg, 0) == -1) {
+ fprintf(stderr, "sfakeroot_recvmsg: error reading message\n");
+ return -1;
+ }
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+ if (cmsg && cmsg->cmsg_type == SCM_RIGHTS) {
+ debug("got an fd!!!\n");
+ memcpy(&m->fd, CMSG_DATA(cmsg), sizeof (int));
+ }
+
+ return 0;
+}
+
+int sfakeroot_sendmsg(int fd, struct sfakeroot_msg *m, int send_fds[], int send_fds_len)
+{
+ union {
+ char *buf;
+ struct cmsghdr align;
+ } u;
+ int ret = -1;
+ struct iovec io = {
+ .iov_base = m,
+ .iov_len = sizeof (*m)
+ };
+ struct msghdr msg = {
+ .msg_iov = &io,
+ .msg_iovlen = 1,
+ };
+ if (send_fds_len > 0) {
+ size_t buflen = CMSG_SPACE(send_fds_len * sizeof(int));
+ u.buf = malloc(buflen);
+ if (u.buf == NULL) {
+ fprintf(stderr, "sfakeroot_sendmsg: out of memory: %s\n", strerror(errno));
+ goto cleanup;
+ }
+ msg.msg_control = u.buf;
+ msg.msg_controllen = buflen;
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(send_fds_len * sizeof (int));
+ memcpy(CMSG_DATA(cmsg), send_fds, send_fds_len * sizeof (int));
+ }
+ if (sendmsg(fd, &msg, 0) == -1) {
+ fprintf(stderr, "sfakeroot_sendmsg: error writing message: %s\n", strerror(errno));
+ goto cleanup;
+ }
+ ret = 0;
+cleanup:
+ if (send_fds_len > 0) {
+ free(u.buf);
+ }
+ return ret;
+}
diff --git a/tests/Makefile b/tests/Makefile
@@ -0,0 +1,14 @@
+# I'm assuming most Linux systems have the "new" statx call now,
+# https://www.man7.org/linux/man-pages/man2/statx.2.html
+# but if you're compiling on Linux and get errors about statx
+# set HAVE_STATX = 0
+HAVE_STATX = 1
+
+CFLAGS=-Wall -Wextra -pedantic -std=c99 -D_POSIX_C_SOURCE=200809L -DHAVE_STATX=$(HAVE_STATX) -D_GNU_SOURCE
+
+BIN=chown lchown chmod stat lstat fstat fstatat fchown fchownat statx
+
+all: $(BIN)
+
+clean:
+ rm -f $(BIN)
+\ No newline at end of file
diff --git a/tests/chmod.c b/tests/chmod.c
@@ -0,0 +1,53 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+
+int main(void)
+{
+ char *template = "/tmp/sfakeroot.test.XXXXXX";
+ char path[strlen(template) + 1];
+ struct stat st_before;
+ struct stat st_after;
+ int fd;
+
+ strcpy(path, template);
+ printf("test making path %s\n", mktemp(path));
+
+ if ((fd = creat(path, 777)) == -1) {
+ fprintf(stderr, "creat '%s' failed: %s\n", path, strerror(errno));
+ return 1;
+ }
+ close(fd);
+
+ if (stat(path, &st_before) == -1)
+ {
+ fprintf(stderr, "stat failed\n");
+ return 1;
+ }
+ if (st_before.st_uid != 0 || st_before.st_gid != 0) {
+ fprintf(stderr, "uid/gid not 0\n");
+ return 1;
+ }
+
+ if (chmod(path, 0444) == -1) {
+ fprintf(stderr, "chown '%s' failed: %s\n", path, strerror(errno));
+ return 1;
+ }
+
+ if (stat(path, &st_after) == -1)
+ {
+ fprintf(stderr, "stat failed\n");
+ return 1;
+ }
+ if ((st_after.st_mode & ~S_IFMT) != 0444) {
+ fprintf(stderr, "mode not 444, it is %o\n", st_after.st_mode & ~S_IFMT);
+ return 1;
+ }
+
+ return 0;
+}
+
diff --git a/tests/chown.c b/tests/chown.c
@@ -0,0 +1,53 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+
+int main(void)
+{
+ char *template = "/tmp/sfakeroot.test.XXXXXX";
+ char path[strlen(template) + 1];
+ struct stat st_before;
+ struct stat st_after;
+
+ strcpy(path, template);
+ printf("test making path %s\n", mktemp(path));
+
+ if (creat(path, 777) == -1) {
+ fprintf(stderr, "creat '%s' failed: %s\n", path, strerror(errno));
+ }
+
+ if (stat(path, &st_before) == -1)
+ {
+ fprintf(stderr, "stat failed\n");
+ return 1;
+ }
+ if (st_before.st_uid != 0 || st_before.st_gid != 0) {
+ fprintf(stderr, "uid/gid not 0\n");
+ return 1;
+ }
+
+ if (chown(path, 42, 43) == -1) {
+ fprintf(stderr, "chown '%s' failed: %s\n", path, strerror(errno));
+ return 1;
+ }
+
+ if (stat(path, &st_after) == -1)
+ {
+ fprintf(stderr, "stat failed\n");
+ return 1;
+ }
+ if (st_after.st_uid != 42) {
+ fprintf(stderr, "uid not 42\n");
+ return 1;
+ }
+ if (st_after.st_gid != 43) {
+ fprintf(stderr, "gid not 43\n");
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/tests/fchown.c b/tests/fchown.c
@@ -0,0 +1,54 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+
+int main(void)
+{
+ char *template = "/tmp/sfakeroot.test.XXXXXX";
+ char path[strlen(template) + 1];
+ struct stat st_before;
+ struct stat st_after;
+ int fd;
+
+ strcpy(path, template);
+ printf("test making path %s\n", mktemp(path));
+
+ if ((fd = creat(path, 777)) == -1) {
+ fprintf(stderr, "creat '%s' failed: %s\n", path, strerror(errno));
+ return 1;
+ }
+ if (stat(path, &st_before) == -1)
+ {
+ fprintf(stderr, "stat failed\n");
+ return 1;
+ }
+ if (st_before.st_uid != 0 || st_before.st_gid != 0) {
+ fprintf(stderr, "uid/gid not 0\n");
+ return 1;
+ }
+
+ if (fchown(fd, 42, 43) == -1) {
+ fprintf(stderr, "chown '%s' failed: %s\n", path, strerror(errno));
+ return 1;
+ }
+
+ if (stat(path, &st_after) == -1)
+ {
+ fprintf(stderr, "stat failed\n");
+ return 1;
+ }
+ if (st_after.st_uid != 42) {
+ fprintf(stderr, "uid not 42\n");
+ return 1;
+ }
+ if (st_after.st_gid != 43) {
+ fprintf(stderr, "gid not 43\n");
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/tests/fchownat.c b/tests/fchownat.c
@@ -0,0 +1,54 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+
+int main(void)
+{
+ char *template = "/tmp/sfakeroot.test.XXXXXX";
+ char path[strlen(template) + 1];
+ struct stat st_before;
+ struct stat st_after;
+ int fd;
+
+ strcpy(path, template);
+ printf("test making path %s\n", mktemp(path));
+
+ if ((fd = creat(path, 777)) == -1) {
+ fprintf(stderr, "creat '%s' failed: %s\n", path, strerror(errno));
+ return 1;
+ }
+ if (stat(path, &st_before) == -1)
+ {
+ fprintf(stderr, "stat failed\n");
+ return 1;
+ }
+ if (st_before.st_uid != 0 || st_before.st_gid != 0) {
+ fprintf(stderr, "uid/gid not 0\n");
+ return 1;
+ }
+
+ if (fchownat(AT_FDCWD, path, 42, 43, 0) == -1) {
+ fprintf(stderr, "chown '%s' failed: %s\n", path, strerror(errno));
+ return 1;
+ }
+
+ if (stat(path, &st_after) == -1)
+ {
+ fprintf(stderr, "stat failed\n");
+ return 1;
+ }
+ if (st_after.st_uid != 42) {
+ fprintf(stderr, "uid not 42\n");
+ return 1;
+ }
+ if (st_after.st_gid != 43) {
+ fprintf(stderr, "gid not 43\n");
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/tests/fstat.c b/tests/fstat.c
@@ -0,0 +1,36 @@
+#include <stdio.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+
+int main(int argc __attribute__((unused)), char *argv[])
+{
+ struct stat st_expected;
+ struct stat st_actual;
+
+ if (stat(argv[0], &st_expected) == -1)
+ {
+ fprintf(stderr, "stat failed\n");
+ return 1;
+ }
+
+
+ int fd = open(argv[0], O_RDONLY);
+ if (fd == -1) {
+ fprintf(stderr, "open failed\n");
+ return 1;
+ }
+
+ if (fstat(fd, &st_actual) == -1)
+ {
+ fprintf(stderr, "fstat failed\n");
+ return 1;
+ }
+
+ if (st_actual.st_uid == 0
+ && st_actual.st_gid == 0
+ && st_expected.st_ino == st_actual.st_ino) {
+ return 0;
+ }
+
+ return 1;
+}
diff --git a/tests/fstatat.c b/tests/fstatat.c
@@ -0,0 +1,29 @@
+#include <stdio.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+
+int main(int argc __attribute__((unused)), char *argv[])
+{
+ struct stat st_expected;
+ struct stat st_actual;
+
+ if (stat(argv[0], &st_expected) == -1)
+ {
+ fprintf(stderr, "stat failed\n");
+ return 1;
+ }
+
+ if (fstatat(AT_FDCWD, argv[0], &st_actual, 0) == -1)
+ {
+ fprintf(stderr, "fstat failed\n");
+ return 1;
+ }
+
+ if (st_actual.st_uid == 0
+ && st_actual.st_gid == 0
+ && st_expected.st_ino == st_actual.st_ino) {
+ return 0;
+ }
+
+ return 0;
+}
diff --git a/tests/lchown.c b/tests/lchown.c
@@ -0,0 +1,54 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+
+int main(void)
+{
+ char *template = "/tmp/sfakeroot.test.XXXXXX";
+ char path[strlen(template) + 1];
+ struct stat st_before;
+ struct stat st_after;
+
+ strcpy(path, template);
+ printf("test making path %s\n", mktemp(path));
+
+ if (symlink("foobar", path) == -1) {
+ fprintf(stderr, "symlink '%s' failed: %s\n", path, strerror(errno));
+ return 1;
+ }
+
+ if (lstat(path, &st_before) == -1)
+ {
+ fprintf(stderr, "stat failed\n");
+ return 1;
+ }
+ if (st_before.st_uid != 0 || st_before.st_gid != 0) {
+ fprintf(stderr, "uid/gid not 0\n");
+ return 1;
+ }
+
+ if (lchown(path, 42, 43) == -1) {
+ fprintf(stderr, "chown '%s' failed: %s\n", path, strerror(errno));
+ return 1;
+ }
+
+ if (lstat(path, &st_after) == -1)
+ {
+ fprintf(stderr, "stat failed\n");
+ return 1;
+ }
+ if (st_after.st_uid != 42) {
+ fprintf(stderr, "uid not 42\n");
+ return 1;
+ }
+ if (st_after.st_gid != 43) {
+ fprintf(stderr, "gid not 43\n");
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/tests/lstat.c b/tests/lstat.c
@@ -0,0 +1,29 @@
+#include <stdio.h>
+#include <sys/stat.h>
+
+int main(int argc __attribute__((unused)), char *argv[])
+{
+ struct stat st_expected;
+ struct stat st_actual;
+
+ if (stat(argv[0], &st_expected) == -1)
+ {
+ fprintf(stderr, "stat failed\n");
+ return 1;
+ }
+
+
+ if (lstat(argv[0], &st_actual) == -1)
+ {
+ fprintf(stderr, "stat failed\n");
+ return 1;
+ }
+
+ if (st_expected.st_uid == 0
+ && st_expected.st_gid == 0
+ && st_expected.st_ino == st_actual.st_ino) {
+ return 0;
+ }
+
+ return 1;
+}
diff --git a/tests/run_tests b/tests/run_tests
@@ -0,0 +1,14 @@
+#!./sfakeroot sh
+
+for test in chmod chown fchown fchownat lchown stat fstat lstat fstatat statx
+do
+ tests/$test
+ e=$?
+ if [ $e -ne 0 ]
+ then
+ echo "\033[31;1mTest '$test' failed\033[0m"
+ exit 1
+ else
+ echo "\033[255;1mTest '$test' passed!\033[0m"
+ fi
+done
diff --git a/tests/stat.c b/tests/stat.c
@@ -0,0 +1,19 @@
+#include <stdio.h>
+#include <sys/stat.h>
+
+int main(int argc __attribute__((unused)), char *argv[])
+{
+ struct stat st;
+
+ if (stat(argv[0], &st) == -1)
+ {
+ fprintf(stderr, "stat failed\n");
+ return 1;
+ }
+#if defined(__linux__)
+ printf("stat %s: ino: %lu uid: %d gid: %d\n", argv[0], st.st_ino, st.st_uid, st.st_gid);
+#else
+ printf("stat %s: ino: %llu uid: %d gid: %d\n", argv[0], st.st_ino, st.st_uid, st.st_gid);
+#endif
+ return 0;
+}
diff --git a/tests/statx.c b/tests/statx.c
@@ -0,0 +1,43 @@
+#include <stdio.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include "../sfakeroot.h"
+
+int test(int argc __attribute__((unused)), char *argv[])
+{
+#if USE_STATX
+ struct stat st_expected;
+ struct statx stxbuf;
+
+ if (stat(argv[0], &st_expected) == -1)
+ {
+ fprintf(stderr, "stat failed\n");
+ return 1;
+ }
+
+ if (statx(AT_FDCWD, argv[0], 0, 0, &stxbuf) == -1) {
+ fprintf(stderr, "statx failed\n");
+ return 1;
+ }
+
+ if (stxbuf.stx_uid == 0
+ && stxbuf.stx_gid == 0
+ && stxbuf.stx_ino == st_expected.st_ino) {
+ return 0;
+ }
+
+ return 1;
+#else
+ (void) argv;
+ return 0;
+#endif
+}
+
+int main(int argc __attribute__((unused)), char *argv[])
+{
+#if USE_STATX
+ return 0;
+#else
+ return test(argc, argv);
+#endif
+}