From 899e473c3b4872a9001ce1df60e6cb575502ebb0 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 23 Mar 2026 13:12:44 -0700 Subject: archive/tar: limit the number of old GNU sparse format entries We did not set a limit on the maximum size of sparse maps in the old GNU sparse format. Set a limit based on the cumulative size of the extension blocks used to encode the map (consistent with how we limit the sparse map size for other formats). Add an additional limit to the total number of sparse file entries, regardless of encoding, to all sparse formats. Thanks to Colin Walters (walters@verbum.org), Uuganbayar Lkhamsuren (https://github.com/uug4na), and Jakub Ciolek for reporting this issue. Fixes #78301 Fixes CVE-2026-32288 Change-Id: I84877345d7b41cc60c58771860ba70e16a6a6964 Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/3901 Reviewed-by: Damien Neil Reviewed-by: Roland Shoemaker Reviewed-on: https://go-review.googlesource.com/c/go/+/763766 Auto-Submit: David Chase TryBot-Bypass: David Chase LUCI-TryBot-Result: Go LUCI Reviewed-by: Jakub Ciolek --- src/archive/tar/format.go | 6 ++++++ src/archive/tar/reader.go | 28 ++++++++++++++++++++++++---- src/archive/tar/reader_test.go | 11 +++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/archive/tar/format.go b/src/archive/tar/format.go index 9954b4d9f5..32e58a9d9b 100644 --- a/src/archive/tar/format.go +++ b/src/archive/tar/format.go @@ -147,6 +147,12 @@ const ( // Max length of a special file (PAX header, GNU long name or link). // This matches the limit used by libarchive. maxSpecialFileSize = 1 << 20 + + // Maximum number of sparse file entries. + // We should never actually hit this limit + // (every sparse encoding will first be limited by maxSpecialFileSize), + // but this adds an additional layer of defense. + maxSparseFileEntries = 1 << 20 ) // blockPadding computes the number of bytes needed to pad offset up to the diff --git a/src/archive/tar/reader.go b/src/archive/tar/reader.go index 16ac2f5b17..f85b998625 100644 --- a/src/archive/tar/reader.go +++ b/src/archive/tar/reader.go @@ -490,7 +490,8 @@ func (tr *Reader) readOldGNUSparseMap(hdr *Header, blk *block) (sparseDatas, err } s := blk.toGNU().sparse() spd := make(sparseDatas, 0, s.maxEntries()) - for { + totalSize := len(s) + for totalSize < maxSpecialFileSize { for i := 0; i < s.maxEntries(); i++ { // This termination condition is identical to GNU and BSD tar. if s.entry(i).offset()[0] == 0x00 { @@ -501,7 +502,11 @@ func (tr *Reader) readOldGNUSparseMap(hdr *Header, blk *block) (sparseDatas, err if p.err != nil { return nil, p.err } - spd = append(spd, sparseEntry{Offset: offset, Length: length}) + var err error + spd, err = appendSparseEntry(spd, sparseEntry{Offset: offset, Length: length}) + if err != nil { + return nil, err + } } if s.isExtended()[0] > 0 { @@ -510,10 +515,12 @@ func (tr *Reader) readOldGNUSparseMap(hdr *Header, blk *block) (sparseDatas, err return nil, err } s = blk.toSparse() + totalSize += len(s) continue } return spd, nil // Done } + return nil, errSparseTooLong } // readGNUSparseMap1x0 reads the sparse map as stored in GNU's PAX sparse format @@ -586,7 +593,10 @@ func readGNUSparseMap1x0(r io.Reader) (sparseDatas, error) { if err1 != nil || err2 != nil { return nil, ErrHeader } - spd = append(spd, sparseEntry{Offset: offset, Length: length}) + spd, err = appendSparseEntry(spd, sparseEntry{Offset: offset, Length: length}) + if err != nil { + return nil, err + } } return spd, nil } @@ -620,12 +630,22 @@ func readGNUSparseMap0x1(paxHdrs map[string]string) (sparseDatas, error) { if err1 != nil || err2 != nil { return nil, ErrHeader } - spd = append(spd, sparseEntry{Offset: offset, Length: length}) + spd, err = appendSparseEntry(spd, sparseEntry{Offset: offset, Length: length}) + if err != nil { + return nil, err + } sparseMap = sparseMap[2:] } return spd, nil } +func appendSparseEntry(spd sparseDatas, ent sparseEntry) (sparseDatas, error) { + if len(spd) >= maxSparseFileEntries { + return nil, errSparseTooLong + } + return append(spd, ent), nil +} + // Read reads from the current file in the tar archive. // It returns (0, io.EOF) when it reaches the end of that file, // until [Next] is called to advance to the next file. diff --git a/src/archive/tar/reader_test.go b/src/archive/tar/reader_test.go index c7611ca044..a324674cb7 100644 --- a/src/archive/tar/reader_test.go +++ b/src/archive/tar/reader_test.go @@ -1145,6 +1145,17 @@ func TestReadOldGNUSparseMap(t *testing.T) { input: makeInput(FormatGNU, "", makeSparseStrings(sparseDatas{{10 << 30, 512}, {20 << 30, 512}})...), wantMap: sparseDatas{{10 << 30, 512}, {20 << 30, 512}}, + }, { + input: makeInput(FormatGNU, "", + makeSparseStrings(func() sparseDatas { + var datas sparseDatas + // This is more than enough entries to exceed our limit. + for i := range int64(1 << 20) { + datas = append(datas, sparseEntry{i * 2, (i * 2) + 1}) + } + return datas + }())...), + wantErr: errSparseTooLong, }} for i, v := range vectors { -- cgit v1.3