Files
libglacier-ng/src/common.h
T
2026-10-05 14:47:58 -04:00

141 lines
3.8 KiB
C

#ifndef COMMON_H_
#define COMMON_H_
#define _POSIX_C_SOURCE 200809L
#define _XOPEN_SOURCE 700
#include <dirent.h>
#include <errno.h>
#include <linux/limits.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct
{
const char *path;
mode_t mode;
}
dir_t;
/*
* On-disk layout. Each scope keeps everything it ever creates, renames
* or locks under ONE base directory it owns:
*
* /glacier/usr/<uid>/{index,store,links,stage,old,lock} owned by uid, 0700
* /glacier/sys/{index,store,links,stage,old,lock} root
*
* /glacier/usr itself is root-owned and not group/world-writable, so a
* user can never create (or squat) another uid's base; root creates
* each one (gl_init_user, via `gstore -n` directly or through doas).
* Every transaction step — stage, old/, lock, the commit renames — then
* happens inside a directory the acting user owns, which is what lets
* an unprivileged user run their own transactions at all.
*/
#define GL_USR_ROOT "/glacier/usr"
#define GL_SYS_ROOT "/glacier/sys"
static inline void
gl_usr_base(uid_t uid, char *out, size_t outsz)
{
snprintf(out, outsz, GL_USR_ROOT "/%u", (unsigned)uid);
}
typedef enum {
GL_USR_BASE_OK = 0,
GL_USR_BASE_MISSING, /* no /glacier/usr/<uid> yet: run gstore -n */
GL_USR_BASE_UNSAFE_ROOT, /* /glacier/usr not root-owned, or group/world-writable */
GL_USR_BASE_UNSAFE /* not a real dir, not owned by uid, or not 0700-ish */
} gl_usr_base_status_t;
/* Refuse to operate on a user base we didn't hand out. lstat(), never
* stat(): a symlink in place of /glacier/usr/<uid> is exactly what a
* squatter would plant. Backstop for the layout invariant above, so a
* hand-loosened /glacier/usr can't silently turn into one user writing
* another's store. */
static inline gl_usr_base_status_t
gl_usr_base_check(uid_t uid)
{
struct stat st;
if (lstat(GL_USR_ROOT, &st) != 0) { return GL_USR_BASE_MISSING; }
if (!S_ISDIR(st.st_mode) || st.st_uid != 0 ||
(st.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
return GL_USR_BASE_UNSAFE_ROOT;
}
char base[PATH_MAX];
gl_usr_base(uid, base, sizeof(base));
if (lstat(base, &st) != 0) { return GL_USR_BASE_MISSING; }
if (!S_ISDIR(st.st_mode) || st.st_uid != uid ||
(st.st_mode & (S_IRWXG | S_IRWXO)) != 0) {
return GL_USR_BASE_UNSAFE;
}
return GL_USR_BASE_OK;
}
static inline int
gl_mkdirp(const char *path, mode_t mode)
{
char tmp[PATH_MAX];
strncpy(tmp, path, sizeof(tmp));
tmp[sizeof(tmp) - 1] = '\0';
for (char *p = tmp + 1; *p; p++) {
if (*p == '/') {
*p = '\0';
if (mkdir(tmp, mode) == -1 && errno != EEXIST) {
return -1;
}
*p = '/';
}
}
if (mkdir(tmp, mode) == -1 && errno != EEXIST) {
return -1;
}
return 0;
}
static inline int
gl_rmdir_recursive(const char *path)
{
DIR *dir = opendir(path);
if (!dir) { return -1; }
struct dirent *ent;
while ((ent = readdir(dir))) {
/* Only . and .. — skipping every dotfile left any directory
* containing one (a package's own dotfiles, the store's
* .displaced tree) impossible to rmdir, so the old/ and stage
* wipes silently failed and the next commit hit ENOTEMPTY. */
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
continue;
}
char full[PATH_MAX];
snprintf(full, sizeof(full), "%s/%s", path, ent->d_name);
struct stat st;
if (lstat(full, &st) != 0) { continue; }
if (S_ISDIR(st.st_mode)) {
gl_rmdir_recursive(full);
}
else {
unlink(full);
}
}
closedir(dir);
return rmdir(path);
}
#endif