Skip to content

Commit 19f5bdf

Browse files
natengallodev
authored andcommitted
fix workspace cache root authorization
1 parent e529339 commit 19f5bdf

2 files changed

Lines changed: 71 additions & 33 deletions

File tree

‎src/foundation/workspace.c‎

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -374,16 +374,12 @@ cbm_ws_verdict_t cbm_workspace_classify_root(const char *canonical_path, const c
374374
return CBM_WS_DENY_ABSOLUTE;
375375
}
376376

377-
/* Order matters below, and not for cosmetic reasons.
378-
*
379-
* The home directory normally CONTAINS the cache directory, so testing the
380-
* cache first would report every $HOME as "holds the cache" — and, worse,
381-
* would make it absolutely denied when the design says a person may override
382-
* it. Home is therefore classified first.
383-
*
384-
* Depth comes before the cache test for the same kind of reason: "/Users" is
385-
* both too broad and an ancestor of the cache, and "too broad, name a
386-
* project directory below it" is the reason that actually helps the reader. */
377+
/* This absolute denial precedes every overridable verdict: a cache path,
378+
* its parent, or a home that contains it must not be grantable. */
379+
if (cache_dir && cache_dir[0] && ws_is_ancestor_or_equal(canonical_path, cache_dir)) {
380+
return CBM_WS_DENY_ABSOLUTE;
381+
}
382+
387383
if (home_dir && home_dir[0] && ws_paths_equal(canonical_path, home_dir)) {
388384
return CBM_WS_DENY_SENSITIVE;
389385
}
@@ -397,20 +393,6 @@ cbm_ws_verdict_t cbm_workspace_classify_root(const char *canonical_path, const c
397393
return CBM_WS_DENY_TOO_SHALLOW;
398394
}
399395

400-
/* No rule here for "this root contains the cache directory".
401-
*
402-
* An earlier draft refused such roots outright, on the theory that indexing
403-
* them would absorb every other project's graph database. Two things make
404-
* that wrong. The indexer only parses recognised source files, and a graph
405-
* database is binary SQLite it would never extract; and refusing an entire
406-
* root is the wrong remedy even where the concern holds — the right one is to
407-
* not walk the cache. Excluding the cache from discovery is tracked
408-
* separately; classifying the root is not the place for it.
409-
*
410-
* cache_dir stays in the signature because the exclusion work will need it
411-
* and because callers already have it to hand. */
412-
(void)cache_dir;
413-
414396
if (ws_any_component_matches(canonical_path, WS_CREDENTIAL_NAMES,
415397
sizeof(WS_CREDENTIAL_NAMES) / sizeof(WS_CREDENTIAL_NAMES[0]),
416398
windows_style)) {

‎tests/test_workspace.c‎

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,15 @@ TEST(ws_non_absolute_paths_are_denied) {
6565
/* One depth rule refuses every POSIX top-level tree without a list to maintain.
6666
* This is the whole reason depth carries its weight. */
6767
TEST(ws_posix_top_level_trees_are_too_shallow) {
68-
static const char *const shallow[] = {"/etc", "/home", "/Users", "/var",
69-
"/opt", "/srv", "/usr", "/private/etc",
70-
"/private/var", "/private/tmp"};
68+
static const char *const shallow[] = {"/etc", "/home", "/var",
69+
"/opt", "/srv", "/usr",
70+
"/private/etc", "/private/var", "/private/tmp"};
7171
for (size_t i = 0; i < sizeof(shallow) / sizeof(shallow[0]); i++) {
7272
ASSERT_EQ(cbm_workspace_classify_root(shallow[i], HOME, CACHE), CBM_WS_DENY_TOO_SHALLOW);
7373
}
74+
/* This injected cache lives under /Users, so the cache-containing denial
75+
* takes precedence over the otherwise shallow-root verdict. */
76+
ASSERT_EQ(cbm_workspace_classify_root("/Users", HOME, CACHE), CBM_WS_DENY_ABSOLUTE);
7477
ASSERT_FALSE(cbm_workspace_verdict_is_overridable(CBM_WS_DENY_TOO_SHALLOW));
7578
PASS();
7679
}
@@ -103,6 +106,22 @@ TEST(ws_allow_broad_root_lifts_too_shallow_for_an_exact_match_only) {
103106
ASSERT_NOT_NULL(strstr(err, "--approve-sensitive"));
104107
ASSERT_FALSE(cbm_workspace_grant_list(cache, listed, sizeof(listed)));
105108

109+
/* Configured roots and broader or differently marked store entries are not
110+
* human approval of this exact broad root. */
111+
ASSERT_FALSE(cbm_workspace_root_allowed("/usr", HOME, cache, "/", err, sizeof(err)));
112+
char store[1024];
113+
ASSERT_TRUE(cbm_workspace_grant_path(cache, store, sizeof(store)));
114+
FILE *f = cbm_fopen(store, "a");
115+
ASSERT_NOT_NULL(f);
116+
(void)fprintf(f, "/\n!/usr\n");
117+
ASSERT_EQ(fclose(f), 0);
118+
ASSERT_FALSE(cbm_workspace_root_allowed("/usr", HOME, cache, NULL, err, sizeof(err)));
119+
ASSERT_NOT_NULL(strstr(err, "--approve-broad"));
120+
ASSERT_FALSE(cbm_workspace_root_allowed("/usr", HOME, cache, "/", err, sizeof(err)));
121+
f = cbm_fopen(store, "w");
122+
ASSERT_NOT_NULL(f);
123+
ASSERT_EQ(fclose(f), 0);
124+
106125
/* The explicit approval is recorded and marked. */
107126
ASSERT_TRUE(cbm_workspace_grant_add(cache, HOME, "/usr", false, true, err, sizeof(err)));
108127
ASSERT_TRUE(cbm_workspace_grant_list(cache, listed, sizeof(listed)));
@@ -123,16 +142,23 @@ TEST(ws_allow_broad_root_lifts_too_shallow_for_an_exact_match_only) {
123142
ASSERT_TRUE(cbm_workspace_root_allowed("/usr", HOME, cache, "/", err, sizeof(err)));
124143
ASSERT_FALSE(cbm_workspace_root_allowed("/dev", HOME, cache, "/", err, sizeof(err)));
125144
ASSERT_NOT_NULL(strstr(err, "allow-root --approve-broad /dev"));
145+
146+
char alias[1024], canonical_alias[1024], canonical_usr[1024];
147+
snprintf(alias, sizeof(alias), "%s/usr-alias", cache);
148+
ASSERT_EQ(symlink("/usr", alias), 0);
149+
ASSERT_TRUE(cbm_canonical_path(alias, canonical_alias, sizeof(canonical_alias)));
150+
ASSERT_TRUE(cbm_canonical_path("/usr", canonical_usr, sizeof(canonical_usr)));
151+
ASSERT_STR_EQ(canonical_alias, canonical_usr);
152+
ASSERT_TRUE(cbm_workspace_root_allowed(canonical_alias, HOME, cache, NULL, err, sizeof(err)));
126153
#endif
127154

128155
/* Each marker lifts only its own verdict: a broad mark on a credential
129156
* directory, which grant_add would never write, still does not lift it. */
130157
char secret[1024];
131158
snprintf(secret, sizeof(secret), "%s/.ssh", cache);
132159
ASSERT_EQ(cbm_mkdir(secret), 0);
133-
char store[1024];
134160
ASSERT_TRUE(cbm_workspace_grant_path(cache, store, sizeof(store)));
135-
FILE *f = cbm_fopen(store, "a");
161+
f = cbm_fopen(store, "a");
136162
ASSERT_NOT_NULL(f);
137163
(void)fprintf(f, "^%s\n", secret);
138164
ASSERT_EQ(fclose(f), 0);
@@ -143,6 +169,31 @@ TEST(ws_allow_broad_root_lifts_too_shallow_for_an_exact_match_only) {
143169
PASS();
144170
}
145171

172+
TEST(ws_cache_containing_roots_are_never_overridable) {
173+
char *created = th_mktempdir("cbm_ws_cache_root");
174+
ASSERT_NOT_NULL(created);
175+
char path[1024], root[1024], cache_path[1024], cache[1024], err[1024], store[1024];
176+
snprintf(path, sizeof(path), "%s", created);
177+
snprintf(cache_path, sizeof(cache_path), "%s/cache", path);
178+
ASSERT_EQ(cbm_mkdir(cache_path), 0);
179+
ASSERT_TRUE(cbm_canonical_path(path, root, sizeof(root)));
180+
ASSERT_TRUE(cbm_canonical_path(cache_path, cache, sizeof(cache)));
181+
ASSERT_EQ(cbm_workspace_classify_root(root, NULL, cache), CBM_WS_DENY_ABSOLUTE);
182+
ASSERT_EQ(cbm_workspace_classify_root(root, root, cache), CBM_WS_DENY_ABSOLUTE);
183+
ASSERT_EQ(cbm_workspace_classify_root(cache, NULL, cache), CBM_WS_DENY_ABSOLUTE);
184+
ASSERT_FALSE(cbm_workspace_grant_add(cache, NULL, root, false, true, err, sizeof(err)));
185+
ASSERT_NOT_NULL(strstr(err, "cannot be indexed"));
186+
ASSERT_FALSE(cbm_workspace_grant_add(cache, root, root, true, false, err, sizeof(err)));
187+
ASSERT_TRUE(cbm_workspace_grant_path(cache, store, sizeof(store)));
188+
FILE *f = cbm_fopen(store, "a");
189+
ASSERT_NOT_NULL(f);
190+
(void)fprintf(f, "^%s\n", root);
191+
ASSERT_EQ(fclose(f), 0);
192+
ASSERT_FALSE(cbm_workspace_root_allowed(root, NULL, cache, root, err, sizeof(err)));
193+
th_cleanup(root);
194+
PASS();
195+
}
196+
146197
/* Legitimately shallow project roots must survive: these are the false positives
147198
* a blanket depth rule would cause, which is why Windows counts drive-relative. */
148199
TEST(ws_legitimate_shallow_roots_are_allowed) {
@@ -156,11 +207,15 @@ TEST(ws_legitimate_shallow_roots_are_allowed) {
156207

157208
/* $HOME is depth 2 on both macOS and Linux, so depth cannot see it. */
158209
TEST(ws_home_itself_is_sensitive_but_subdirs_are_fine) {
159-
ASSERT_EQ(cbm_workspace_classify_root("/Users/dev", HOME, CACHE), CBM_WS_DENY_SENSITIVE);
160-
ASSERT_EQ(cbm_workspace_classify_root("/Users/dev/", HOME, CACHE), CBM_WS_DENY_SENSITIVE);
161-
ASSERT_EQ(cbm_workspace_classify_root("/Users/dev/projects", HOME, CACHE), CBM_WS_ALLOW);
210+
ASSERT_EQ(cbm_workspace_classify_root("/Users/dev", HOME, "/var/cache/cbm"),
211+
CBM_WS_DENY_SENSITIVE);
212+
ASSERT_EQ(cbm_workspace_classify_root("/Users/dev/", HOME, "/var/cache/cbm"),
213+
CBM_WS_DENY_SENSITIVE);
214+
ASSERT_EQ(cbm_workspace_classify_root("/Users/dev/projects", HOME, "/var/cache/cbm"),
215+
CBM_WS_ALLOW);
162216
/* A sibling that merely shares a prefix is not the home directory. */
163-
ASSERT_EQ(cbm_workspace_classify_root("/Users/developer", HOME, CACHE), CBM_WS_ALLOW);
217+
ASSERT_EQ(cbm_workspace_classify_root("/Users/developer", HOME, "/var/cache/cbm"),
218+
CBM_WS_ALLOW);
164219
ASSERT_TRUE(cbm_workspace_verdict_is_overridable(CBM_WS_DENY_SENSITIVE));
165220
PASS();
166221
}
@@ -987,6 +1042,7 @@ SUITE(workspace) {
9871042
RUN_TEST(ws_non_absolute_paths_are_denied);
9881043
RUN_TEST(ws_posix_top_level_trees_are_too_shallow);
9891044
RUN_TEST(ws_allow_broad_root_lifts_too_shallow_for_an_exact_match_only);
1045+
RUN_TEST(ws_cache_containing_roots_are_never_overridable);
9901046
RUN_TEST(ws_legitimate_shallow_roots_are_allowed);
9911047
RUN_TEST(ws_home_itself_is_sensitive_but_subdirs_are_fine);
9921048
RUN_TEST(ws_credential_directories_are_sensitive_at_any_depth);

0 commit comments

Comments
 (0)