From bc0ef58309116280a698fbcbc4872ec9f5ea91f8 Mon Sep 17 00:00:00 2001 From: mscasso-scanoss Date: Fri, 16 Oct 2026 21:57:28 +0200 Subject: [PATCH 1/2] fix(snippet): return one candidate per snippet region in multi-component scans A file concatenating code from several components collapsed to a single candidate and segfaulted with -M >= 2. Three issues, same selection path shared by -M/--max-snippets and -S/--snippet-scan (the /scan/snippets endpoint): 1. match_data_copy() dropped scan_ower (and lines_matched). The multi-snippet output path copies matches, so print_json_match() dereferenced a NULL scan_ower -> SIGSEGV (src/report.c:337) on any -M >= 2. 2. biggest_snippet() created a per-region list but added every candidate to matches_list_array[i] (almost always index 0), so all blocks competed in a single list and only one component survived. Reworked to place each match in the list of the region whose ranges intersect it, opening a new list per non-overlapping region up to MAX_MULTIPLE_COMPONENTS. 3. snippet-scan (-S) defaulted to SCAN_MAX_SNIPPETS_DEFAULT (1), collapsing the discovery endpoint to a single group regardless of the input. It now opens up to MAX_MULTIPLE_COMPONENTS groups unless the caller explicitly raised -M. Also order multi-snippet matches by covered lines (more_lines_matched) so the most-relevant block is the principal match (match_index 0). Verified against KB ossv2 on the concatenated nginx/tomcat/curl/httpd/postgres fixture: -M 9 returns curl+httpd (tomcat too with --ignore-file-ext), main.java -> tomcat only, main.py -> no match, and -S returns one group per region. No segfault across -M 1..9; valgrind clean. nginx/postgres remain out of reach (popular-hash blocks dropped by the matchmap category cap) and are tracked as pending in the regression test. Adds tests/snippet_multicomponent/ regression test (skips without a KB). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/match.c | 16 ++- src/purl_scan.c | 9 +- src/snippet_selection.c | 45 +++--- tests/snippet_multicomponent/main.c.wfp | 110 +++++++++++++++ tests/snippet_multicomponent/run.sh | 176 ++++++++++++++++++++++++ 5 files changed, 338 insertions(+), 18 deletions(-) create mode 100644 tests/snippet_multicomponent/main.c.wfp create mode 100755 tests/snippet_multicomponent/run.sh diff --git a/src/match.c b/src/match.c index b2baffa..823b94b 100644 --- a/src/match.c +++ b/src/match.c @@ -91,7 +91,9 @@ match_data_t * match_data_copy(match_data_t * in) out->line_ranges = strdup(in->line_ranges); out->oss_ranges = strdup(in->oss_ranges); out->matched_percent = in->matched_percent; + out->lines_matched = in->lines_matched; out->snippet_ids = strdup(in->snippet_ids); + out->scan_ower = in->scan_ower; /* keep owning scan so downstream (e.g. print_json_match) can deref it */ strcpy(out->source_md5, in->source_md5); return out; } @@ -1099,6 +1101,18 @@ void match_select_best(scan_data_t *scan) } } +/** + * @brief Order multi-snippet matches by relevance (matched lines, descending). + * Used to rank the per-region candidates so the most-covered block is the + * principal match (match_index 0) and the rest follow as alternatives. + */ +bool more_lines_matched(match_data_t *a, match_data_t *b) +{ + if (!a) + return true; + return (b->lines_matched > a->lines_matched); +} + match_list_t *match_select_m_component_best(scan_data_t *scan) { scanlog("<<>>>\n", scan->max_snippets_to_process); @@ -1119,7 +1133,7 @@ match_list_t *match_select_m_component_best(scan_data_t *scan) dup_match->component_list.match_ref = dup_match; component_list_add(&dup_match->component_list, dup_comp, NULL, false); dup_match->component_list.max_items = 1; // harcoded to show only fist component in report. - if (!match_list_add(final, dup_match, find_oldest_match, true)) + if (!match_list_add(final, dup_match, more_lines_matched, true)) match_data_free(dup_match); } diff --git a/src/purl_scan.c b/src/purl_scan.c index 3ba7508..2c5026b 100644 --- a/src/purl_scan.c +++ b/src/purl_scan.c @@ -542,8 +542,15 @@ static int snippet_scan_stream(FILE *in) extract_csv(size_field, (char *) rec, 1, sizeof(size_field)); file_size = strtoull(size_field, NULL, 10); + /* snippet-scan is a multi-candidate discovery endpoint: it must expose + every non-overlapping snippet region, not just the single best block. + The engine-wide default (SCAN_MAX_SNIPPETS_DEFAULT = 1) would collapse + biggest_snippet() to one group, so unless the caller explicitly raised + -M we open it up to MAX_MULTIPLE_COMPONENTS groups. */ + int snippet_max = scan_max_snippets > 1 ? scan_max_snippets : MAX_MULTIPLE_COMPONENTS; + scan = scan_data_init(file_path, - scan_max_snippets, + snippet_max, scan_max_components, scan_adjust_tolerance, scan_ranking_threshold, diff --git a/src/snippet_selection.c b/src/snippet_selection.c index ea95e2d..ae19333 100644 --- a/src/snippet_selection.c +++ b/src/snippet_selection.c @@ -158,28 +158,41 @@ void biggest_snippet(scan_data_t *scan) match_new->lines_matched = matched_lines; //match_new->hits = hits; - do /*Check if there is already a list for this line ranges */ + /* Place the match in the list of the snippet region it belongs to. + * First look for an existing list whose ranges intersect this match + * (same region -> alternative candidate). If none intersects, open a + * new list for this region while there is room; otherwise fall back to + * the last list. */ + bool assigned = false; + for (i = 0; i < scan->matches_list_array_index; i++) { - if (!scan->matches_list_array[scan->matches_list_array_index] && scan->matches_list_array_index < scan->max_snippets_to_process) + if (match_list_eval(scan->matches_list_array[i], match_new, ranges_intersection)) { - scan->matches_list_array[scan->matches_list_array_index] = match_list_init(true, 1); /*create the list if it doesnt exist*/ - scan->matches_list_array_index++; - if(!match_list_add(scan->matches_list_array[i], match_new, hit_test, true)) - { - match_data_free(match_new); - } + if (!match_list_add(scan->matches_list_array[i], match_new, hit_test, true)) + match_data_free(match_new); + assigned = true; break; } - if (match_list_eval(scan->matches_list_array[i], match_new, ranges_intersection) || i == scan->max_snippets_to_process -1) + } + + if (!assigned) + { + if (scan->matches_list_array_index < scan->max_snippets_to_process) { - if(!match_list_add(scan->matches_list_array[i], match_new, hit_test, true)) - { - match_data_free(match_new); - } - break; + /* New non-overlapping region: create its own list and add the match there */ + int new_index = scan->matches_list_array_index; + scan->matches_list_array[new_index] = match_list_init(true, 1); + scan->matches_list_array_index++; + if (!match_list_add(scan->matches_list_array[new_index], match_new, hit_test, true)) + match_data_free(match_new); } - i++; - } while(i < scan->matches_list_array_index); /*Check if there is already a list for this line ranges */ + else + { + /* No room left for new regions: force into the last list */ + if (!match_list_add(scan->matches_list_array[scan->max_snippets_to_process - 1], match_new, hit_test, true)) + match_data_free(match_new); + } + } } } /*just for loging*/ diff --git a/tests/snippet_multicomponent/main.c.wfp b/tests/snippet_multicomponent/main.c.wfp new file mode 100644 index 0000000..7f7089e --- /dev/null +++ b/tests/snippet_multicomponent/main.c.wfp @@ -0,0 +1,110 @@ +file=3bb103a46f94ed298d8c37b4a80d973c,9918,main.c +7=e795bac8 +10=6e7bf056 +14=16579738 +25=23d6d030 +28=d32e499d +30=09e0aecd,b7908937,21949e96 +33=d400f39f +37=ef7d6cba,894d3fa4,2d1597e8 +41=c848f202 +44=be39f156 +59=905e02fb +61=b7908937 +67=18b3beaf +76=491ac1ea +81=06fbfde3 +83=95c75ef3 +84=d400f39f +88=b036ba15 +95=c848f202,be39f156 +111=c711d2e1 +112=447c4417 +114=9a106044,12ab20bb,0ce7ece1 +115=151c9088,d434c84e,bac49cc6 +116=2a6b8b81,1f38d8be +117=7001190b +119=25c9f85f,a6453c32,e9e65a61 +120=4186b77b,ef9fe105 +124=da55d5b9,b0924155 +128=b378b9e0 +129=addfb6b0 +134=0dbb599f,ec33702f +135=80a48c6a +136=f4347ad7,d5fbdf3a +137=7ce444f8 +139=137b0602,f12edbd6 +142=3dd980ae,ac0b0110 +144=a76b34a5 +148=e0fa1c18,c741c7b2 +151=193cc2c3 +152=79eee8b8,b2e74c6e +156=7b280e25,d864c721 +157=216caf87 +158=a6a3bbe1,b66372cb +159=04ca6169,a7452328 +162=75fa5176 +163=d0cb6c6b,abafa52d +167=9396f071 +168=30792053 +169=4b7b9a0a +171=e6489ee3,8c952f0c +172=b9cded56 +174=2d95465b +176=11781789 +178=bd14a25f,f67df255 +180=46491e29 +183=c29128bc +184=5625dd8a,30792053 +185=4b7b9a0a +187=dd6588d4,0a041ed0 +188=341c8c04 +189=e3cc063a +193=bea1b86c,802ddd80 +196=f537a752,5218fbf7 +198=f21eefbd +199=c2b9b9f9,9f6d2f6f +201=0724e239 +203=1ec0629c +206=8dbf6363 +207=771caaeb +212=35ac838c +214=f94d5b4c,a169fcbf +216=4e6c1cbe +217=4666a13c +219=077eccf8 +222=0ae64d50 +224=9712f0ff,4d24ea64 +228=78ef004d,46c56394,07bc7452 +230=97b52f82 +232=e3af2023 +236=161a335f +237=f6a61105 +241=273539a2,08dcf25a,ac7340d8 +246=d0c75748 +248=061baf8c +250=f941efa7 +253=36d3594e +255=af8cf5fb,40b333f4 +259=8b1ff8bf,6cf0d5ac +260=b803e9eb +264=3cc8ad46 +265=74d8a41f +269=0547b4df,b846d5c2 +272=3926ed53,4bf276e9,29af2a76 +274=fdb2863f,6b99be79 +275=6bc888a0 +278=05bf9646 +281=48848bae +284=4d6fcc13 +285=cfc7ff90 +288=ab7fc8cd,fb85472f +291=64f5cde0,0d2eca0b +292=791fbabd +296=07cfc5fe +297=357fa0d4,87642ec3 +299=16e81aa2 +301=0acd0d5d,085f6061,c727d6af +303=ac932df0 +306=0698ae44 +308=3dbbeabb,5919152f diff --git a/tests/snippet_multicomponent/run.sh b/tests/snippet_multicomponent/run.sh new file mode 100755 index 0000000..50decec --- /dev/null +++ b/tests/snippet_multicomponent/run.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env bash +# +# Regression test: multi-component snippet scan (-M / --max-snippets). +# +# Guards two bugs that made a single file concatenating code from several +# components collapse to a single candidate (and segfault with -M >= 2): +# +# 1. match_data_copy() dropped scan_ower/lines_matched -> print_json_match() +# dereferenced a NULL scan_ower (segfault, src/report.c). +# 2. biggest_snippet() added every candidate to matches_list_array[0] instead +# of the per-region list, so only one block survived (src/snippet_selection.c). +# +# The input is main.c.wfp: a WFP of a file that concatenates blocks from +# nginx / tomcat / curl / httpd / postgres (see the header table in the PR). +# +# This test needs an OSS knowledge base. If none is reachable it SKIPS (exit 0) +# so it does not break CI images that ship without KB data. +# +# SCANOSS_BIN path to the engine binary (default: ../../scanoss) +# SCANOSS_KB KB/database name for -n (default: ossv2) +# +set -u + +HERE="$(cd "$(dirname "$0")" && pwd)" +BIN="${SCANOSS_BIN:-$HERE/../../scanoss}" +KB="${SCANOSS_KB:-ossv2}" +WFP_C="$HERE/main.c.wfp" + +if [ ! -x "$BIN" ]; then + echo "SKIP: engine binary not found at $BIN (set SCANOSS_BIN)" + exit 0 +fi + +# Derive java/py WFPs (identical content, only the file name/extension differs). +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT +WFP_JAVA="$TMP/main.java.wfp" +WFP_PY="$TMP/main.py.wfp" +sed 's/,main\.c$/,main.java/' "$WFP_C" > "$WFP_JAVA" +sed 's/,main\.c$/,main.py/' "$WFP_C" > "$WFP_PY" + +# Probe: make sure the KB actually answers; otherwise skip rather than fail. +PROBE="$("$BIN" -n "$KB" -w "$WFP_C" 2>/dev/null)" +if [ $? -ne 0 ] || ! printf '%s' "$PROBE" | grep -q '"id": "snippet"'; then + echo "SKIP: KB '$KB' produced no snippet match (KB missing or not loaded)" + exit 0 +fi + +FAILED=0 + +# run