From 4d97d7819b80f96e2492569ae54b442af4406f98 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Mon, 26 Oct 2026 01:46:33 +0100 Subject: [PATCH] fix(routes): read a ServeMux pattern only for Handle and HandleFunc The Go 1.22 pattern split ran for every route-verb callee, so other Go calls whose name ends in Get/Post/Put/Delete/Patch became route registrations: st.Get("users/123") on a project method minted __route__GET__/123 and lost its CALLS edge to Store.Get, and c.Get("https://api.example.com/v1/x") split "https:" off as a host and minted __route__GET__//api.example.com/v1/x, in both passes. cbm_service_pattern_mux_route_path reads the pattern only for a callee ending ".Handle" or ".HandleFunc", and never for an argument containing "://". The extractor's handler capture and both passes (route_reg_path, find_route_path_in_args, cbm_pipeline_route_reg_method) go through it, so the two passes still mint identical Routes. Two negative cases extend pipeline_go_servemux_method_patterns_issue2537 in both passes, and the infrascan unit covers the gate. Refs #2537 Signed-off-by: Martin Vogel --- internal/cbm/extract_calls.c | 3 +- internal/cbm/service_patterns.c | 15 ++++++ internal/cbm/service_patterns.h | 8 ++++ src/pipeline/pass_calls.c | 7 +-- src/pipeline/pass_parallel.c | 13 +++--- src/pipeline/pipeline_internal.h | 11 +++-- tests/test_infrascan.c | 17 ++++++- tests/test_pipeline.c | 80 +++++++++++++++++++++++++++++++- 8 files changed, 137 insertions(+), 17 deletions(-) diff --git a/internal/cbm/extract_calls.c b/internal/cbm/extract_calls.c index f16b8ecf8..4beebc220 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -4559,7 +4559,8 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML if (call.first_string_arg && (call.first_string_arg[0] == '/' || (ctx->language == CBM_LANG_GO && - cbm_service_pattern_mux_split(call.first_string_arg, NULL, 0) != NULL)) && + cbm_service_pattern_mux_route_path(call.callee_name, call.first_string_arg) != + NULL)) && !(laravel_uri && php_route_action_is_string(node))) { call.second_arg_name = extract_handler_arg(ctx, args); } diff --git a/internal/cbm/service_patterns.c b/internal/cbm/service_patterns.c index 1eaa50941..503d18538 100644 --- a/internal/cbm/service_patterns.c +++ b/internal/cbm/service_patterns.c @@ -1227,6 +1227,21 @@ const char *cbm_service_pattern_mux_split(const char *pattern, char *method, siz return slash; } +const char *cbm_service_pattern_mux_route_path(const char *callee_name, const char *pattern) { + static const char *const mux_callees[] = {".Handle", ".HandleFunc"}; + if (!callee_name || !pattern || strstr(pattern, "://") != NULL) { + return NULL; + } + size_t clen = strlen(callee_name); + for (size_t i = 0; i < sizeof(mux_callees) / sizeof(mux_callees[0]); i++) { + size_t slen = strlen(mux_callees[i]); + if (clen >= slen && strcmp(callee_name + clen - slen, mux_callees[i]) == 0) { + return cbm_service_pattern_mux_split(pattern, NULL, 0); + } + } + return NULL; +} + const char *cbm_service_pattern_broker(const char *resolved_qn) { if (!resolved_qn) { return NULL; diff --git a/internal/cbm/service_patterns.h b/internal/cbm/service_patterns.h index 59f3ea0af..0a182568e 100644 --- a/internal/cbm/service_patterns.h +++ b/internal/cbm/service_patterns.h @@ -95,6 +95,14 @@ const char *cbm_service_pattern_route_method(const char *callee_name); * on the file language. */ const char *cbm_service_pattern_mux_split(const char *pattern, char *method, size_t method_sz); +/* The path of a ServeMux registration's pattern (#2537): only a callee ending + * ".Handle" or ".HandleFunc" registers one, and never with a URL ("://"). + * Another route-verb call names no pattern: `store.Get("users/123")` is no + * host "users" + path "/123", `client.Get("https://...")` no host "https:". + * The split of cbm_service_pattern_mux_split for such a registration, else + * NULL. The grammar is Go's: callers gate on the file language. */ +const char *cbm_service_pattern_mux_route_path(const char *callee_name, const char *pattern); + /* Get the broker name for an async QN (e.g., "pubsub" from a Pub/Sub QN). * Returns NULL if not an async pattern. */ const char *cbm_service_pattern_broker(const char *resolved_qn); diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 0e7069ab6..97c8e784e 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -573,8 +573,9 @@ cbm_svc_kind_t cbm_pipeline_external_import_service(cbm_external_import_index_t /* The path a route registration's first argument names: the argument itself * when it opens with a slash, or in Go the path of a ServeMux pattern that - * names a method or host first ("POST /v1/x", #2537). NULL when it names no - * path. MUST match pass_parallel.c find_route_path_in_args. */ + * names a method or host first ("POST /v1/x", #2537; Handle / HandleFunc + * only). NULL when it names no path. MUST match pass_parallel.c + * find_route_path_in_args. */ static const char *route_reg_path(const CBMCall *call, CBMLanguage lang) { const char *arg = call->first_string_arg; if (!arg) { @@ -583,7 +584,7 @@ static const char *route_reg_path(const CBMCall *call, CBMLanguage lang) { if (arg[0] == '/') { return arg; } - return cbm_pipeline_mux_route_path(arg, lang); + return cbm_pipeline_mux_route_path(call, lang); } /* Handle a route registration call: create Route node + HANDLES edge. diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 380110295..feff58fdb 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -2263,11 +2263,12 @@ static bool is_route_path_shaped(const char *val) { return val && val[0] == '/' && !cbm_service_pattern_is_comment_text(val); } -const char *cbm_pipeline_mux_route_path(const char *arg, CBMLanguage lang) { +const char *cbm_pipeline_mux_route_path(const CBMCall *call, CBMLanguage lang) { + const char *arg = call->first_string_arg; if (lang != CBM_LANG_GO || !arg || cbm_service_pattern_is_comment_text(arg)) { return NULL; } - return cbm_service_pattern_mux_split(arg, NULL, 0); + return cbm_service_pattern_mux_route_path(call->callee_name, arg); } const char *cbm_pipeline_route_reg_method(const CBMCall *call, CBMLanguage lang, char *buf, @@ -2276,7 +2277,7 @@ const char *cbm_pipeline_route_reg_method(const CBMCall *call, CBMLanguage lang, if (method && strcmp(method, "ANY") != 0) { return method; } - if (cbm_pipeline_mux_route_path(call->first_string_arg, lang) && + if (cbm_pipeline_mux_route_path(call, lang) && cbm_service_pattern_mux_split(call->first_string_arg, buf, buf_sz) && buf[0] != '\0') { return buf; } @@ -2291,9 +2292,9 @@ static const char *find_route_path_in_args(const CBMCall *call, const char **out *out_handler = call->second_arg_name; return call->first_string_arg; } - /* 1b. Go ServeMux pattern with a method or host before the path (#2537). - * MUST match pass_calls.c route_reg_path. */ - const char *mux_path = cbm_pipeline_mux_route_path(call->first_string_arg, lang); + /* 1b. Go ServeMux pattern with a method or host before the path (#2537), + * read for Handle / HandleFunc only. MUST match pass_calls.c route_reg_path. */ + const char *mux_path = cbm_pipeline_mux_route_path(call, lang); if (mux_path) { *out_handler = call->second_arg_name; return mux_path; diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index d6882067e..d2fb1c736 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -788,11 +788,12 @@ bool cbm_pipeline_http_client_call_url(const cbm_gbuf_t *gbuf, const char *proje void cbm_pipeline_close_call_props(char *props, int n, const CBMCall *call, bool with_line); /* Go 1.22 ServeMux registrations may name a method and a host before the path - * ("POST /v1/x", "GET api.example.com/v1/x", #2537). For a Go file, the path - * inside such a route-registration argument; NULL for any other language and - * for an argument that is no such pattern (a plain "/v1/x" included). The one - * reader of that grammar behind both passes (pass_parallel.c). */ -const char *cbm_pipeline_mux_route_path(const char *arg, CBMLanguage lang); + * ("POST /v1/x", "GET api.example.com/v1/x", #2537). For a Go `Handle` / + * `HandleFunc` call, the path inside its first argument; NULL for any other + * language or callee (`store.Get("users/123")`), for a URL argument, and for an + * argument that is no such pattern (a plain "/v1/x" included). The one reader + * of that grammar behind both passes (pass_parallel.c). */ +const char *cbm_pipeline_mux_route_path(const CBMCall *call, CBMLanguage lang); /* The method of a route registration's Route: the verb its callee names * (`router.GET`), else the method its Go ServeMux pattern names diff --git a/tests/test_infrascan.c b/tests/test_infrascan.c index 2e33e5168..94ba522e1 100644 --- a/tests/test_infrascan.c +++ b/tests/test_infrascan.c @@ -551,7 +551,8 @@ TEST(infrascan_data_flows_test_handlers_pair_only_within_their_file) { /* Issue #2537: the Go 1.22 ServeMux grammar "[METHOD ][HOST]/[PATH]". The path * is found inside the written pattern; a plain path, a lowercase word, a - * pattern without a path and a method too long for the buffer are no split. */ + * pattern without a path and a method too long for the buffer are no split. + * Only a Handle / HandleFunc registration is read with it. */ TEST(infrascan_servemux_pattern_split_issue2537) { char m[16]; const char *pat = "POST /v1/payments"; @@ -579,6 +580,20 @@ TEST(infrascan_servemux_pattern_split_issue2537) { ASSERT_NULL(cbm_service_pattern_mux_split(NULL, m, sizeof(m))); ASSERT_NULL(cbm_service_pattern_mux_split("ABCDEFGHIJKLMNOP /x", m, sizeof(m))); ASSERT_STR_EQ(m, ""); + + /* only Handle / HandleFunc register a pattern, and never with a URL */ + pat = "POST /v1/payments"; + ASSERT_TRUE(cbm_service_pattern_mux_route_path("mux.HandleFunc", pat) == pat + 5); + pat = "api.example.com/v1/x"; + ASSERT_TRUE(cbm_service_pattern_mux_route_path("s.mux.Handle", pat) == pat + 15); + ASSERT_NULL(cbm_service_pattern_mux_route_path("st.Get", "users/123")); + ASSERT_NULL(cbm_service_pattern_mux_route_path("c.Get", "https://api.example.com/v1/x")); + ASSERT_NULL(cbm_service_pattern_mux_route_path("mux.Handle", "https://api.example.com/v1/x")); + ASSERT_NULL(cbm_service_pattern_mux_route_path("PatchCollector.Delete", "storage.k8s.io/v1")); + ASSERT_NULL(cbm_service_pattern_mux_route_path("r.Route", "GET /v1/x")); + ASSERT_NULL(cbm_service_pattern_mux_route_path("mux.HandleFunc", "/v1/x")); + ASSERT_NULL(cbm_service_pattern_mux_route_path(NULL, "GET /v1/x")); + ASSERT_NULL(cbm_service_pattern_mux_route_path("mux.HandleFunc", NULL)); PASS(); } diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index e30d95a26..e1e5e0f5d 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -13376,6 +13376,53 @@ TEST(pipeline_route_prefix_bridge_is_order_free_and_root_bridges_nothing) { PASS(); } +/* Route nodes whose qualified name contains `part`. */ +static int route_nodes_containing(cbm_store_t *s, const char *project, const char *part) { + cbm_node_t *routes = NULL; + int route_count = 0; + if (cbm_store_find_nodes_by_label(s, project, "Route", &routes, &route_count) != CBM_STORE_OK) { + return -1; + } + int matches = 0; + for (int i = 0; i < route_count; i++) { + if (routes[i].qualified_name && strstr(routes[i].qualified_name, part)) { + matches++; + } + } + if (routes) { + cbm_store_free_nodes(routes, route_count); + } + return matches; +} + +/* CALLS edges out of the function `source_name` into a Route node: the edge a + * route registration emits beside its Route (an HTTP client call's edge to a + * Route is HTTP_CALLS instead). */ +static int calls_into_route_count(cbm_store_t *s, const char *project, const char *source_name) { + cbm_edge_t *edges = NULL; + int edge_count = 0; + if (cbm_store_find_edges_by_type(s, project, "CALLS", &edges, &edge_count) != CBM_STORE_OK) { + return -1; + } + int matches = 0; + for (int i = 0; i < edge_count; i++) { + cbm_node_t source = {0}; + cbm_node_t target = {0}; + int source_ok = cbm_store_find_node_by_id(s, edges[i].source_id, &source) == CBM_STORE_OK; + int target_ok = cbm_store_find_node_by_id(s, edges[i].target_id, &target) == CBM_STORE_OK; + if (source_ok && target_ok && source.name && target.label && + strcmp(source.name, source_name) == 0 && strcmp(target.label, "Route") == 0) { + matches++; + } + cbm_node_free_fields(&source); + cbm_node_free_fields(&target); + } + if (edges) { + cbm_store_free_edges(edges, edge_count); + } + return matches; +} + /* Issue #2537: since Go 1.22 a ServeMux pattern may name a method and a host * before the path ("POST /v1/payments", "GET api.example.com/v1/x"). Such a * registration minted no Route and no HANDLES edge, because every registration @@ -13383,7 +13430,10 @@ TEST(pipeline_route_prefix_bridge_is_order_free_and_root_bridges_nothing) { * written with "/v1/payments" had both. The method now comes from the pattern * and a host is dropped from the path, in the sequential and the parallel pass * alike. Only Go reads that grammar: a Python registration of "GET /py" is still - * no route. */ + * no route. Only a ServeMux registration (Handle / HandleFunc) is read with it: + * another route-verb call names no pattern and mints no Route, neither + * `st.Get("users/123")` on a project method (which keeps its CALLS edge) nor + * `c.Get("https://api.example.com/v1/x")` on a client. */ TEST(pipeline_go_servemux_method_patterns_issue2537) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_servemux_XXXXXX"); @@ -13401,6 +13451,16 @@ TEST(pipeline_go_servemux_method_patterns_issue2537) { "func hostOnly(w http.ResponseWriter, r *http.Request) {}\n" "func legacyList(w http.ResponseWriter, r *http.Request) {}\n" "\n" + "type Store struct{ m map[string]string }\n" + "\n" + "func (s *Store) Get(k string) string { return s.m[k] }\n" + "\n" + "func lookup(st *Store) string { return st.Get(\"users/123\") }\n" + "\n" + "func fetchRemote(c *http.Client) {\n" + "\t_, _ = c.Get(\"https://api.example.com/v1/x\")\n" + "}\n" + "\n" "func main() {\n" "\tmux := http.NewServeMux()\n" "\tmux.HandleFunc(\"POST /v1/payments\", createPayment)\n" @@ -13438,6 +13498,11 @@ TEST(pipeline_go_servemux_method_patterns_issue2537) { int legacy[2] = {-1, -1}; int py_any[2] = {-1, -1}; int py_get[2] = {-1, -1}; + int store_calls[2] = {-1, -1}; + int store_route[2] = {-1, -1}; + int lookup_reg[2] = {-1, -1}; + int remote_route[2] = {-1, -1}; + int remote_reg[2] = {-1, -1}; for (int mode = 0; mode < 2; mode++) { if (mode == 0) { cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1); @@ -13463,6 +13528,11 @@ TEST(pipeline_go_servemux_method_patterns_issue2537) { legacy[mode] = handles_to_route(s, project, "legacyList", "__route__ANY__/v1/legacy"); py_any[mode] = handles_to_route(s, project, "py_view", "__route__ANY__/py"); py_get[mode] = handles_to_route(s, project, "py_view", "__route__GET__/py"); + store_calls[mode] = named_edge_count(s, project, "CALLS", "lookup", "Get"); + store_route[mode] = route_nodes_containing(s, project, "__route__GET__/123"); + lookup_reg[mode] = calls_into_route_count(s, project, "lookup"); + remote_route[mode] = route_nodes_containing(s, project, "//api.example.com"); + remote_reg[mode] = calls_into_route_count(s, project, "fetchRemote"); } if (s) { cbm_store_close(s); @@ -13496,6 +13566,14 @@ TEST(pipeline_go_servemux_method_patterns_issue2537) { /* another language's "GET /py" is not read as a ServeMux pattern */ ASSERT_EQ(py_any[mode], 0); ASSERT_EQ(py_get[mode], 0); + /* a project method named Get keeps its CALLS edge; "users/123" is no + * host + path */ + ASSERT_EQ(store_calls[mode], 1); + ASSERT_EQ(store_route[mode], 0); + ASSERT_EQ(lookup_reg[mode], 0); + /* a client Get on a URL registers no route: "https:" is no host */ + ASSERT_EQ(remote_route[mode], 0); + ASSERT_EQ(remote_reg[mode], 0); } PASS(); }