6c676a0e9ed642676008731762141e0c16822787
braney
  Sat Aug 22 09:31:14 2026 -0700
vcf: tighten validation of the genotype allele index, refs #38155

New parseAlleleIx returns missing data for a GT allele index that the record
has no allele for, and for a value that will not fit the field.  It checks the
value as an int, before it reaches the field.  vcfParseGenotypes and
vcfParseGenotypesGtOnly both go through it, and a check after the per-genotype
loop covers the PL and SGT fallbacks, which choose an index without consulting
the allele count.

hapIxA and hapIxB become signed char.  Plain char is unsigned on some
architectures, and the negative missing-data value did not stay negative there.
Size and field offsets are unchanged, and x86 code generation is identical.

Adds a lib/tests case for malformed and boundary genotype indexes.

diff --git src/lib/tests/makefile src/lib/tests/makefile
index 4edefea0e3e..879bd33e16c 100644
--- src/lib/tests/makefile
+++ src/lib/tests/makefile
@@ -302,60 +302,67 @@
 	${tabixTester} input/YRI.trio.2010_06.novelsequences.sites.vcf.gz 2:26790860-194631353 > output/$@.out
 	diff expected/$@.out output/$@.out
 
 tabixFetch1kGWithGenotypes: ${tabixTester} mkdirs
 	${tabixTester} input/YRI.low_coverage.2010_07_excerpt.genotypes.vcf.gz 2:26793738-26794385 > output/$@.out
 	diff expected/$@.out output/$@.out
 
 ${BIN_DIR}/tabixFetch: tabixFetch.o ${MYLIBS}
 	${MKDIR} ${BIN_DIR}
 	${CC} ${COPT} -o ${BIN_DIR}/tabixFetch tabixFetch.o ${MYLIBS} ${L}
 
 
 # vcf:
 vcfTester=${BIN_DIR}/vcfParseTest
 vcfTest: vcfParse1kGNoGenotypes vcfParse1kGWithGenotypes vcfParseOldV3 \
-	vcfHeader1kGNoGenotypes vcfHeader1kGWithGenotypes vcfHeaderOldV3
+	vcfHeader1kGNoGenotypes vcfHeader1kGWithGenotypes vcfHeaderOldV3 \
+	vcfParseBadGenotypeIx
 
 vcfParse1kGNoGenotypes: ${vcfTester} mkdirs
 	${vcfTester} input/YRI.trio.2010_06.novelsequences.sites.vcf.gz 2 26790859 194631353 > output/$@.out
 	diff expected/$@.out output/$@.out
 
 vcfParse1kGWithGenotypes: ${vcfTester} mkdirs
 	${vcfTester} input/YRI.low_coverage.2010_07_excerpt.genotypes.vcf.gz 2 26793737 26794385 > output/$@.out
 	diff expected/$@.out output/$@.out
 
 vcfParseOldV3: ${vcfTester} mkdirs
 	${vcfTester} input/20091110_pilot1_vcf_merged_call_sets_YRI.2and3_way.vcf.gz 1 3000 50000 >& output/$@.out
 	diff expected/$@.out output/$@.out
 
 # Regression tests for the tabix header-read path (htslib >= 1.21 tbx_readrec
 # strips meta_char lines, so the VCF header must be read off the htsFile
 # directly, not via the tabix iterator).  These check version, def counts, and
 # sample IDs -- all of which silently degrade if the header parser sees nothing.
 vcfHeader1kGNoGenotypes: ${vcfTester} mkdirs
 	${vcfTester} -headerOnly input/YRI.trio.2010_06.novelsequences.sites.vcf.gz > output/$@.out
 	diff expected/$@.out output/$@.out
 
 vcfHeader1kGWithGenotypes: ${vcfTester} mkdirs
 	${vcfTester} -headerOnly input/YRI.low_coverage.2010_07_excerpt.genotypes.vcf.gz > output/$@.out
 	diff expected/$@.out output/$@.out
 
 vcfHeaderOldV3: ${vcfTester} mkdirs
 	${vcfTester} -headerOnly input/20091110_pilot1_vcf_merged_call_sets_YRI.2and3_way.vcf.gz > output/$@.out
 	diff expected/$@.out output/$@.out
 
+# A GT allele index that this record has no allele for must parse as missing data, so that
+# every caller sees either a real allele or missing data.
+vcfParseBadGenotypeIx: ${vcfTester} mkdirs
+	${vcfTester} -genotypes input/badGenotypeIx.vcf.gz chr1 0 10000 > output/$@.out
+	diff expected/$@.out output/$@.out
+
 ${BIN_DIR}/vcfParseTest: vcfParseTest.o ${MYLIBS}
 	${MKDIR} ${BIN_DIR}
 	${CC} ${COPT} -o ${BIN_DIR}/vcfParseTest vcfParseTest.o ${MYLIBS} ${L}
 
 
 # hacTree:
 hacTreeTester=${BIN_DIR}/hacTreeTest
 hacTreeTest: ${hacTreeTester} mkdirs
 	${hacTreeTester} input/$@.txt output/$@.out
 	diff expected/$@.out output/$@.out
 
 ${BIN_DIR}/hacTreeTest: hacTreeTest.o ${MYLIBS}
 	${MKDIR} ${BIN_DIR}
 	${CC} ${COPT} -o ${BIN_DIR}/hacTreeTest hacTreeTest.o ${MYLIBS} ${L}