Skip to content

Commit 60f6253

Browse files
authored
Merge pull request #2490 from DeusData/fix/workspace-manifest-resolved-entries
fix(workspace): classify manifest entries in their resolved form
2 parents df143c9 + 1d7c807 commit 60f6253

3 files changed

Lines changed: 441 additions & 27 deletions

File tree

‎src/foundation/workspace.c‎

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
779816
bool 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
}

‎src/foundation/workspace.h‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,19 @@ bool cbm_workspace_manifest_read(const char *project_root, cbm_ws_manifest_t *ou
145145
bool cbm_workspace_manifest_is_approved(const char *cache_dir, const char *project_root,
146146
const cbm_ws_manifest_t *manifest);
147147

148-
/* Record approval for the manifest currently on disk. Refuses when a requested
149-
* entry would not be allowable as a root on its own — approving a manifest must
150-
* not become a way around the breadth policy. */
148+
/* Record approval for the manifest currently on disk. Each entry is judged in
149+
* its resolved form (links followed, `..` collapsed), since that is the
150+
* directory that would be indexed; an entry that is relative, does not exist
151+
* or is not a directory is refused, as is one that would not be allowable as a
152+
* root on its own — approving a manifest must not become a way around the
153+
* breadth policy. err names the entry as written and, when it differs, the
154+
* directory it resolves to. */
151155
bool cbm_workspace_manifest_approve(const char *cache_dir, const char *home_dir,
152156
const char *project_root, char *err, size_t err_sz);
153157

154-
/* True when candidate is at or below an APPROVED manifest entry of project_root. */
158+
/* True when candidate is at or below an APPROVED manifest entry of project_root.
159+
* Entries are resolved and classified again here: approval binds to the
160+
* manifest text, and where an entry leads is read at the time it is used. An
161+
* entry that no longer resolves matches nothing. */
155162
bool cbm_workspace_manifest_allows(const char *cache_dir, const char *home_dir,
156163
const char *project_root, const char *candidate);

0 commit comments

Comments
 (0)