Escape sliced source text in edge properties JSON

Edge properties interpolated raw source-text slices into JSON via snprintf without escaping: decorator text (quotes + raw newlines, e.g. @register.tag("block") or multi-line @override_settings) in DECORATES props - django produced 3826 malformed edges - plus usage ref_name and route-registration callee/url in USAGE/CALLS props. Malformed edge JSON aborts every json_extract consumer, including the url_path_gen generated-column evaluation that runs during PRAGMA integrity_check. All such sites now route through cbm_json_escape (DECORATES twins in pass_parallel.c/pass_semantic.c, usage emit twins, route-registration calls). Regression: pipeline_edge_props_valid_json (register.tag fixture; real-repo red recorded on the django index).
This commit is contained in:
Martin Vogel
2026-06-10 13:44:09 +02:00
parent 0442cceb80
commit 6c04ab7f61
5 changed files with 95 additions and 9 deletions
+6 -2
View File
@@ -193,10 +193,14 @@ static void handle_route_registration(cbm_pipeline_ctx_t *ctx, const CBMCall *ca
snprintf(route_props, sizeof(route_props), "{\"method\":\"%s\"}", method ? method : "ANY");
int64_t route_id = cbm_gbuf_upsert_node(ctx->gbuf, "Route", call->first_string_arg, route_qn,
"", 0, 0, route_props);
char esc_cn[CBM_SZ_256]; /* sliced source text: escape quotes/newlines */
char esc_fa[CBM_SZ_256];
cbm_json_escape(esc_cn, sizeof(esc_cn), call->callee_name);
cbm_json_escape(esc_fa, sizeof(esc_fa), call->first_string_arg);
char props[CBM_SZ_512];
snprintf(props, sizeof(props),
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"via\":\"route_registration\"}",
call->callee_name, call->first_string_arg);
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"via\":\"route_registration\"}", esc_cn,
esc_fa);
cbm_gbuf_insert_edge(ctx->gbuf, source_node->id, route_id, "CALLS", props);
if (call->second_arg_name != NULL && call->second_arg_name[0] != '\0') {
cbm_resolution_t hres = cbm_registry_resolve(ctx->registry, call->second_arg_name,
+11 -3
View File
@@ -1858,7 +1858,9 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
continue;
}
char uprops[CBM_SZ_256];
snprintf(uprops, sizeof(uprops), "{\"callee\":\"%s\"}", usage->ref_name);
char esc_ref[CBM_SZ_256]; /* sliced source text: escape quotes/newlines */
cbm_json_escape(esc_ref, sizeof(esc_ref), usage->ref_name);
snprintf(uprops, sizeof(uprops), "{\"callee\":\"%s\"}", esc_ref);
cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, tgt->id, "USAGE", uprops);
ws->usages_resolved++;
}
@@ -1984,8 +1986,14 @@ static void resolve_def_decorators(resolve_ctx_t *rc, resolve_worker_state_t *ws
cbm_gbuf_upsert_node(ws->local_edge_buf, "Decorator", fn, syn_qn, "", 0, 0, "{}");
}
if (dn_id != 0 && node->id != dn_id) {
char dp[CBM_SZ_256];
snprintf(dp, sizeof(dp), "{\"decorator\":\"%s\"}", def->decorators[dc]);
/* Decorator SOURCE TEXT can contain quotes and raw newlines
* (e.g. @register.tag("block"), multi-line @override_settings) —
* interpolating it raw produced malformed properties JSON that
* aborts every json_extract consumer (django: 3826 such edges). */
char esc_dec[CBM_SZ_256];
cbm_json_escape(esc_dec, sizeof(esc_dec), def->decorators[dc]);
char dp[CBM_SZ_512];
snprintf(dp, sizeof(dp), "{\"decorator\":\"%s\"}", esc_dec);
cbm_gbuf_insert_edge(ws->local_edge_buf, node->id, dn_id, "DECORATES", dp);
/* Ensure a reference-style edge exists so the decorator appears in queries
* without being misclassified as a real call by downstream passes. */
+8 -2
View File
@@ -12,6 +12,7 @@
* Depends on: pass_definitions having populated the registry and graph buffer
*/
#include "foundation/constants.h"
#include "foundation/str_util.h" // cbm_json_escape
#include "pipeline/pipeline.h"
#include <stdint.h>
#include "pipeline/pipeline_internal.h"
@@ -398,8 +399,13 @@ static void resolve_decorator(cbm_pipeline_ctx_t *ctx, const cbm_gbuf_node_t *no
}
}
if (dec && node->id != dec->id) {
char props[CBM_SZ_256];
snprintf(props, sizeof(props), "{\"decorator\":\"%s\"}", decorator);
/* Decorator source text can contain quotes and raw newlines — escape
* it or the edge properties JSON is malformed (twin of the parallel
* path in pass_parallel.c). */
char esc_dec[CBM_SZ_256];
cbm_json_escape(esc_dec, sizeof(esc_dec), decorator);
char props[CBM_SZ_512];
snprintf(props, sizeof(props), "{\"decorator\":\"%s\"}", esc_dec);
cbm_gbuf_insert_edge(ctx->gbuf, node->id, dec->id, "DECORATES", props);
/* Ensure a reference edge exists so the decorator appears in usage queries
* without being misclassified as a real call by downstream passes. */
+7 -2
View File
@@ -12,6 +12,7 @@
* Depends on: pass_definitions having populated the registry and graph buffer
*/
#include "foundation/constants.h"
#include "foundation/str_util.h" // cbm_json_escape
#include "pipeline/pipeline.h"
#include "pipeline/pipeline_internal.h"
#include "graph_buffer/graph_buffer.h"
@@ -223,8 +224,12 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
continue;
}
char uprops[CBM_SZ_256];
snprintf(uprops, sizeof(uprops), "{\"callee\":\"%s\"}", usage->ref_name);
/* ref_name is sliced source text and can contain quotes/newlines —
* escape it or the edge properties JSON is malformed. */
char esc_ref[CBM_SZ_256];
cbm_json_escape(esc_ref, sizeof(esc_ref), usage->ref_name);
char uprops[CBM_SZ_512];
snprintf(uprops, sizeof(uprops), "{\"callee\":\"%s\"}", esc_ref);
cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, "USAGE", uprops);
resolved++;
}
+63
View File
@@ -565,6 +565,68 @@ TEST(pipeline_def_props_valid_json_when_oversized) {
PASS();
}
/* Edge properties must be VALID JSON. Decorator source text (quotes, raw
* newlines: @register.tag("block"), multi-line @override_settings) was
* interpolated raw into the DECORATES properties — django produced 3826
* malformed edges, and any json_extract-based consumer (including the
* url_path_gen generated-column evaluation during PRAGMA integrity_check)
* aborts on them. Usage/call emit sites had the same hole for sliced source
* text. */
TEST(pipeline_edge_props_valid_json) {
if (setup_test_repo() != 0) {
FAIL("failed to create temp dir");
}
char path[512];
snprintf(path, sizeof(path), "%s/deco.py", g_tmpdir);
FILE *f = fopen(path, "w");
if (!f) {
teardown_test_repo();
FAIL("failed to write deco.py");
}
fprintf(f, "from x import register\n"
"@register.tag(\"block\")\n"
"def do_block(parser):\n"
" return parser\n"
"@register.tag(\"extends\")\n"
"def do_extends(parser):\n"
" return parser\n");
fclose(f);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/test_edge_props.db", g_tmpdir);
cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
const char *project = cbm_pipeline_project_name(p);
cbm_edge_t *edges = NULL;
int edge_count = 0;
ASSERT_EQ(cbm_store_find_edges_by_type(s, project, "DECORATES", &edges, &edge_count),
CBM_STORE_OK);
ASSERT_GT(edge_count, 0); /* the decorators must produce DECORATES edges */
for (int i = 0; i < edge_count; i++) {
const char *pj = edges[i].properties_json;
if (!pj) {
continue;
}
yyjson_doc *doc = yyjson_read(pj, strlen(pj), 0);
if (!doc) {
printf(" INVALID edge properties JSON: %.80s\n", pj);
}
ASSERT_NOT_NULL(doc);
yyjson_doc_free(doc);
}
cbm_store_free_edges(edges, edge_count);
cbm_store_close(s);
cbm_pipeline_free(p);
teardown_test_repo();
PASS();
}
/* ── Calls pass tests ──────────────────────────────────────────── */
TEST(pipeline_calls_resolution) {
@@ -5630,6 +5692,7 @@ SUITE(pipeline) {
RUN_TEST(pipeline_definitions_defines_edges);
RUN_TEST(pipeline_definitions_properties);
RUN_TEST(pipeline_def_props_valid_json_when_oversized);
RUN_TEST(pipeline_edge_props_valid_json);
/* Complexity propagation pass (Tier B) */
RUN_TEST(pipeline_complexity_transitive_loop_depth);
/* Calls pass */