de609b7489963f32ad474619cb3dff0283ef7e76
braney
  Sun Aug 23 15:13:46 2026 -0700
vcf: bound the genotype allele index by the field it is stored in, refs #38155

parseAlleleIx checked the index against alleleCount only. A record can hold up
to VCF_MAX_INFO alleles, so an index of 128 or more could pass that check and
then narrow on the way into the signed char field. The narrowed value was
sometimes another real allele of the record: with 260 ALT alleles, index 260
came out as 4. The parser then reported a genotype the VCF never named.

The check now also rejects an index above SCHAR_MAX, so an index too large for
the field reads as missing data. SCHAR_MAX and not CHAR_MAX, because CHAR_MAX
is 255 on the unsigned char platforms that the field is declared signed for.

Every other assignment to hapIxA and hapIxB in this file is a literal in the
range -1 to 2, so parseAlleleIx was the only path that could carry an
out-of-range value.

New test vcfParseManyAlleles, with a record of 260 ALT alleles. Without the
fix, GT 128/1 reads as -128/1, 130/130 as -126/-126, and 260/260 as 4/4.

diff --git src/lib/tests/makefile src/lib/tests/makefile
index 879bd33e16c..78760d99568 100644
--- src/lib/tests/makefile
+++ src/lib/tests/makefile
@@ -303,31 +303,31 @@
 	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 \
-	vcfParseBadGenotypeIx
+	vcfParseBadGenotypeIx vcfParseManyAlleles
 
 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
@@ -339,30 +339,38 @@
 
 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
 
+# A record can have more alleles than fit in the signed char that holds a genotype's allele
+# index.  An index too large for the field must also parse as missing data, instead of being
+# silently narrowed to some other value.  input/manyAlleles.vcf.gz has 260 ALT alleles, so
+# index 128 would narrow to a negative value and index 260 to 4, a real allele of that record.
+vcfParseManyAlleles: ${vcfTester} mkdirs
+	${vcfTester} -genotypes input/manyAlleles.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}