fix(contigs): guard the out-of-bounds ploidy read and the supercluster-merge null deref (#250) - #251
Open
TimD1-bot wants to merge 2 commits into
Open
fix(contigs): guard the out-of-bounds ploidy read and the supercluster-merge null deref (#250)#251TimD1-bot wants to merge 2 commits into
TimD1-bot wants to merge 2 commits into
Conversation
TimD1
reviewed
Aug 11, 2026
| * nc at 0 as wf_swg_cluster() does, so `clusters` is never empty and `clusters[0]` is always | ||
| * readable. Callers rely on that because a contig is superclustered whenever *either* callset has | ||
| * variants there. | ||
| * |
TimD1
reviewed
Aug 11, 2026
| /** | ||
| * @brief Intersects reference FASTA, query VCF, truth VCF, and optional BED regions, retaining only common contigs. | ||
| * | ||
| * On return, both callsets carry identical `contigs` lists and every contig in them is indexable in |
Collaborator
Author
There was a problem hiding this comment.
Claude Opus 5 🤖: Removed in 4acd15e. I also reverted the two injection-site comments to their original wording, since they referred to "the invariant" and nothing defined it any more. Each guard keeps its own one-line reason at the site.
TimD1
force-pushed
the
250_td_guard-ctg-oob
branch
from
August 11, 2026 21:26
4acd15e to
7ffe250
Compare
TimD1
force-pushed
the
250_td_guard-ctg-oob
branch
from
August 11, 2026 21:34
7ffe250 to
6f08676
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
Authorship: the content below was drafted by Claude Opus 5 (an AI coding agent) and
filed via
ghunder @TimD1-bot, a bot account operated by @TimD1. It reflects theagent's analysis, not a statement authored by @TimD1.
Closes #250.
What
Guards the two memory-safety defects in the contig-handling path so that a violated
invariant degrades to a skipped contig rather than undefined behaviour.
Neither defect is reachable on
devtoday. Both are held unreachable by a singleunstated invariant, so this changes no output and no metric —
vcfdist's behaviour on anyinput that reaches these lines today is byte-identical.
The invariant
intersect_contigs()leaves the query and truth callsets carrying identicalcontigslists, with every contig in those lists indexable in both callsets'
variants[hap]maps.It gets there by pruning both callsets to the BED contigs and injecting every BED contig
either one lacks, or — with no BED — by cross-injecting query into truth and truth into
query. Two consumers read past the end of a vector or dereference null without it, which
made the pruning load-bearing rather than merely tidy.
#250 proposed documenting that in
intersect_contigs()'s doc comment; both added docparagraphs were removed on review, so the invariant is recorded in #250 rather than in the
source. Each guard carries its own one-line reason at the site.
The two guards
src/bed.cpp:643std::findbecamequery->contigs.size()and indexedobserved_ploidiesout of boundssrc/cluster.cpp:100vars[h][ctg]inserted a default-constructed nullshared_ptrfor an absent contig, then->ndereferenced itsuperclusterDatabuildsthis->contigsas the union of both callsets' contig lists,which is why the merge can be handed a contig one callset never declared at all. Since
supercluster()readssuperclusters[ctg]->callset_vars[c]->nfor every contig in thatsame union, the merge still has to leave a container behind for a contig it skips — so the
undeclared case routes into the existing
!nvarsbranch, which already pushes the singletrailing boundary that read needs.
Both guards cost nothing on the hot path: the merge runs once per contig per callset, not
per variant.
Verification
dev's test suite cannot distinguish these builds, because the invariant holds on everyinput it feeds them — 839 unit tests and 132 integration workflows pass identically before
and after. So the guards were verified against a deliberately broken tree instead: an
AddressSanitizer build with the cross-injection and the FASTA pruning removed from
intersect_contigs(), run on a two-contig synthetic pair where one callset omitssc2from its VCF header (a contig present in the header but never called is still
indexable, so header absence is what the second defect needs).
With the guards removed, both defects reproduce in exactly the two frames above (the
bed.cppline number is lower on that tree, since removing the injection shortened thefile):
With the guards in place and the invariant still broken, both directions exit 0 with no
sanitizer report. The tampering was local to the experiment and is not part of this
branch.
Tests
None added, at the author's request. #250 proposed three — the invariant asserted
directly, a
superclusterDatabuilt from mismatched contig lists, and the ploidycomparison under ASan — and they remain unwritten. Two of them would also need the ASan
workarounds #250 records, since
CaptureStderr()swallows the sanitizer report and theprebuilt
libgtestis uninstrumented.declare_contig()'s doc comment intests/unit/src/test_cluster.cppdescribed the mergeas dereferencing without a null check, which is no longer true, so it now describes the
guard instead.