@@ -776,6 +776,43 @@ bool cbm_workspace_manifest_is_approved(const char *cache_dir, const char *proje
776776 return found ;
777777}
778778
779+ /* Resolve a manifest entry to the directory it names — the form the policy is
780+ * defined over. The file holds the entry as a person wrote it, and a spelling
781+ * can reach a directory through a link or through `..` segments; the directory
782+ * that would be indexed is the resolved one, so that is the one classified, at
783+ * approval and again at use. Only an absolute entry is accepted: the manifest
784+ * is read by a long-lived process whose working directory is unrelated to the
785+ * project, so a relative spelling has no base it could honestly be resolved
786+ * against. Returns NULL on success, otherwise the reason the entry is unusable.
787+ * out must hold WS_LINE_MAX bytes. */
788+ static const char * ws_manifest_entry_resolve (const char * entry , char * out , size_t out_sz ) {
789+ if (ws_volume_prefix_len (entry ) == 0 ) {
790+ return "must be an absolute path" ;
791+ }
792+ if (!cbm_canonical_path (entry , out , out_sz )) {
793+ return "must name an existing directory" ;
794+ }
795+ if (!cbm_is_dir (out )) {
796+ return "is not a directory" ;
797+ }
798+ return NULL ;
799+ }
800+
801+ /* "requested path <entry>: <reason>", with the resolved form shown when it
802+ * differs from what was written: the person typed one and the policy judged
803+ * the other. */
804+ static void ws_manifest_entry_refusal (char * err , size_t err_sz , const char * entry ,
805+ const char * resolved , const char * reason ) {
806+ if (!err || err_sz == 0 ) {
807+ return ;
808+ }
809+ if (resolved && strcmp (resolved , entry ) != 0 ) {
810+ snprintf (err , err_sz , "requested path %s (resolves to %s): %s" , entry , resolved , reason );
811+ } else {
812+ snprintf (err , err_sz , "requested path %s: %s" , entry , reason );
813+ }
814+ }
815+
779816bool cbm_workspace_manifest_approve (const char * cache_dir , const char * home_dir ,
780817 const char * project_root , char * err , size_t err_sz ) {
781818 if (err && err_sz ) {
@@ -800,14 +837,19 @@ bool cbm_workspace_manifest_approve(const char *cache_dir, const char *home_dir,
800837 }
801838
802839 /* Approving a manifest must not become a way around the breadth policy: every
803- * requested entry has to stand on its own as an indexing root. */
840+ * requested entry has to stand on its own as an indexing root, judged by the
841+ * directory it resolves to. */
804842 for (int i = 0 ; i < m .count ; i ++ ) {
805- cbm_ws_verdict_t v = cbm_workspace_classify_root (m .entries [i ], home_dir , cache_dir );
843+ char resolved [WS_LINE_MAX ];
844+ const char * unusable = ws_manifest_entry_resolve (m .entries [i ], resolved , sizeof (resolved ));
845+ if (unusable ) {
846+ ws_manifest_entry_refusal (err , err_sz , m .entries [i ], NULL , unusable );
847+ return false;
848+ }
849+ cbm_ws_verdict_t v = cbm_workspace_classify_root (resolved , home_dir , cache_dir );
806850 if (v != CBM_WS_ALLOW ) {
807- if (err ) {
808- snprintf (err , err_sz , "requested path %s: %s" , m .entries [i ],
809- cbm_workspace_verdict_reason (v ));
810- }
851+ ws_manifest_entry_refusal (err , err_sz , m .entries [i ], resolved ,
852+ cbm_workspace_verdict_reason (v ));
811853 return false;
812854 }
813855 }
@@ -850,12 +892,19 @@ bool cbm_workspace_manifest_allows(const char *cache_dir, const char *home_dir,
850892 return false;
851893 }
852894 for (int i = 0 ; i < m .count ; i ++ ) {
853- /* Re-classify at use time as well as at approval time: the credential list
854- * may have grown since, and a stored approval must not outrank it. */
855- if (cbm_workspace_classify_root (m .entries [i ], home_dir , cache_dir ) != CBM_WS_ALLOW ) {
895+ /* Resolve and re-classify at use time as well as at approval time: the
896+ * credential list may have grown since, and a link may lead somewhere
897+ * else than it did when the person approved it. A stored approval must
898+ * not outrank either. An entry that no longer resolves matches nothing,
899+ * and the other entries of the manifest are consulted as before. */
900+ char resolved [WS_LINE_MAX ];
901+ if (ws_manifest_entry_resolve (m .entries [i ], resolved , sizeof (resolved )) != NULL ) {
902+ continue ;
903+ }
904+ if (cbm_workspace_classify_root (resolved , home_dir , cache_dir ) != CBM_WS_ALLOW ) {
856905 continue ;
857906 }
858- if (cbm_path_within_root (m . entries [ i ] , candidate )) {
907+ if (cbm_path_within_root (resolved , candidate )) {
859908 return true;
860909 }
861910 }
0 commit comments