mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(daemon): explain foreign-owned cache ancestors and warn on network filesystems (#1717)
A cache root whose resolved path runs through a directory owned by another non-root account (typical on shared/HPC filesystems where ~/.cache is a symlink into a shared tree) is refused by the private-directory walk. The refusal itself is correct: the owner of an ancestor can rename or replace everything below it after the walk, so it could redirect the cache. But posix_directory_transition_secure() set no validation detail, so the caller printed "ancestry component validation failed (errno 17)" -- a stale EEXIST from mkdirat() that named neither the directory nor the reason. - The refusal now names the directory, its owner uid and account name, the caller's uid, and the remedy (a location with a self/root-owned ancestry, e.g. via CBM_CACHE_DIR). The path is shortened from the left so the remedy always fits the 384-byte detail buffer. - On Linux the account name is read from /etc/passwd, not getpwuid(). The release binary is statically linked against glibc, and getpwuid() there loads the host's NSS modules at runtime, which can crash on a glibc version mismatch (a real risk on sssd/LDAP cluster hosts). Accounts that exist only in a directory service are shown by uid. - New advice-only check: on Linux, a cache root on NFS, CIFS, SMB2, Lustre or GPFS (statfs f_type) logs one daemon.cache_root_network_fs warning per process. SQLite WAL is unreliable on network filesystems. Nothing is refused. - docs/CONFIGURATION.md gains a "Shared / HPC filesystems" section. The ownership policy is unchanged, and there is still no override (same stance as #1830). Test seams: one seam reports a directory (matched by dev/ino) as foreign-owned, and another supplies the statfs magic. The refusal, the message and the once-per-process warning are therefore testable without root and on every platform. Refs #1717 Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
0f52d30c29
commit
6eb33534f6
5 files changed
+458
-4
No files matched your search
@@ -235,6 +235,33 @@ be indexed and later returned. And the credential list is a denylist, so it
|
||||
raises the cost of a mistake rather than closing the class — a directory it does
|
||||
not name is permitted.
|
||||
|
||||
### Shared / HPC filesystems
|
||||
|
||||
On clusters and other shared machines, home directories often live on a network
|
||||
filesystem (NFS, Lustre, GPFS, SMB/CIFS), and `~/.cache` is sometimes a symlink
|
||||
into a shared tree. Three rules keep CBM working there:
|
||||
|
||||
- **Put the cache and the rendezvous on local storage.** Set `CBM_CACHE_DIR` (indexes,
|
||||
`_config.db`) and, if needed, `CBM_RUNTIME_DIR` (the daemon rendezvous) to a
|
||||
local disk you own, for example `/local/scratch/$USER/cbm` or `/tmp/$USER-cbm`.
|
||||
Set the same values in your MCP client's environment and your shell.
|
||||
- **Do not route the cache into another account's tree.** A symlinked `~/.cache` is
|
||||
resolved and allowed, but every directory on the *resolved* path must be owned
|
||||
by you or by root. If a parent such as `/shared/fs` belongs to another account,
|
||||
that account can rename or replace anything below it, so CBM refuses the path
|
||||
and names the directory and its owner:
|
||||
|
||||
```text
|
||||
... (cache-private) - '/shared/fs' is owned by uid 1234 (alice), not by you (uid 1000) or root; ...
|
||||
```
|
||||
|
||||
This check has no override. Choose a location with a safe ancestry instead.
|
||||
- **Avoid network filesystems for the cache.** The indexes are SQLite databases in
|
||||
WAL mode, which needs shared-memory locking that network filesystems do not
|
||||
provide reliably. On Linux, CBM logs one `daemon.cache_root_network_fs` warning
|
||||
at startup when the cache root is on NFS, Lustre, GPFS, CIFS or SMB2. The cache
|
||||
still works there, but it can be slow, and a database can be corrupted.
|
||||
|
||||
## 5. Agent and Editor Integration Files
|
||||
|
||||
The `install` command can also write MCP entries and instruction blocks into agent/editor config files such as Claude Code, Codex, Gemini, VS Code, Cursor, Zed, and others.
|
||||
|
||||
+205
-4
@@ -21,6 +21,9 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#if defined(__linux__)
|
||||
#include <sys/vfs.h>
|
||||
#endif
|
||||
|
||||
enum {
|
||||
CBM_DAEMON_IPC_SEND_TIMEOUT_MS = 5000,
|
||||
@@ -54,6 +57,76 @@ const char *cbm_daemon_ipc_validation_detail(void) {
|
||||
return ipc_validation_detail_buffer;
|
||||
}
|
||||
|
||||
/* #1717: a cache root on a network filesystem (a shared HPC home, an NFS or
|
||||
* SMB mount) is ACCEPTED -- this is advice, never a refusal. SQLite's WAL mode
|
||||
* relies on shared-memory locking that network filesystems do not provide
|
||||
* reliably, so indexes there can be slow or corrupt. Detection is statfs
|
||||
* f_type, Linux only; other platforms never warn. */
|
||||
static const char *ipc_network_fs_name(unsigned long magic) {
|
||||
static const struct {
|
||||
unsigned long magic;
|
||||
const char *name;
|
||||
} kinds[] = {
|
||||
{0x6969UL, "NFS"}, {0xFF534D42UL, "CIFS"}, {0xFE534D42UL, "SMB2"},
|
||||
{0x0BD00BD0UL, "Lustre"}, {0x47504653UL, "GPFS"},
|
||||
};
|
||||
for (size_t index = 0; index < sizeof(kinds) / sizeof(kinds[0]); index++) {
|
||||
if (kinds[index].magic == magic) {
|
||||
return kinds[index].name;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static atomic_bool g_ipc_network_cache_warned;
|
||||
#ifdef CBM_ENABLE_TEST_SEAMS
|
||||
static bool g_ipc_fs_magic_override_active;
|
||||
static unsigned long g_ipc_fs_magic_override;
|
||||
void cbm_daemon_ipc_set_fs_magic_for_test(bool active, unsigned long magic) {
|
||||
g_ipc_fs_magic_override_active = active;
|
||||
g_ipc_fs_magic_override = magic;
|
||||
atomic_store(&g_ipc_network_cache_warned, false);
|
||||
}
|
||||
#endif
|
||||
|
||||
static bool ipc_path_fs_magic(const char *path, unsigned long *magic_out) {
|
||||
#ifdef CBM_ENABLE_TEST_SEAMS
|
||||
if (g_ipc_fs_magic_override_active) {
|
||||
*magic_out = g_ipc_fs_magic_override;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#if defined(__linux__)
|
||||
struct statfs status;
|
||||
if (statfs(path, &status) != 0) {
|
||||
return false;
|
||||
}
|
||||
/* f_type is a signed word on some ABIs; the magics are 32-bit. */
|
||||
*magic_out = (unsigned long)status.f_type & 0xFFFFFFFFUL;
|
||||
return true;
|
||||
#else
|
||||
(void)path;
|
||||
(void)magic_out;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool cbm_daemon_ipc_warn_if_network_cache_root(const char *cache_root) {
|
||||
unsigned long magic = 0;
|
||||
const char *kind = NULL;
|
||||
if (!cache_root || !cache_root[0] || !ipc_path_fs_magic(cache_root, &magic) ||
|
||||
!(kind = ipc_network_fs_name(magic))) {
|
||||
return false;
|
||||
}
|
||||
if (atomic_exchange(&g_ipc_network_cache_warned, true)) {
|
||||
return false; /* one warning per process */
|
||||
}
|
||||
cbm_log_warn("daemon.cache_root_network_fs", "path", cache_root, "fs", kind, "reason",
|
||||
"sqlite_wal_unreliable_on_network_fs", "hint",
|
||||
"set CBM_CACHE_DIR to local storage (docs/CONFIGURATION.md)");
|
||||
return true;
|
||||
}
|
||||
|
||||
static cbm_daemon_ipc_listen_failure_t ipc_listen_failure;
|
||||
|
||||
/* The failure recorder and its reset are only reached from the POSIX socket
|
||||
@@ -401,6 +474,9 @@ int cbm_daemon_ipc_wait_pending(const cbm_ipc_pending_ops_t *ops, uint32_t timeo
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
#include <stddef.h>
|
||||
#if !defined(__linux__)
|
||||
#include <pwd.h>
|
||||
#endif
|
||||
#include <sys/file.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
@@ -1788,14 +1864,138 @@ static bool posix_directory_parent_secure(int directory_fd) {
|
||||
* never chmod'd or ACL-rewritten. The euid-only enforcement that stops a
|
||||
* squatted private directory lives in the created/final/snapshot checks, not
|
||||
* here — this only walks ancestors. */
|
||||
static bool posix_directory_transition_secure(int parent_fd, int child_fd) {
|
||||
#ifdef CBM_ENABLE_TEST_SEAMS
|
||||
/* #1717 seam: report one directory (by dev/ino) as owned by a foreign uid, so
|
||||
* the foreign-owned-ancestor refusal and its diagnostic are testable without
|
||||
* root (an unprivileged test cannot chown a directory to another account). */
|
||||
static bool g_posix_foreign_owner_active;
|
||||
static dev_t g_posix_foreign_owner_dev;
|
||||
static ino_t g_posix_foreign_owner_ino;
|
||||
static uid_t g_posix_foreign_owner_uid;
|
||||
bool cbm_daemon_ipc_posix_set_foreign_owned_dir_for_test(const char *directory_path,
|
||||
unsigned long foreign_uid) {
|
||||
struct stat status;
|
||||
g_posix_foreign_owner_active = false;
|
||||
if (!directory_path || lstat(directory_path, &status) != 0 || !S_ISDIR(status.st_mode)) {
|
||||
return false;
|
||||
}
|
||||
g_posix_foreign_owner_dev = status.st_dev;
|
||||
g_posix_foreign_owner_ino = status.st_ino;
|
||||
g_posix_foreign_owner_uid = (uid_t)foreign_uid;
|
||||
g_posix_foreign_owner_active = true;
|
||||
return true;
|
||||
}
|
||||
void cbm_daemon_ipc_posix_clear_foreign_owned_dir_for_test(void) {
|
||||
g_posix_foreign_owner_active = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
static uid_t posix_directory_observed_owner(const struct stat *status) {
|
||||
#ifdef CBM_ENABLE_TEST_SEAMS
|
||||
if (g_posix_foreign_owner_active && status->st_dev == g_posix_foreign_owner_dev &&
|
||||
status->st_ino == g_posix_foreign_owner_ino) {
|
||||
return g_posix_foreign_owner_uid;
|
||||
}
|
||||
#endif
|
||||
return status->st_uid;
|
||||
}
|
||||
|
||||
enum { POSIX_OWNER_NAME_CAP = 33, POSIX_PASSWD_LINE_CAP = 1024 };
|
||||
|
||||
/* The account name for uid, or "" when it cannot be named. Diagnostic only.
|
||||
*
|
||||
* Linux reads /etc/passwd directly instead of calling getpwuid(): the Linux
|
||||
* release binary is statically linked against glibc, where getpwuid() loads the
|
||||
* HOST's NSS modules at runtime -- a version-mismatch crash hazard exactly on
|
||||
* the sssd/LDAP hosts (#1717's shared HPC filesystems) this message serves.
|
||||
* A directory-service account is then shown by uid only. */
|
||||
static void posix_owner_name(uid_t uid, char out[POSIX_OWNER_NAME_CAP]) {
|
||||
out[0] = '\0';
|
||||
#if defined(__linux__)
|
||||
FILE *passwd = cbm_fopen("/etc/passwd", "r");
|
||||
if (!passwd) {
|
||||
return;
|
||||
}
|
||||
char line[POSIX_PASSWD_LINE_CAP];
|
||||
while (fgets(line, sizeof(line), passwd)) {
|
||||
char *name_end = strchr(line, ':');
|
||||
char *password_end = name_end ? strchr(name_end + 1, ':') : NULL;
|
||||
if (!password_end || name_end == line) {
|
||||
continue;
|
||||
}
|
||||
char *uid_end = NULL;
|
||||
errno = 0;
|
||||
unsigned long line_uid = strtoul(password_end + 1, &uid_end, 10);
|
||||
if (errno == 0 && uid_end != password_end + 1 && *uid_end == ':' &&
|
||||
line_uid == (unsigned long)uid) {
|
||||
(void)snprintf(out, POSIX_OWNER_NAME_CAP, "%.*s", (int)(name_end - line), line);
|
||||
break;
|
||||
}
|
||||
}
|
||||
(void)fclose(passwd);
|
||||
#else
|
||||
struct passwd entry;
|
||||
struct passwd *found = NULL;
|
||||
char storage[POSIX_PASSWD_LINE_CAP];
|
||||
if (getpwuid_r(uid, &entry, storage, sizeof(storage), &found) == 0 && found && found->pw_name) {
|
||||
(void)snprintf(out, POSIX_OWNER_NAME_CAP, "%s", found->pw_name);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/* The foreign-owned-ancestor refusal. The remedy is the part the reader acts
|
||||
* on, so it must survive the fixed detail buffer: the path is shortened from
|
||||
* the LEFT ("...") to fit, which keeps the refusing component at its end. */
|
||||
static void posix_foreign_owner_detail_set(const char *child_path, uid_t owner) {
|
||||
char name[POSIX_OWNER_NAME_CAP];
|
||||
posix_owner_name(owner, name);
|
||||
char owner_text[64];
|
||||
if (name[0]) {
|
||||
(void)snprintf(owner_text, sizeof(owner_text), "uid %ld (%s)", (long)owner, name);
|
||||
} else {
|
||||
(void)snprintf(owner_text, sizeof(owner_text), "uid %ld", (long)owner);
|
||||
}
|
||||
char tail[sizeof(ipc_validation_detail_buffer)];
|
||||
int tail_length = snprintf(tail, sizeof(tail),
|
||||
"' is owned by %s, not by you (uid %ld) or root; that account "
|
||||
"controls this path. Use a location whose parent directories are "
|
||||
"yours or root's (e.g. set CBM_CACHE_DIR)",
|
||||
owner_text, (long)geteuid());
|
||||
if (tail_length < 0) {
|
||||
return;
|
||||
}
|
||||
size_t used = (size_t)tail_length + 1; /* the opening quote */
|
||||
size_t room = used < sizeof(ipc_validation_detail_buffer)
|
||||
? sizeof(ipc_validation_detail_buffer) - 1 - used
|
||||
: 0;
|
||||
size_t path_length = strlen(child_path);
|
||||
const char *ellipsis = "";
|
||||
const char *shown = child_path;
|
||||
if (path_length > room) {
|
||||
ellipsis = "...";
|
||||
shown = child_path + path_length - (room > 3 ? room - 3 : 0);
|
||||
}
|
||||
ipc_validation_detail_set("'%s%s%s", ellipsis, shown, tail);
|
||||
}
|
||||
|
||||
static bool posix_directory_transition_secure(int parent_fd, int child_fd, const char *child_path) {
|
||||
struct stat parent;
|
||||
struct stat child;
|
||||
if (parent_fd < 0 || child_fd < 0 || !posix_directory_parent_secure(parent_fd) ||
|
||||
fstat(parent_fd, &parent) != 0 || fstat(child_fd, &child) != 0 ||
|
||||
!S_ISDIR(parent.st_mode) || !S_ISDIR(child.st_mode) ||
|
||||
!posix_directory_ancestor_owner_trusted(parent.st_uid) ||
|
||||
!posix_directory_ancestor_owner_trusted(child.st_uid)) {
|
||||
!posix_directory_ancestor_owner_trusted(posix_directory_observed_owner(&parent))) {
|
||||
return false;
|
||||
}
|
||||
uid_t child_owner = posix_directory_observed_owner(&child);
|
||||
if (!posix_directory_ancestor_owner_trusted(child_owner)) {
|
||||
/* #1717: this refusal used to set no detail, so the caller printed
|
||||
* "ancestry component validation failed (errno 17)" -- a stale EEXIST
|
||||
* from the mkdirat() on the existing component, naming neither the
|
||||
* directory nor the reason. The policy is unchanged: an account that
|
||||
* owns an ancestor can rename or replace everything below it after
|
||||
* this walk, so it could redirect the private directory. Name it. */
|
||||
posix_foreign_owner_detail_set(child_path, child_owner);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -1870,7 +2070,8 @@ static int private_directory_tree_open(const char *directory_path) {
|
||||
: -1;
|
||||
struct stat status;
|
||||
ok = next_fd >= 0 && fd_set_cloexec(next_fd) && fstat(next_fd, &status) == 0 &&
|
||||
S_ISDIR(status.st_mode) && posix_directory_transition_secure(current_fd, next_fd);
|
||||
S_ISDIR(status.st_mode) &&
|
||||
posix_directory_transition_secure(current_fd, next_fd, path);
|
||||
if (ok && created) {
|
||||
ok = status.st_uid == geteuid() && fchmod(next_fd, 0700) == 0 &&
|
||||
cbm_macos_extended_acl_fd_clear(next_fd) &&
|
||||
|
||||
@@ -92,6 +92,17 @@ bool cbm_daemon_ipc_private_directory_secure(const char *directory_path);
|
||||
* append it to their error messages; policy decisions never read it. */
|
||||
const char *cbm_daemon_ipc_validation_detail(void);
|
||||
|
||||
/* #1717: advice, never a refusal. When cache_root lives on a network
|
||||
* filesystem (Linux statfs: NFS, CIFS, SMB2, Lustre, GPFS), log one
|
||||
* `daemon.cache_root_network_fs` warning per process pointing at local
|
||||
* storage. Returns true only when this call emitted that warning. */
|
||||
bool cbm_daemon_ipc_warn_if_network_cache_root(const char *cache_root);
|
||||
#ifdef CBM_ENABLE_TEST_SEAMS
|
||||
/* Report `magic` as the cache root's statfs f_type on every platform (active),
|
||||
* or restore real detection; either way re-arm the once-per-process warning. */
|
||||
void cbm_daemon_ipc_set_fs_magic_for_test(bool active, unsigned long magic);
|
||||
#endif
|
||||
|
||||
/* This process's most recent listener-publication failure: the stage that
|
||||
* refused, the errno the failing step reported (0 when the step reported
|
||||
* none), and the artifact path it was operating on ("" when no path applies).
|
||||
@@ -127,6 +138,12 @@ bool cbm_daemon_ipc_win_sid_trusted_for_testing(void *sid);
|
||||
* so they can be tested without a chown-able overflow-owned directory. */
|
||||
void cbm_daemon_ipc_posix_set_ancestor_overflow_uid_for_test(bool active,
|
||||
unsigned long overflow_uid);
|
||||
/* #1717 seam (POSIX): report the directory at directory_path (matched by
|
||||
* dev/ino) as owned by foreign_uid during the ancestor walk. False when the
|
||||
* path is not a directory. Clear restores real ownership. */
|
||||
bool cbm_daemon_ipc_posix_set_foreign_owned_dir_for_test(const char *directory_path,
|
||||
unsigned long foreign_uid);
|
||||
void cbm_daemon_ipc_posix_clear_foreign_owned_dir_for_test(void);
|
||||
bool cbm_daemon_ipc_posix_uid_map_is_single_uid_for_test(const char *uid_map, unsigned long euid);
|
||||
bool cbm_daemon_ipc_posix_ancestor_stat_ok_for_test(unsigned long owner, unsigned int mode,
|
||||
unsigned long euid, bool overflow_active,
|
||||
|
||||
@@ -1455,6 +1455,8 @@ static main_build_identity_status_t main_build_identity(cbm_daemon_build_identit
|
||||
if (!cbm_daemon_ipc_private_directory_secure(canonical_cache)) {
|
||||
return MAIN_BUILD_IDENTITY_CACHE_PRIVATE;
|
||||
}
|
||||
/* #1717: advice only -- a network-filesystem cache root is accepted. */
|
||||
(void)cbm_daemon_ipc_warn_if_network_cache_root(canonical_cache);
|
||||
/* Every cache consumer in this process must use the exact path whose
|
||||
* fingerprint joins the account-wide cohort. Keeping an original symlink
|
||||
* spelling in the environment would let a later retarget move storage
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
#include <sys/types.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/wait.h>
|
||||
#include <pwd.h>
|
||||
#include <unistd.h>
|
||||
#ifdef __linux__
|
||||
#include <sched.h> /* #1830 userns real smoke: unshare(CLONE_NEWUSER). */
|
||||
@@ -5274,6 +5275,147 @@ TEST(daemon_ipc_posix_wsl_drvfs_refusal_names_remedy_issue1687) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #1717: an ancestor owned by ANOTHER non-root account (a shared filesystem
|
||||
* whose parent directory belongs to a colleague) is refused -- that account can
|
||||
* rename or replace everything below it, so the policy stays. What was wrong is
|
||||
* the message: the refusal set no detail, and the caller printed
|
||||
* "ancestry component validation failed (errno 17)", a stale EEXIST from
|
||||
* mkdirat() that named neither the directory nor the reason. The reporter
|
||||
* could not tell which directory, or why. The refusal must name the refusing
|
||||
* directory, its owner uid, and the remedy. The seam reports one directory as
|
||||
* foreign-owned because an unprivileged test cannot chown to another account. */
|
||||
enum { IPC_TEST_FOREIGN_MAX_DEPTH = 16 };
|
||||
|
||||
typedef struct {
|
||||
bool paths_ok;
|
||||
bool seam_set;
|
||||
bool refused;
|
||||
bool cache_created;
|
||||
bool secured_without_seam;
|
||||
size_t foreign_path_length;
|
||||
char detail[512];
|
||||
} ipc_test_foreign_result_t;
|
||||
|
||||
/* Build <parent>/<depth x 24-char dirs>/shared-fs, mark shared-fs as owned by
|
||||
* foreign_uid through the seam, and ask for <...>/shared-fs/cache. Deeper
|
||||
* chains produce a path long enough that the detail must be shortened. */
|
||||
static void ipc_test_foreign_ancestor_run(const char *tag, int depth, unsigned long foreign_uid,
|
||||
ipc_test_foreign_result_t *result) {
|
||||
char parent[TEST_PATH_CAP];
|
||||
char chain[IPC_TEST_FOREIGN_MAX_DEPTH + 1][TEST_PATH_CAP];
|
||||
char cache[TEST_PATH_CAP];
|
||||
int made = 0;
|
||||
memset(result, 0, sizeof(*result));
|
||||
result->paths_ok = depth <= IPC_TEST_FOREIGN_MAX_DEPTH && ipc_test_parent_new(parent, tag);
|
||||
const char *base = parent;
|
||||
for (int level = 0; result->paths_ok && level <= depth; level++) {
|
||||
const char *leaf = level == depth ? "shared-fs" : "level-component-24-chars";
|
||||
int w = snprintf(chain[level], sizeof(chain[level]), "%s/%s", base, leaf);
|
||||
result->paths_ok = w > 0 && w < (int)sizeof(chain[level]) && mkdir(chain[level], 0755) == 0;
|
||||
made += result->paths_ok ? 1 : 0;
|
||||
base = chain[level];
|
||||
}
|
||||
if (result->paths_ok) {
|
||||
int c = snprintf(cache, sizeof(cache), "%s/cache", chain[depth]);
|
||||
result->paths_ok = c > 0 && c < (int)sizeof(cache);
|
||||
result->foreign_path_length = strlen(chain[depth]);
|
||||
}
|
||||
if (result->paths_ok) {
|
||||
result->seam_set =
|
||||
cbm_daemon_ipc_posix_set_foreign_owned_dir_for_test(chain[depth], foreign_uid);
|
||||
}
|
||||
if (result->seam_set) {
|
||||
result->refused = !cbm_daemon_ipc_private_directory_secure(cache);
|
||||
const char *why = cbm_daemon_ipc_validation_detail();
|
||||
(void)snprintf(result->detail, sizeof(result->detail), "%s", why ? why : "");
|
||||
struct stat probe;
|
||||
result->cache_created = lstat(cache, &probe) == 0;
|
||||
cbm_daemon_ipc_posix_clear_foreign_owned_dir_for_test();
|
||||
/* Control: with real ownership the same path is accepted, so the
|
||||
* refusal above is the foreign owner and nothing else. */
|
||||
result->secured_without_seam = cbm_daemon_ipc_private_directory_secure(cache);
|
||||
(void)rmdir(cache);
|
||||
}
|
||||
cbm_daemon_ipc_posix_clear_foreign_owned_dir_for_test();
|
||||
while (made > 0) {
|
||||
(void)rmdir(chain[--made]);
|
||||
}
|
||||
if (depth <= IPC_TEST_FOREIGN_MAX_DEPTH) {
|
||||
ipc_test_remove_flat_dir(parent);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(daemon_ipc_posix_foreign_owned_ancestor_refusal_names_directory_issue1717) {
|
||||
/* A uid no account resolves to: the detail names the uid alone. */
|
||||
unsigned long foreign_uid = (unsigned long)geteuid() + 4242UL;
|
||||
ASSERT_TRUE(getpwuid((uid_t)foreign_uid) == NULL); /* precondition */
|
||||
ipc_test_foreign_result_t result;
|
||||
ipc_test_foreign_ancestor_run("posix-foreign-ancestor", 0, foreign_uid, &result);
|
||||
char owner_text[64];
|
||||
(void)snprintf(owner_text, sizeof(owner_text), "owned by uid %lu,", foreign_uid);
|
||||
|
||||
ASSERT_TRUE(result.paths_ok);
|
||||
ASSERT_TRUE(result.seam_set);
|
||||
ASSERT_TRUE(result.refused);
|
||||
ASSERT_TRUE(!result.cache_created); /* nothing is created below a foreign ancestor */
|
||||
ASSERT_NOT_NULL(strstr(result.detail, "/shared-fs'"));
|
||||
ASSERT_NOT_NULL(strstr(result.detail, owner_text));
|
||||
ASSERT_NOT_NULL(strstr(result.detail, "CBM_CACHE_DIR"));
|
||||
ASSERT_TRUE(strstr(result.detail, "errno") == NULL);
|
||||
ASSERT_TRUE(result.secured_without_seam);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #1717: the owner is named when the account resolves. uid 1 is a local system
|
||||
* account in /etc/passwd on macOS and on the Linux images we test ("daemon" on
|
||||
* both; Alpine calls it "bin"), so the expected name is taken from the same
|
||||
* local database rather than hardcoded. */
|
||||
TEST(daemon_ipc_posix_foreign_owned_ancestor_names_owner_account_issue1717) {
|
||||
const unsigned long named_uid = 1;
|
||||
struct passwd *expected = getpwuid((uid_t)named_uid);
|
||||
if (!expected || !expected->pw_name || !expected->pw_name[0]) {
|
||||
SKIP_PLATFORM("uid 1 has no local account name on this system");
|
||||
}
|
||||
char owner_text[96];
|
||||
(void)snprintf(owner_text, sizeof(owner_text), "owned by uid %lu (%s),", named_uid,
|
||||
expected->pw_name);
|
||||
ipc_test_foreign_result_t result;
|
||||
ipc_test_foreign_ancestor_run("posix-foreign-named", 0, named_uid, &result);
|
||||
|
||||
ASSERT_TRUE(result.paths_ok);
|
||||
ASSERT_TRUE(result.seam_set);
|
||||
ASSERT_TRUE(result.refused);
|
||||
ASSERT_NOT_NULL(strstr(result.detail, owner_text));
|
||||
ASSERT_NOT_NULL(strstr(result.detail, "/shared-fs'"));
|
||||
ASSERT_TRUE(result.secured_without_seam);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #1717: the detail buffer is fixed (384 bytes). A long path must not push the
|
||||
* remedy out: the path is shortened from the left, so the refusing component
|
||||
* and the whole remedy survive. */
|
||||
TEST(daemon_ipc_posix_foreign_owned_ancestor_long_path_keeps_remedy_issue1717) {
|
||||
const unsigned long named_uid = 1;
|
||||
ipc_test_foreign_result_t result;
|
||||
ipc_test_foreign_ancestor_run("posix-foreign-long", IPC_TEST_FOREIGN_MAX_DEPTH, named_uid,
|
||||
&result);
|
||||
static const char remedy[] =
|
||||
"Use a location whose parent directories are yours or root's (e.g. set CBM_CACHE_DIR)";
|
||||
|
||||
ASSERT_TRUE(result.paths_ok);
|
||||
ASSERT_TRUE(result.foreign_path_length > 384); /* the input really overflows */
|
||||
ASSERT_TRUE(result.refused);
|
||||
ASSERT_TRUE(strlen(result.detail) < 384);
|
||||
ASSERT_TRUE(strncmp(result.detail, "'...", 4) == 0);
|
||||
ASSERT_NOT_NULL(strstr(result.detail, "/shared-fs' is owned by uid 1"));
|
||||
size_t detail_length = strlen(result.detail);
|
||||
ASSERT_TRUE(detail_length >= sizeof(remedy) - 1);
|
||||
ASSERT_STR_EQ(result.detail + detail_length - (sizeof(remedy) - 1), remedy);
|
||||
PASS();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CBM_ENABLE_TEST_SEAMS
|
||||
/* #1830: /proc/self/uid_map single-uid detection. A single-uid map is exactly
|
||||
* one line "<inside> <outside> 1" whose inside id is our euid; anything else —
|
||||
* the init map, a count other than 1, a foreign inside id, extra lines, or junk
|
||||
@@ -5496,6 +5638,65 @@ static void ipc_test_log_capture_sink(const char *line) {
|
||||
ipc_test_log_capture[ipc_test_log_capture_used] = '\0';
|
||||
}
|
||||
|
||||
#ifdef CBM_ENABLE_TEST_SEAMS
|
||||
/* #1717: a cache root on a network filesystem (shared HPC home, NFS/SMB mount)
|
||||
* is ACCEPTED, but SQLite WAL is unreliable there, so startup logs ONE warning
|
||||
* with the local-storage advice. The seam supplies the statfs f_type on every
|
||||
* platform (real detection is Linux-only), so the mapping and the
|
||||
* once-per-process rule are bound deterministically here. */
|
||||
static bool ipc_test_network_warning_emitted(unsigned long magic, const char *fs_name) {
|
||||
ipc_test_log_capture_used = 0;
|
||||
ipc_test_log_capture[0] = '\0';
|
||||
cbm_daemon_ipc_set_fs_magic_for_test(true, magic);
|
||||
cbm_log_set_sink_ex(ipc_test_log_capture_sink, CBM_LOG_SINK_REPLACE);
|
||||
bool warned = cbm_daemon_ipc_warn_if_network_cache_root("/shared/fs/me/.cache/cbm");
|
||||
cbm_log_set_sink(NULL);
|
||||
char expected[32];
|
||||
(void)snprintf(expected, sizeof(expected), "fs=%s", fs_name);
|
||||
const char *line = strstr(ipc_test_log_capture, "daemon.cache_root_network_fs");
|
||||
return warned && line && strstr(line, expected) && strstr(line, "CBM_CACHE_DIR") &&
|
||||
strstr(line, "path=/shared/fs/me/.cache/cbm");
|
||||
}
|
||||
|
||||
TEST(daemon_ipc_network_cache_root_warns_once_issue1717) {
|
||||
CBMLogLevel saved_level = cbm_log_get_level();
|
||||
cbm_log_set_level(CBM_LOG_WARN);
|
||||
bool nfs = ipc_test_network_warning_emitted(0x6969UL, "NFS");
|
||||
bool cifs = ipc_test_network_warning_emitted(0xFF534D42UL, "CIFS");
|
||||
bool smb2 = ipc_test_network_warning_emitted(0xFE534D42UL, "SMB2");
|
||||
bool lustre = ipc_test_network_warning_emitted(0x0BD00BD0UL, "Lustre");
|
||||
bool gpfs = ipc_test_network_warning_emitted(0x47504653UL, "GPFS");
|
||||
|
||||
/* Once per process: a second startup check stays silent. */
|
||||
cbm_daemon_ipc_set_fs_magic_for_test(true, 0x6969UL);
|
||||
bool first = cbm_daemon_ipc_warn_if_network_cache_root("/nfs/cache");
|
||||
bool second = cbm_daemon_ipc_warn_if_network_cache_root("/nfs/cache");
|
||||
|
||||
/* Local filesystems (ext4, xfs, tmpfs) never warn. */
|
||||
cbm_daemon_ipc_set_fs_magic_for_test(true, 0xEF53UL);
|
||||
bool ext4 = cbm_daemon_ipc_warn_if_network_cache_root("/home/me/.cache/cbm");
|
||||
cbm_daemon_ipc_set_fs_magic_for_test(true, 0x58465342UL);
|
||||
bool xfs = cbm_daemon_ipc_warn_if_network_cache_root("/home/me/.cache/cbm");
|
||||
cbm_daemon_ipc_set_fs_magic_for_test(true, 0x01021994UL);
|
||||
bool tmpfs = cbm_daemon_ipc_warn_if_network_cache_root("/home/me/.cache/cbm");
|
||||
|
||||
cbm_daemon_ipc_set_fs_magic_for_test(false, 0);
|
||||
cbm_log_set_level(saved_level);
|
||||
|
||||
ASSERT_TRUE(nfs);
|
||||
ASSERT_TRUE(cifs);
|
||||
ASSERT_TRUE(smb2);
|
||||
ASSERT_TRUE(lustre);
|
||||
ASSERT_TRUE(gpfs);
|
||||
ASSERT_TRUE(first);
|
||||
ASSERT_TRUE(!second);
|
||||
ASSERT_TRUE(!ext4);
|
||||
ASSERT_TRUE(!xfs);
|
||||
ASSERT_TRUE(!tmpfs);
|
||||
PASS();
|
||||
}
|
||||
#endif
|
||||
|
||||
/* #1828: a full /tmp made every daemon start die at pending publication, and
|
||||
* the only durable trace was `daemon.ipc.listen_failed stage=pending_publication`
|
||||
* -- no syscall, no errno, no path. The reporter needed hours (and a wrong
|
||||
@@ -5599,6 +5800,9 @@ SUITE(daemon_ipc) {
|
||||
RUN_TEST(daemon_ipc_posix_world_writable_ancestor_still_refused_issue1537);
|
||||
#ifdef CBM_ENABLE_TEST_SEAMS
|
||||
RUN_TEST(daemon_ipc_posix_wsl_drvfs_refusal_names_remedy_issue1687);
|
||||
RUN_TEST(daemon_ipc_posix_foreign_owned_ancestor_refusal_names_directory_issue1717);
|
||||
RUN_TEST(daemon_ipc_posix_foreign_owned_ancestor_names_owner_account_issue1717);
|
||||
RUN_TEST(daemon_ipc_posix_foreign_owned_ancestor_long_path_keeps_remedy_issue1717);
|
||||
RUN_TEST(daemon_ipc_posix_uid_map_single_uid_parse_issue1830);
|
||||
RUN_TEST(daemon_ipc_posix_overflow_ancestor_tolerated_only_in_single_uid_ns_issue1830);
|
||||
RUN_TEST(daemon_ipc_posix_overflow_uid_is_never_cached_issue1830);
|
||||
@@ -5633,6 +5837,9 @@ SUITE(daemon_ipc) {
|
||||
RUN_TEST(daemon_ipc_posix_rejects_non_socket_and_symlink_endpoints);
|
||||
#ifndef _WIN32
|
||||
RUN_TEST(daemon_ipc_listen_failure_names_errno_and_path);
|
||||
#ifdef CBM_ENABLE_TEST_SEAMS
|
||||
RUN_TEST(daemon_ipc_network_cache_root_warns_once_issue1717);
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
Reference in new issue
Block a user