From 4b843c27916cd2ebe80e041df80a2cbbbcfe3838 Mon Sep 17 00:00:00 2001 From: Gaurav Dubey Date: Thu, 22 Oct 2026 00:44:18 +0000 Subject: [PATCH] fix(extract): YAML walkers grow their stacks instead of capping content walk_yaml_mapping (dotted key paths, 256 frames) and scan_yaml_for_infra_bindings (512 nodes) kept their pending nodes in fixed-size stacks and stopped pushing once full. A mapping with more than 256 nested sibling mappings lost the full key_path of every leaf past the cap: `k299.url` was only recorded as the shorter path that the walk of a nested block emits on its own. The infra Route pass decides on those paths. A `data:` payload block with 300 entries lost `data.svcN.webhook_url` for N >= 256, the payload deny never saw those values, and 44 of them were minted as infra Route nodes. Both walkers now start on the same-sized on-stack chunk and grow through cbm_walk_stack_reserve / cbm_walk_stack_release (#2375). If the stack cannot grow, the failure is logged as extract.walk_stack_alloc_failed and the walk stops, instead of truncating silently. Part of #2388. Signed-off-by: Gaurav Dubey --- internal/cbm/extract_unified.c | 49 +++++++++++++++----- tests/test_extraction.c | 81 ++++++++++++++++++++++++++++++++++ tests/test_lang_contract.c | 34 ++++++++++++++ 3 files changed, 152 insertions(+), 12 deletions(-) diff --git a/internal/cbm/extract_unified.c b/internal/cbm/extract_unified.c index fbb7e12c3..c42ccebb6 100644 --- a/internal/cbm/extract_unified.c +++ b/internal/cbm/extract_unified.c @@ -1663,27 +1663,39 @@ typedef struct { TSNode node; const char *prefix; } yaml_walk_frame_t; -#define YAML_WALK_STACK_CAP CBM_SZ_256 +/* On-stack first chunk of the key-path walk; wider mappings spill to the heap + * through cbm_walk_stack_reserve instead of dropping frames (#2388). */ +#define YAML_WALK_STACK_INLINE CBM_SZ_256 -/* Push block_mapping children of a block_node/block_mapping value onto the walk stack. */ -static void push_yaml_block_children(TSNode val, const char *path, yaml_walk_frame_t *stack, - int *top) { +/* Push block_mapping children of a block_node/block_mapping value onto the walk + * stack, growing it as needed. False when the stack could not grow (logged by + * cbm_walk_stack_reserve as extract.walk_stack_alloc_failed). */ +static bool push_yaml_block_children(TSNode val, const char *path, yaml_walk_frame_t **stack, + int *cap, int *top, const yaml_walk_frame_t *inline_buf) { uint32_t vnc = ts_node_named_child_count(val); - for (int vi = (int)vnc - SKIP_ONE; vi >= 0 && *top < YAML_WALK_STACK_CAP; vi--) { + if (!cbm_walk_stack_reserve((void **)stack, cap, *top + (int)vnc, sizeof(yaml_walk_frame_t), + inline_buf, "yaml_key_paths")) { + return false; + } + for (int vi = (int)vnc - SKIP_ONE; vi >= 0; vi--) { TSNode vc = ts_node_named_child(val, (uint32_t)vi); const char *vctype = ts_node_type(vc); if (strcmp(vctype, "block_mapping") == 0 || strcmp(vctype, "block_mapping_pair") == 0) { - stack[(*top)++] = (yaml_walk_frame_t){vc, path}; + (*stack)[(*top)++] = (yaml_walk_frame_t){vc, path}; } } + return true; } static void walk_yaml_mapping(CBMExtractCtx *ctx, TSNode root, const char *root_prefix) { - yaml_walk_frame_t stack[YAML_WALK_STACK_CAP]; + yaml_walk_frame_t inline_stack[YAML_WALK_STACK_INLINE]; + yaml_walk_frame_t *stack = inline_stack; + int cap = YAML_WALK_STACK_INLINE; int top = 0; stack[top++] = (yaml_walk_frame_t){root, root_prefix}; + bool grown = true; - while (top > 0) { + while (grown && top > 0) { yaml_walk_frame_t frame = stack[--top]; TSNode node = frame.node; const char *prefix = frame.prefix; @@ -1709,12 +1721,16 @@ static void walk_yaml_mapping(CBMExtractCtx *ctx, TSNode root, const char *root_ } const char *vk = ts_node_type(val); if (strcmp(vk, "block_node") == 0 || strcmp(vk, "block_mapping") == 0) { - push_yaml_block_children(val, path, stack, &top); + grown = push_yaml_block_children(val, path, &stack, &cap, &top, inline_stack); + if (!grown) { + break; + } continue; } emit_yaml_leaf_value(ctx, val, path); } } + cbm_walk_stack_release(stack, inline_stack); } /* ── Infrastructure binding extraction ───────────────────────────── @@ -1873,9 +1889,13 @@ static void scan_mapping_for_bindings(CBMExtractCtx *ctx, TSNode mapping) { emit_infra_bindings(ctx, sources, source_keys, n_sources, targets, n_targets); } -#define INFRA_SCAN_STACK_CAP CBM_SZ_512 +/* On-stack first chunk of the binding scan; wider subtrees spill to the heap + * through cbm_walk_stack_reserve instead of dropping nodes (#2388). */ +#define INFRA_SCAN_STACK_INLINE CBM_SZ_512 static void scan_yaml_for_infra_bindings(CBMExtractCtx *ctx, TSNode root) { - TSNode stack[INFRA_SCAN_STACK_CAP]; + TSNode inline_stack[INFRA_SCAN_STACK_INLINE]; + TSNode *stack = inline_stack; + int cap = INFRA_SCAN_STACK_INLINE; int top = 0; stack[top++] = root; while (top > 0) { @@ -1884,10 +1904,15 @@ static void scan_yaml_for_infra_bindings(CBMExtractCtx *ctx, TSNode root) { scan_mapping_for_bindings(ctx, node); } uint32_t nc = ts_node_named_child_count(node); - for (int i = (int)nc - SKIP_ONE; i >= 0 && top < INFRA_SCAN_STACK_CAP; i--) { + if (!cbm_walk_stack_reserve((void **)&stack, &cap, top + (int)nc, sizeof(TSNode), + inline_stack, "yaml_infra_bindings")) { + break; + } + for (int i = (int)nc - SKIP_ONE; i >= 0; i--) { stack[top++] = ts_node_named_child(node, (uint32_t)i); } } + cbm_walk_stack_release(stack, inline_stack); } /* ── HCL infrastructure binding extraction ─────────────────────────── diff --git a/tests/test_extraction.c b/tests/test_extraction.c index e1c1e4491..3484852fa 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -7229,6 +7229,85 @@ TEST(body_tokens_wide_body_samples_from_the_start) { PASS(); } +/* The YAML walkers kept their pending mappings in fixed-size stacks (256 for + * the dotted key-path walk, 512 for the infrastructure-binding scan) and + * silently stopped pushing once full (#2388). A root mapping with more nested + * sibling mappings than the old cap lost the full dotted key_path of every + * leaf past it: `k299.url` was only ever recorded as the bare `url` that the + * per-mapping walk of the nested block emits. */ +enum { WIDE_YAML_KEYS = 300, WIDE_YAML_ITEMS = 600 }; + +static int has_string_ref_path(CBMFileResult *r, const char *key_path, const char *value) { + for (int i = 0; i < r->string_refs.count; i++) { + const CBMStringRef *s = &r->string_refs.items[i]; + if (s->key_path && s->value && strcmp(s->key_path, key_path) == 0 && + strcmp(s->value, value) == 0) { + return 1; + } + } + return 0; +} + +TEST(yaml_key_paths_wide_mapping_exceed_old_stack_cap) { + size_t cap = (size_t)WIDE_YAML_KEYS * 48 + 64; + char *src = malloc(cap); + ASSERT_NOT_NULL(src); + size_t pos = 0; + for (int i = 0; i < WIDE_YAML_KEYS; i++) { + pos += (size_t)snprintf(src + pos, cap - pos, + "k%d:\n url: \"https://h%d.example.com/x\"\n", i, i); + } + CBMFileResult *r = extract(src, CBM_LANG_YAML, "t", "wide.yaml"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + for (int i = 0; i < WIDE_YAML_KEYS; i++) { + char path[32]; + char value[48]; + snprintf(path, sizeof(path), "k%d.url", i); + snprintf(value, sizeof(value), "https://h%d.example.com/x", i); + ASSERT(has_string_ref_path(r, path, value)); + } + cbm_free_result(r); + free(src); + PASS(); +} + +static int has_infra_source(CBMFileResult *r, const char *source, const char *url) { + for (int i = 0; i < r->infra_bindings.count; i++) { + const CBMInfraBinding *b = &r->infra_bindings.items[i]; + if (b->source_name && b->target_url && strcmp(b->source_name, source) == 0 && + strcmp(b->target_url, url) == 0) { + return 1; + } + } + return 0; +} + +TEST(yaml_infra_bindings_wide_sequence_exceed_old_stack_cap) { + size_t cap = (size_t)WIDE_YAML_ITEMS * 64 + 64; + char *src = malloc(cap); + ASSERT_NOT_NULL(src); + size_t pos = 0; + for (int i = 0; i < WIDE_YAML_ITEMS; i++) { + pos += (size_t)snprintf(src + pos, cap - pos, + "- topic: t%d\n push_endpoint: \"https://h%d.example.com/p\"\n", i, + i); + } + CBMFileResult *r = extract(src, CBM_LANG_YAML, "t", "pubsub.yaml"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + for (int i = 0; i < WIDE_YAML_ITEMS; i++) { + char source[16]; + char url[48]; + snprintf(source, sizeof(source), "t%d", i); + snprintf(url, sizeof(url), "https://h%d.example.com/p", i); + ASSERT(has_infra_source(r, source, url)); + } + cbm_free_result(r); + free(src); + PASS(); +} + /* ═══════════════════════════════════════════════════════════════════ * Perl call-graph noise (#459 follow-up) * ═══════════════════════════════════════════════════════════════════ */ @@ -13483,6 +13562,8 @@ SUITE(extraction) { RUN_TEST(complexity_wide_body_exceeds_old_stack_cap); RUN_TEST(count_branching_wide_body_exceeds_old_stack_cap); RUN_TEST(body_tokens_wide_body_samples_from_the_start); + RUN_TEST(yaml_key_paths_wide_mapping_exceed_old_stack_cap); + RUN_TEST(yaml_infra_bindings_wide_sequence_exceed_old_stack_cap); RUN_TEST(extract_c_ifdef_split_brace_fn_recovered_issue961); RUN_TEST(extract_cpp_preproc_signature_gap_issue946); RUN_TEST(extract_cpp_preproc_macro_generated_callable_skipped_issue949); diff --git a/tests/test_lang_contract.c b/tests/test_lang_contract.c index 73d8c5575..ff333410b 100644 --- a/tests/test_lang_contract.c +++ b/tests/test_lang_contract.c @@ -1553,6 +1553,39 @@ TEST(contract_edge_infra_routes_from_deploy_configs_still_minted) { PASS(); } +/* #2388: the YAML key-path walk kept its pending mappings in a 256-frame stack + * and stopped pushing once full. A `data:` payload block with more nested + * entries than that lost the full `data..webhook_url` path of every entry + * past the cap; only the shorter per-mapping refs survived, the payload deny + * never saw them, and each of those values minted an infra Route (44 of 300 + * here). Every payload value must stay unminted however wide the block is. */ +enum { WIDE_PAYLOAD_ENTRIES = 300 }; + +TEST(contract_edge_no_infra_routes_from_wide_payload_block_issue2388) { + size_t cap = (size_t)WIDE_PAYLOAD_ENTRIES * 64 + 16; + char *src = malloc(cap); + ASSERT_NOT_NULL(src); + size_t pos = (size_t)snprintf(src, cap, "data:\n"); + for (int i = 0; i < WIDE_PAYLOAD_ENTRIES; i++) { + pos += + (size_t)snprintf(src + pos, cap - pos, + " svc%d:\n webhook_url: https://h%d.payload.example/hook\n", i, i); + } + LangProj lp; + LangFile f[] = {{"service.yaml", src}}; + cbm_store_t *store = lang_index_files(&lp, f, 1); + ASSERT_NOT_NULL(store); + int routes = count_infra_routes_matching(store, lp.project, "payload.example"); + if (routes != 0) { + fprintf(stderr, " [2388] FAIL wide payload block minted %d infra routes (expected 0)\n", + routes); + } + ASSERT_EQ(routes, 0); + lang_cleanup(&lp, store); + free(src); + PASS(); +} + /* A config URL becomes an infra Route only under a key that names an endpoint * this service exposes. Reference links are not: across 17 infra repositories * the infra Routes were changelog `pull_request` links (deckhouse 6,240), @@ -1945,6 +1978,7 @@ SUITE(lang_contract) { RUN_TEST(contract_edge_python_aliased_import_call_resolves_issue988); RUN_TEST(contract_edge_no_infra_routes_from_ci_configs_issue999); RUN_TEST(contract_edge_infra_routes_from_deploy_configs_still_minted); + RUN_TEST(contract_edge_no_infra_routes_from_wide_payload_block_issue2388); RUN_TEST(contract_edge_no_infra_routes_from_reference_links); RUN_TEST(contract_edge_infra_routes_from_endpoint_key_spellings); RUN_TEST(contract_edge_commonjs_require_call_resolves_issue871);