#ifndef COMMON_H_ #define COMMON_H_ #define _POSIX_C_SOURCE 200809L #define _XOPEN_SOURCE 700 #include #include #include #include #include #include #include #include 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//{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/ 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/ 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