From 9f57edfa576072398728eda2c6fc2585d2ecb063 Mon Sep 17 00:00:00 2001 From: Amir Fathi Date: Mon, 7 Sep 2026 08:00:09 +0000 Subject: [PATCH] fix(pipeline): skip an unreadable subdirectory in the control-file walk instead of aborting the whole index semantic_manifest_walk_controls() returns CBM_NOT_FOUND the moment cbm_opendir() fails on any subdirectory, and that propagates all the way up through cbm_pipeline_build_semantic_manifest(), aborting the entire index_repository run. discover.c's walk_dir() already treats the same failure (a permission-denied directory it can't open) as "nothing to discover here" and moves on. The control walk has no reason to be stricter: it only looks for .gitignore/.cbmignore/package-manifest files, which a locked-out directory simply doesn't contribute. Skip it and keep walking, except at depth 0, where an inaccessible manifest root is still a real error (pipeline_semantic_manifest_rejects_non_directory_root pins that). Signed-off-by: Amir Fathi --- src/pipeline/pipeline_incremental.c | 5 +++- tests/test_pipeline.c | 42 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index bf71a1f84..aa7d04321 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -338,7 +338,10 @@ static int semantic_manifest_walk_controls(semantic_manifest_builder_t *builder, } cbm_dir_t *dir = cbm_opendir(abs_dir); if (!dir) { - return CBM_NOT_FOUND; + /* A nested directory we can't open has no control files to + * contribute; skip it like discover.c's walk_dir already does. + * The root call (depth 0) still fails closed. */ + return depth == 0 ? CBM_NOT_FOUND : 0; } int rc = 0; cbm_dirent_t *entry; diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 5608ee559..eae749bc1 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -4123,6 +4123,45 @@ TEST(pipeline_semantic_manifest_rejects_non_directory_root) { PASS(); } +#ifndef _WIN32 +/* A nested directory the process cannot open (permission-denied) must not + * abort the whole manifest walk: it has no control files to contribute, so + * it is skipped, and control files in sibling directories still get found. + * Root-not-a-directory (above) is a different, still fail-closed case. */ +TEST(pipeline_semantic_manifest_skips_unreadable_subdirectory) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_manifest_locked_dir_XXXXXX"); + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + write_temp_file(tmp, "readable.py", "def Readable():\n return 1\n"); + write_temp_file(tmp, "locked/secret.txt", "should never be opened\n"); + write_temp_file(tmp, "sibling/.gitignore", "*.log\n"); + + char locked_path[512]; + snprintf(locked_path, sizeof(locked_path), "%s/locked", tmp); + ASSERT_EQ(chmod(locked_path, 0), 0); + + cbm_file_hash_t *manifest = NULL; + int manifest_count = -1; + int rc = cbm_pipeline_build_semantic_manifest("manifest-skip-locked", tmp, NULL, 0, NULL, 0, + NULL, NULL, &manifest, &manifest_count); + + bool found_sibling_gitignore = false; + for (int i = 0; i < manifest_count; i++) { + if (manifest[i].rel_path && strcmp(manifest[i].rel_path, "sibling/.gitignore") == 0) { + found_sibling_gitignore = true; + } + } + + chmod(locked_path, 0755); + cbm_pipeline_free_semantic_manifest(manifest, manifest_count > 0 ? manifest_count : 0); + th_rmtree(tmp); + + ASSERT_EQ(rc, 0); + ASSERT_TRUE(found_sibling_gitignore); + PASS(); +} +#endif /* !_WIN32 */ + /* A fully validated staged graph must be able to recover from a definitely * non-SQLite destination without deleting evidence or overwriting an earlier * quarantine. The replacement happens only after the corrupt bytes are moved. */ @@ -13862,6 +13901,9 @@ SUITE(pipeline_semantic_manifest_repro) { RUN_TEST(pipeline_incremental_successful_publication_preserves_adr); RUN_TEST(pipeline_full_adr_capture_failure_preserves_previous_generation); RUN_TEST(pipeline_semantic_manifest_rejects_non_directory_root); +#ifndef _WIN32 + RUN_TEST(pipeline_semantic_manifest_skips_unreadable_subdirectory); +#endif RUN_TEST(pipeline_full_reindex_quarantines_corrupt_destination_without_overwrite); RUN_TEST(pipeline_full_reindex_replaces_legacy_schema_without_quarantine); #endif