Skip to content

fix(snippet): return one candidate per snippet region in multi-component scans - #123

Merged
mscasso-scanoss merged 2 commits into
crc64from
fix/snippet-multicomponent-scan
Oct 26, 2026
Merged

mscasso-scanoss merged 2 commits into
crc64from
fix/snippet-multicomponent-scan

Conversation

@mscasso-scanoss

@mscasso-scanoss mscasso-scanoss commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A file that concatenates code from multiple components returned a single candidate and segfaulted with -M ≥ 2. This affects both -M/--max-snippets and -S/--snippet-scan (the /scan/snippets endpoint path), which share the selection logic.

Reproduced locally against the ossv2 KB with the WFP of main.c (nginx/tomcat/curl/httpd/postgres concatenation):

  • scanoss -M 1 → a single candidate (curl).
  • scanoss -M 9 → segfault (report.c:337, scan_ower == NULL).
  • scanoss -S - → a single snippet_group with 29 candidates (all tomcat).

Diagnosis and fix

1. Segfault with -M ≥ 2 · src/match.c
match_data_copy() did not copy scan_ower (nor lines_matched). The multi-snippet path copies the matches, so print_json_match() dereferenced a null scan_ower. Both fields are now copied.

2. Collapse into a single block · src/snippet_selection.c
biggest_snippet() created one list per region but appended every candidate to matches_list_array[i] (almost always index 0), so all of them competed in a single list and only one component survived. Rewritten: each match goes to the list of the region whose ranges it intersects; if none intersects and there is room, it opens a new list for a non-overlapping region, up to MAX_MULTIPLE_COMPONENTS.

3. -S/--snippet-scan collapsed by default · src/purl_scan.c
The discovery endpoint used SCAN_MAX_SNIPPETS_DEFAULT (= 1), collapsing the output into a single group regardless of the input (the API calls -S without -M). It now opens up to MAX_MULTIPLE_COMPONENTS groups unless the caller explicitly raises -M.

Ordering by relevance · src/match.c
New more_lines_matched comparator so the block with the most covered lines becomes the main match (match_index 0).

Verified results (KB ossv2)

Command Result
-M 9 main.c curl (203-250) + httpd (255-278)
-M 9 --ignore-file-ext main.c tomcat (114-196) + curl (203-250) + httpd (255-278)
-M 9 main.java tomcat only (114-196)
-M 9 main.py no match
-S - (without -M) 3 groups: tomcat / httpd / curl

No segfault in -M 1..9; clean valgrind in -M 9 and -S.

Pending (not included, point 3 of the issue)

nginx (100-line block) and postgres (10 lines) still do not appear. Cause: biggest_snippet() only considers the file with the most hits per sector, and before that the matchmap prioritizes low-popularity hashes and cuts off at MAX_MATCHMAP_FILES. The nginx block (auto/module, build boilerplate) is made of very popular hashes (each one in 900-3600 OSS files) that fall outside the cutoff, so the file never enters the matchmap with enough hits. This is a recall-vs-performance tradeoff; it is marked as PENDING in the test.

Test

tests/snippet_multicomponent/ — validates the 4 -M cases, the ordering by relevance, and the -S output (≥3 groups without passing -M). It skips gracefully if no KB/binary is available, so it does not break CI that builds without a KB.

🤖 Generated with Claude Code

mscasso-scanoss and others added 2 commits October 16, 2026 21:57
…ent 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) <noreply@anthropic.com>
Bump SCANOSS_VERSION 6.0.1-crc64 -> 6.0.2-crc64.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mscasso-scanoss mscasso-scanoss self-assigned this Oct 26, 2026
@mscasso-scanoss
mscasso-scanoss merged commit a4f6286 into crc64 Oct 26, 2026
1 check passed
@mscasso-scanoss
mscasso-scanoss deleted the fix/snippet-multicomponent-scan branch October 26, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant