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/vcf.c src/lib/vcf.c
index 723ce9d72f5..c942defea80 100644
--- src/lib/vcf.c
+++ src/lib/vcf.c
@@ -1275,38 +1275,39 @@
     if (sameString(key, def->key))
 	return def;
     }
 return NULL;
 }
 
 static enum vcfInfoType typeForGtFormat(struct vcfFile *vcff, const char *key)
 /* Look up the type of FORMAT component key, in the definitions from the header,
  * and failing that, from the keys reserved in the spec. */
 {
 struct vcfInfoDef *def = vcfInfoDefForGtKey(vcff, key);
 return def ? def->type : vcfInfoString;
 }
 
 static signed char parseAlleleIx(char *string, int alleleCount)
-/* Parse one allele index out of a GT field.  Return -1 for missing data, and also for an
- * index that this record has no allele for, so that every caller sees either a real allele
- * or missing data. */
+/* Parse one allele index out of a GT field.  Return -1 for missing data, for an index that
+ * this record has no allele for, and for an index too large for the signed char that holds it,
+ * so that every caller sees either a real allele or missing data.  A record may have up to
+ * VCF_MAX_INFO alleles, so alleleCount alone is not a tight enough bound. */
 {
 if (string[0] == '.')
     return -1;
 int alleleIx = atoi(string);
-if (alleleIx < 0 || alleleIx >= alleleCount)
+if (alleleIx < 0 || alleleIx >= alleleCount || alleleIx > SCHAR_MAX)
     return -1;
 return alleleIx;
 }
 
 static void parseGt(char *genotype, struct vcfGenotype *gt, int alleleCount)
 /* Parse genotype, which should be something like "0/0", "0/1", "1|0" or "0/." into gt fields. */
 {
 char *sep = strchr(genotype, '|');
 if (sep != NULL)
     gt->isPhased = TRUE;
 else
     sep = strchr(genotype, '/');
 gt->hapIxA = parseAlleleIx(genotype, alleleCount);
 if (sep == NULL)
     gt->isHaploid = TRUE;