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 <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-26 01:46:33 +01:00
1 parent ad7899399b
commit 4d97d7819b
8 files changed
+137 -17

No files matched your search

+2 -1
View File
@@ -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);
}
+15
View File
@@ -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;
+8
View File
@@ -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);
+4 -3
View File
@@ -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.
+7 -6
View File
@@ -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;
+6 -5
View File
@@ -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
+16 -1
View File
@@ -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();
}
+79 -1
View File
@@ -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();
}