055cc335c485aa94f48b440b0105eae2e1ecacb4
braney
  Thu Sep 10 09:41:34 2026 -0700
lib/fa.c: grow the FASTA read buffer to hold the line being added, refs #38320

faMixedSpeedReadNext decided to grow its buffer when bufIx + lineSize no
longer fit, but then asked expandFaFastBuf to reach only lineSize.
expandFaFastBuf stops doubling as soon as it meets that size, so it could
hand back a buffer smaller than bufIx + lineSize. The copy loop wrote the
whole line regardless.

Ask for bufIx + lineSize + 1, which covers the line and the terminating NUL
written after the loop. The other two calls each add one byte to a buffer
that is exactly full, so doubling always covers them and they are unchanged.

Doubling usually leaves enough room, which is why this took so long to show
up. It needs a record whose lines vary a lot in length. faToTwoBit aborted
on such a file with a corrupted heap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

diff --git src/lib/fa.c src/lib/fa.c
index 61f448b5ed0..5ab33251fe4 100644
--- src/lib/fa.c
+++ src/lib/fa.c
@@ -459,31 +459,31 @@
 else
     {
     errAbort("Expecting '>' line %d of %s", lf->lineIx, lf->fileName);
     }
 /* Read until next '>' */
 for (;;)
     {
     if (!lineFileNext(lf, &line, &lineSize))
         break;
     if (line[0] == '>')
         {
 	lineFileReuse(lf);
 	break;
 	}
     if (bufIx + lineSize >= faFastBufSize)
-	expandFaFastBuf(bufIx, lineSize);
+	expandFaFastBuf(bufIx, bufIx + lineSize + 1);
     for (i=0; i<lineSize; ++i)
         {
 	c = line[i];
 	if (isalpha(c) || c == '-')
 	    faFastBuf[bufIx++] = c;
 	}
     }
 if (bufIx >= faFastBufSize)
     expandFaFastBuf(bufIx, 0);
 faFastBuf[bufIx] = 0;
 *retDna = faFastBuf;
 *retSize = bufIx;
 *retName = name;
 if (bufIx == 0)
     {