@@ -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. */
6767TEST (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. */
148199TEST (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. */
158209TEST (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