From 988c629a10a33d40d6b67969a0e8bf69dadef9bb Mon Sep 17 00:00:00 2001 From: kaizenman <15638776+kaizenman@users.noreply.github.com> Date: Sat, 25 Apr 2026 01:37:37 +0200 Subject: [PATCH] fix(importer): bound runaway loops on corrupt count fields in GP3-5 binary parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gp3To5Importer.readBend and readTremoloBarEffect read a pointCount integer from the input stream and then loop pointCount times, reading 9 bytes per iteration. Neither validates that pointCount * 9 fits in the remaining stream. When the parser becomes misaligned mid-stream (corrupt or crafted input) pointCount can be read from random bytes and reach ~2^31. ByteBuffer.readByte returns -1 silently at EOF rather than throwing, so the loop never terminates from the input side; each iteration allocates a BendPoint object and V8 OOM-crashes after ~10s. Reject counts on the basis that count * bytesPerItem must fit in the remaining stream — a mathematical constraint of the format, not a magic cap. Real bends have ~30 points (270 bytes), well below any stream's remaining capacity, so the check never fires on valid input. Fixture: a 3.8 KB GP5 from a real-world corpus where pointCount reads as 587530544 against 1413 bytes remaining (6 orders of magnitude mismatch). Pre-fix Node OOM-crashes; post-fix throws UnsupportedFormatError in <100ms. --- packages/alphatab/src/importer/Gp3To5Importer.ts | 13 +++++++++++++ .../guitarpro5/corrupted-bend-point-count.gp5 | Bin 0 -> 3803 bytes .../alphatab/test/importer/Gp5Importer.test.ts | 10 ++++++++++ 3 files changed, 23 insertions(+) create mode 100755 packages/alphatab/test-data/guitarpro5/corrupted-bend-point-count.gp5 diff --git a/packages/alphatab/src/importer/Gp3To5Importer.ts b/packages/alphatab/src/importer/Gp3To5Importer.ts index cc74873be..7dbf63617 100644 --- a/packages/alphatab/src/importer/Gp3To5Importer.ts +++ b/packages/alphatab/src/importer/Gp3To5Importer.ts @@ -1039,6 +1039,8 @@ export class Gp3To5Importer extends ScoreImporter { IOHelper.readInt32LE(this.data); // value const pointCount: number = IOHelper.readInt32LE(this.data); + // 9 bytes per point (i32 offset + i32 value + 1 byte vibrato) + Gp3To5Importer._requireFits(this.data, pointCount, 9, 'whammy bar point count'); if (pointCount > 0) { for (let i: number = 0; i < pointCount; i++) { const point: BendPoint = new BendPoint(0, 0); @@ -1053,6 +1055,15 @@ export class Gp3To5Importer extends ScoreImporter { } } + private static _requireFits(data: IReadable, count: number, bytesPerItem: number, fieldName: string): void { + const remaining = data.length - data.position; + if (count < 0 || count * bytesPerItem > remaining) { + throw new UnsupportedFormatError( + `${fieldName}=${count} (${count * bytesPerItem} bytes) exceeds remaining ${remaining} bytes` + ); + } + } + private static _toStrokeValue(value: number): number { switch (value) { case 1: @@ -1337,6 +1348,8 @@ export class Gp3To5Importer extends ScoreImporter { IOHelper.readInt32LE(this.data); // value const pointCount: number = IOHelper.readInt32LE(this.data); + // 9 bytes per point (i32 offset + i32 value + 1 byte vibrato) + Gp3To5Importer._requireFits(this.data, pointCount, 9, 'bend point count'); if (pointCount > 0) { for (let i: number = 0; i < pointCount; i++) { const point: BendPoint = new BendPoint(0, 0); diff --git a/packages/alphatab/test-data/guitarpro5/corrupted-bend-point-count.gp5 b/packages/alphatab/test-data/guitarpro5/corrupted-bend-point-count.gp5 new file mode 100755 index 0000000000000000000000000000000000000000..4effb7d976be8305c43884008f9fb72dd6f29342 GIT binary patch literal 3803 zcmeHJ-A)rh6y7eBLgh~l#tXzr$P$I1U?gC?X=xEQ0o!c1AzoIxLSt#k?v{in@F{!; zyf%8FH(r_Q1NbP^^UZYkEXA4V;~FeRp}4MIN9c9fNL#j9unpyx-d->67}v=#xm$P3o_Kj8jNP1-Ehyzq!>jB` zSNs22jXBVBmL`gqifA zLphadB2#sUFN%CcWGkLuchNyG%}Dz2NZ9q76`AMpokua&PArG=SKS1{Du= z{9oV*Y(_<}j%Q{dcow$O#h@LF-?}E(MFx>{6*$;=yEtL)5eSB)n-p1y_i;WKl@+cN zN { } } }); + + it('corrupted-bend-point-count', async () => { + // Regression: misaligned read produced pointCount ~5e8, OOM-crashing the + // import. Now rejected upfront with a typed error. + const reader = await GpImporterTestHelper.prepareImporterWithFile( + 'guitarpro5/corrupted-bend-point-count.gp5' + ); + expect(() => reader.readScore()).to.throw(UnsupportedFormatError); + }); });