Bug 4 - Duplicate symbols (~43 entries, 195 extra): When a fn signature was both deleted and added (modified), each line independently produced a symbol. Added dedup to the fn-declaration path that upgrades existing entries to Modified instead of adding duplicates. Bug 5 - Trait name as type (~146 symbols): Truncated hunk headers like `impl From<X>` (without `for ActualType`) caused the trait name to be captured as the implementing type, producing wrong symbols like `From::from`. Added is_std_trait() blocklist to return None for well-known traits when `for` is absent, falling through to the safer `<method>` placeholder. Added 4 regression tests. All 51 tests pass. https://claude.ai/code/session_01P1LKP6aqGt68rQAXrF6kSE
7.1 KiB
Lessons Learned from Spot-Checking vuln_db.json
Summary
A spot-check of the enriched vulnerability database (642 entries, 147 with symbols) uncovered three classes of bugs in diff_analyzer.rs, all now fixed with regression tests.
Bug 1: for keyword leaking into symbol names (~14 entries)
Symptom: Symbols like instance::for PyObject::from instead of instance::PyObject::from.
Root cause: The impl_re regex used (?:<.*>)? to skip generic params after impl. The greedy .* matched across nested <> brackets. For impl<T> From<Py<T>> for PyObject, the <.*> consumed <T> From<Py<T>> (everything up to the last >), leaving for PyObject as the "type" capture.
Fix: Replaced the regex-based impl parser with parse_impl_type(), which uses bracket-counting (skip_balanced_angles) to correctly handle nested generics, then finds for only at the top level (not inside <>).
Lesson: Greedy regex quantifiers inside delimiter pairs (<.*>) are a classic source of over-matching when the delimiters can nest. Use iterative bracket-counting instead.
Bug 2: where clause leaking into symbol names (~5 entries)
Symptom: Symbols like lock_api::mutex::where::<method> or array::where T: HasAfEnum::get_backend.
Root cause: Same as Bug 1 — the regex type capture ([a-zA-Z_][a-zA-Z0-9_:<>, ]*) included spaces and continued matching through where clauses. Even after generics were (incorrectly) consumed, the remaining text was slurped into the type name.
Fix: The new parse_impl_type() uses find_top_level_keyword(type_str, " where ") to stop the type capture at where clauses, again only at the top bracket-nesting level.
Lesson: When parsing structured syntax, stop-conditions matter as much as start-conditions. The type name capture needed explicit terminators (where, {, newline).
Bug 3: Test functions included in symbols (~39 entries)
Symptom: Symbols like pycell::impl_::test_inherited_size or fyrox_core::test_combine_uuids appearing as "vulnerable symbols" even though they're test functions.
Root cause: The is_test_file() filter only checked file paths (tests/, *_test.rs, etc.). Functions marked with #[test] or #[cfg(test)] inside library source files (src/lib.rs, src/pycell/impl_.rs, etc.) were not caught.
Fix: Added two filters in extract_symbols():
- Track
#[test]and#[cfg(test)]attributes in diff lines; skip the next fn declaration when seen. - Skip any function whose name starts with
test_(standard Rust convention).
Lesson: Path-based test detection is necessary but not sufficient. Rust commonly has #[cfg(test)] mod tests { ... } inside library files. Attribute-level and naming-convention filters are needed too.
Verified Good Entries
Not everything was broken. Several entries verified correctly against their actual GitHub commits:
- RUSTSEC-2026-0076 (libcrux-ml-dsa):
libcrux_ml_dsa::encoding::signature::deserialize— correct crate name conversion from hyphenated path, correct function extraction, test files properly filtered. - RUSTSEC-2022-0022 (hyper):
Client::record_header_indices— correctly identified fromMaybeUninitsafety fix. Impl typesServer/Clientproperly tracked. - RUSTSEC-2023-0001 (tokio): Named pipe functions (
pipe_mode,opts_default_pipe_mode) plausible forreject_remote_clientsfix.
Bug 4: Duplicate symbols within entries (~43 entries, 195 extra symbols)
Symptom: The same function appearing 2+ times in an entry, e.g. open_mmap x2.
Root cause: When a function signature is both removed (-fn foo(old: X)) and added (+fn foo(new: Y)), both lines match fn_decl_re independently, each producing a symbol. The existing dedup check (symbols.iter().any(...)) only applied to the body-change code path (lines 131-154), not the fn-declaration code path (lines 106-124).
Fix: Added dedup to the fn-declaration path: when a symbol with the same qualified name already exists, upgrade its change_type to Modified instead of adding a duplicate. This is semantically correct — if a fn is both deleted and added, it was modified.
Bug 5: Trait name captured as implementing type (~146 symbols)
Symptom: Symbols like From::from, Into::new, TryFrom::try_from where a standard trait name appears where the implementing type should be.
Root cause: Hunk headers often show truncated impl lines like impl From<Error> without the for ActualType part (which is on the next line or beyond the context window). parse_impl_type correctly extracted the text after impl<generics>, but without a for, it captured the trait name as the type.
Fix: Added is_std_trait() blocklist — when parse_impl_type finds no for keyword and the captured type name is a well-known trait (From, Into, TryFrom, Clone, Default, Iterator, operator traits, serde traits, etc.), it returns None instead of the trait name. This causes the symbol to fall through to the <method> placeholder, which is less precise but at least not wrong.
Lesson: When parsing partial/truncated context (like git hunk headers), a wrong answer is worse than no answer. The <method> placeholder is imprecise but correct; From::from is precise but wrong and will never match real call sites.
Remaining Known Limitations
These are documented in CLAUDE.md but not yet fixed:
<method>placeholders (~90+ entries): When git hunk headers don't include the fn declaration, the tool recordsType::<method>. This is a fundamental limitation of diff-based analysis — the fn context depends on git's hunk header generation, which uses a limited window. The trait-name fix increases this count slightly (trait impls now fall through to<method>instead ofTrait::method).- Missing crate name prefix: For non-workspace repos,
src/foo/bar.rs→foo::barinstead ofcrate_name::foo::bar. The crate name isn't available from the file path alone; fixing this would require readingCargo.tomlfrom the repo. - Empty symbol lists (495/642): Many advisories lack GitHub commit references, or the referenced commits don't contain extractable Rust function changes (version bumps only, C code, etc.).
Testing Strategy
Each fix has targeted regression tests using synthetic diffs that reproduce the exact patterns found in the real vuln_db:
test_for_keyword_no_longer_leaks_into_symbol— reproduces pyo3'simpl<T> From<Py<T>> for PyObjecttest_where_clause_no_longer_leaks_into_symbol— reproduces lock_api'simpl ... where R: Sendtest_test_functions_filtered_by_attribute—#[test]fn inside library sourcetest_test_functions_filtered_by_name_prefix—test_*naming conventiontest_cfg_test_functions_filtered—#[cfg(test)]attributetest_parse_impl_type_*— unit tests for the bracket-counting parsertest_duplicate_fn_deduped_as_modified— dedup when same fn is both+and-test_trait_name_not_captured_as_type— blocklist preventsFrom/Intoas typetest_trait_as_type_not_in_symbols— end-to-end with truncated hunk headertest_is_std_trait— unit test for the trait blocklist