141 lines
3.8 KiB
C
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
|