Skip to content

Commit 38d422f

Browse files
DeusDataahundt
andcommitted
fix(routes): a document or log path is not an HTTP route literal
Distilled from #1245 by Andrew Hundt (a slice of 0370d52). detect_url_in_args in the parallel resolver runs for every resolved call with no callee gating. A first argument like "/new/file.txt" clears normalize_url_arg (it has a second slash and no whitespace) and then reaches cbm_service_pattern_is_http_route_literal, whose filesystem extension list knew about config, key and socket files but not about documents or logs. So open_file("/new/file.txt") read_file("/docs/guide.md") write_file("/data/app.log") each minted a Route node and an HTTP_CALLS edge -- the graph reported a web endpoint where the program opens a file. Five extensions join the list: .log .md .pdf .rst .txt, alphabetical order preserved. Two things the tests are careful about, because the obvious fixture would not have been RED. A bare Python builtin open(...) never resolves to an indexed target, so it never reaches the URL detector at all; the bug shows only for calls that resolve to a project symbol, which is what the fixture defines. And /var/... is already rejected by the filesystem ROOT list, so the fixture paths start with segments that list does not know, leaving the extension guard as the only thing that can reject them. RED before the fix: infrascan_http_route_literal_guard_rejects_document_extensions FAIL tests/test_infrascan.c:44: ASSERT(!(cbm_service_pattern_is_http_route_literal("/new/file.txt", "open"))) pipeline_arg_url_rejects_document_file_paths FAIL tests/test_pipeline.c:6652: count_nodes_named(s, project, "/new/file.txt") == 1, expected 0 == 0 The pipeline test keeps a positive control above that line -- a genuine requests.get("/api/items") still yields its Route node and HTTP_CALLS edge -- so the guard cannot pass by rejecting everything. GREEN after: infrascan 4 passed; pipeline 282 passed; extraction edge_types_probe route_canon 420 passed. Co-authored-by: Andrew Hundt <ATHundt@gmail.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1 parent bedbc0a commit 38d422f

3 files changed

Lines changed: 118 additions & 3 deletions

File tree

‎internal/cbm/service_patterns.c‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -752,9 +752,9 @@ static bool has_filesystem_extension(const char *path) {
752752
ext[ext_len] = '\0';
753753

754754
static const char *const hard_file_exts[] = {
755-
".cfg", ".conf", ".credentials", ".crt", ".db", ".env",
756-
".ini", ".key", ".pem", ".pid", ".properties", ".service",
757-
".sock", ".socket", ".sqlite", ".toml", NULL};
755+
".cfg", ".conf", ".credentials", ".crt", ".db", ".env", ".ini", ".key",
756+
".log", ".md", ".pdf", ".pem", ".pid", ".properties", ".rst", ".service",
757+
".sock", ".socket", ".sqlite", ".toml", ".txt", NULL};
758758
for (int i = 0; hard_file_exts[i]; i++) {
759759
if (path_ext_matches(ext, hard_file_exts[i])) {
760760
return true;

‎tests/test_infrascan.c‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,25 @@ TEST(infrascan_service_pattern_match_keeps_real_library_qns) {
225225
PASS();
226226
}
227227

228+
/* Document/log extensions are never HTTP routes (distilled from PR #1245):
229+
* the arg-URL detector runs for every resolved call, so without the
230+
* extension guard `open("/new/file.txt")` minted a Route + HTTP_CALLS.
231+
* First segments deliberately avoid the filesystem-root list so ONLY the
232+
* extension guard can reject them; `/api/...` marker paths stay routes. */
233+
TEST(infrascan_http_route_literal_guard_rejects_document_extensions) {
234+
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/new/file.txt", "open"));
235+
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/docs/guide.md", "read"));
236+
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/data/app.log", "write"));
237+
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/fake/path.pdf", "open"));
238+
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/docs/index.rst", "open"));
239+
/* Same extensions under a query string are still files. */
240+
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/docs/guide.md?raw=1", "open"));
241+
/* Positive controls: extension-less routes and the same callees. */
242+
ASSERT_TRUE(cbm_service_pattern_is_http_route_literal("/api/items", "requests.get"));
243+
ASSERT_TRUE(cbm_service_pattern_is_http_route_literal("/docs/guide", "open"));
244+
PASS();
245+
}
246+
228247
TEST(infrascan_route_nodes_skip_bad_http_url_paths) {
229248
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp/cbm_infrascan_route_guard");
230249
ASSERT_NOT_NULL(gb);
@@ -305,6 +324,7 @@ TEST(infrascan_http_calls_join_matching_handler_route) {
305324

306325
SUITE(infrascan) {
307326
RUN_TEST(infrascan_http_route_literal_guard_rejects_filesystem_paths);
327+
RUN_TEST(infrascan_http_route_literal_guard_rejects_document_extensions);
308328
RUN_TEST(infrascan_service_pattern_match_rejects_ids_inside_words);
309329
RUN_TEST(infrascan_service_pattern_match_keeps_real_library_qns);
310330
RUN_TEST(infrascan_route_nodes_skip_bad_http_url_paths);

‎tests/test_pipeline.c‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7517,6 +7517,100 @@ TEST(pipeline_ts_config_object_url_http_calls_issue2235) {
75177517
PASS();
75187518
}
75197519

7520+
/* Count HTTP_CALLS-style edges whose url_path property contains `keyword`. */
7521+
static int count_edges_with_url_path(cbm_store_t *s, const char *project, const char *keyword) {
7522+
cbm_edge_t *es = NULL;
7523+
int n = 0;
7524+
cbm_store_find_edges_by_url_path(s, project, keyword, &es, &n);
7525+
if (es) {
7526+
cbm_store_free_edges(es, n);
7527+
}
7528+
return n;
7529+
}
7530+
7531+
/* Distilled from PR #1245: detect_url_in_args (parallel resolver, >= 50 files)
7532+
* runs for EVERY resolved call with no callee gating, so a filesystem path
7533+
* with a document/log extension passed to a project-defined open/read/write
7534+
* helper passed normalize_url_arg (second slash, no space) and the route
7535+
* literal guard, minting a Route node + an HTTP_CALLS edge for a file. The
7536+
* callees are project functions on purpose: a bare builtin `open(...)` never
7537+
* resolves to a target and so never reaches emit_service_edge at all. The
7538+
* first path segments avoid the filesystem-root list (etc/var/tmp/...) so
7539+
* only the extension guard can reject them. requests.get('/api/items') is
7540+
* the positive control that must keep its Route + HTTP_CALLS. */
7541+
TEST(pipeline_arg_url_rejects_document_file_paths) {
7542+
char tmp[256];
7543+
snprintf(tmp, sizeof(tmp), "/tmp/cbm_arg_url_docs_XXXXXX");
7544+
if (!cbm_mkdtemp(tmp)) {
7545+
FAIL("tmpdir");
7546+
}
7547+
7548+
write_temp_file(tmp, "src/files.py",
7549+
"import requests\n"
7550+
"def open_file(path):\n"
7551+
" return path\n"
7552+
"def read_file(path):\n"
7553+
" return path\n"
7554+
"def write_file(path):\n"
7555+
" return path\n"
7556+
"def load_docs():\n"
7557+
" open_file('/new/file.txt')\n"
7558+
" read_file('/docs/guide.md')\n"
7559+
" write_file('/data/app.log')\n"
7560+
" read_file('/reports/summary.pdf')\n"
7561+
" open_file('/docs/index.rst')\n"
7562+
"def fetch_items():\n"
7563+
" return requests.get('/api/items')\n");
7564+
for (int i = 0; i < 52; i++) {
7565+
char name[64];
7566+
char body[128];
7567+
snprintf(name, sizeof(name), "src/filler%d.ts", i);
7568+
snprintf(body, sizeof(body), "export function filler%d(): number { return %d; }\n", i, i);
7569+
write_temp_file(tmp, name, body);
7570+
}
7571+
7572+
char *old_workers = getenv("CBM_WORKERS");
7573+
char *saved = old_workers ? strdup(old_workers) : NULL;
7574+
cbm_setenv("CBM_WORKERS", "4", 1);
7575+
7576+
char db_path[512];
7577+
snprintf(db_path, sizeof(db_path), "%s/arg_url_docs.db", tmp);
7578+
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
7579+
ASSERT_NOT_NULL(p);
7580+
ASSERT_EQ(cbm_pipeline_run(p), 0);
7581+
const char *project = cbm_pipeline_project_name(p);
7582+
7583+
cbm_store_t *s = cbm_store_open_path(db_path);
7584+
ASSERT_NOT_NULL(s);
7585+
7586+
/* Positive control first: the genuine HTTP call still mints its route. */
7587+
ASSERT_GTE(count_nodes_named(s, project, "/api/items"), 1);
7588+
ASSERT_GTE(count_edges_with_url_path(s, project, "/api/items"), 1);
7589+
/* No Route node for any filesystem document path... */
7590+
ASSERT_EQ(count_nodes_named(s, project, "/new/file.txt"), 0);
7591+
ASSERT_EQ(count_nodes_named(s, project, "/docs/guide.md"), 0);
7592+
ASSERT_EQ(count_nodes_named(s, project, "/data/app.log"), 0);
7593+
ASSERT_EQ(count_nodes_named(s, project, "/reports/summary.pdf"), 0);
7594+
ASSERT_EQ(count_nodes_named(s, project, "/docs/index.rst"), 0);
7595+
/* ...and no HTTP_CALLS edge carrying one as its url_path either. */
7596+
ASSERT_EQ(count_edges_with_url_path(s, project, "/new/file.txt"), 0);
7597+
ASSERT_EQ(count_edges_with_url_path(s, project, "/docs/guide.md"), 0);
7598+
ASSERT_EQ(count_edges_with_url_path(s, project, "/data/app.log"), 0);
7599+
ASSERT_EQ(count_edges_with_url_path(s, project, "/reports/summary.pdf"), 0);
7600+
ASSERT_EQ(count_edges_with_url_path(s, project, "/docs/index.rst"), 0);
7601+
7602+
cbm_store_close(s);
7603+
cbm_pipeline_free(p);
7604+
if (saved) {
7605+
cbm_setenv("CBM_WORKERS", saved, 1);
7606+
free(saved);
7607+
} else {
7608+
cbm_unsetenv("CBM_WORKERS");
7609+
}
7610+
th_rmtree(tmp);
7611+
PASS();
7612+
}
7613+
75207614
/* Native `fetch()` (#856), sequential path (< 50 files → pass_calls.c). A bare
75217615
* unqualified call to the global fetch API has no import and no local
75227616
* definition anywhere in this project, so registry resolution comes back
@@ -16360,6 +16454,7 @@ SUITE(pipeline) {
1636016454
RUN_TEST(pipeline_parallel_rust_cross_only_macro_hidden_gets_synthetic_carrier);
1636116455
RUN_TEST(pipeline_arg_url_rejects_non_http_slash_arguments);
1636216456
RUN_TEST(pipeline_ts_config_object_url_http_calls_issue2235);
16457+
RUN_TEST(pipeline_arg_url_rejects_document_file_paths);
1636316458
RUN_TEST(pipeline_native_fetch_classified_as_http_calls);
1636416459
RUN_TEST(pipeline_swift_nested_url_makes_route_issue1892);
1636516460
RUN_TEST(pipeline_swift_http_call_makes_route_issue1892);

0 commit comments

Comments
 (0)