mirror of
https://github.com/anchore/syft.git
synced 2026-08-19 16:48:27 +02:00
fix(dotnet): bound the bundle signature search by the file, not the headers
The single-file bundle marker search sized its buffer from `calculatePEEndOffset` / `calculateELFEndOffset`, both of which sum user-controlled header fields. A tiny file declaring 4GB sections drove an 8.6GB allocation, and on the ELF side the sum could overflow negative and panic in `make`. The search now clamps its window to the real file length before sizing anything. The PE and ELF paths had grown two copies of the same find-and-read sequence, so they collapse into one `bundle.ExtractDepsJSON`, which lets the signature search and the header reader both go unexported. Also guards `read7BitEncodedInt` against returning a negative length, which on a 32-bit `int` would seek a manifest walk backwards and re-read the same bytes per declared file. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
parent
360dbc04aa
commit
57f54350da
@ -1,6 +1,7 @@
|
|||||||
package bundle
|
package bundle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -15,6 +16,54 @@ var dotNetBundleSignature = []byte{
|
|||||||
0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae,
|
0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExtractDepsJSON returns the deps.json embedded in the .NET single-file bundle in r, or "" if r carries no
|
||||||
|
// bundle marker.
|
||||||
|
//
|
||||||
|
// searchLimit is where the caller's format parsing says the executable structure ends, which is as far into
|
||||||
|
// the file as the marker can be. It comes from user-controlled header fields, so it may describe far more
|
||||||
|
// than the file holds or overflow negative; it is clamped to the real file length before anything is sized
|
||||||
|
// from it. A non-positive limit reads nothing.
|
||||||
|
func ExtractDepsJSON(r io.ReadSeeker, searchLimit int64) (string, error) {
|
||||||
|
headerOffset, err := findSignatureOffset(r, searchLimit)
|
||||||
|
if err != nil || headerOffset == 0 {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return readDepsJSONFromBundleHeader(r, headerOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// findSignatureOffset searches the start of r for the .NET single-file bundle signature and returns the
|
||||||
|
// bundle header offset stored in the 8 bytes immediately before it, or 0 if the signature is not found.
|
||||||
|
func findSignatureOffset(r io.ReadSeeker, searchLimit int64) (int64, error) {
|
||||||
|
if searchLimit <= 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
end, err := r.Seek(0, io.SeekEnd)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// this scans a whole executable, routinely over 100MB for a single-file bundle, so the buffer is sized
|
||||||
|
// exactly once. An append-growing read holds both arrays at its final growth and would cost well over
|
||||||
|
// twice the file's own size for the same result.
|
||||||
|
searchData := make([]byte, min(searchLimit, end))
|
||||||
|
if _, err := io.ReadFull(r, searchData); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := bytes.Index(searchData, dotNetBundleSignature)
|
||||||
|
if idx == -1 || idx < 8 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return int64(binary.LittleEndian.Uint64(searchData[idx-8 : idx])), nil
|
||||||
|
}
|
||||||
|
|
||||||
// dotNetBundleHeader represents the fixed portion of the bundle header (version 1+)
|
// dotNetBundleHeader represents the fixed portion of the bundle header (version 1+)
|
||||||
type dotNetBundleHeader struct {
|
type dotNetBundleHeader struct {
|
||||||
MajorVersion uint32
|
MajorVersion uint32
|
||||||
@ -44,7 +93,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ReadDepsJSONFromBundleHeader parses the bundle header at the given offset and extracts deps.json content.
|
// ReadDepsJSONFromBundleHeader parses the bundle header at the given offset and extracts deps.json content.
|
||||||
func ReadDepsJSONFromBundleHeader(r io.ReadSeeker, headerOffset int64) (string, error) {
|
func readDepsJSONFromBundleHeader(r io.ReadSeeker, headerOffset int64) (string, error) {
|
||||||
if _, err := r.Seek(headerOffset, io.SeekStart); err != nil {
|
if _, err := r.Seek(headerOffset, io.SeekStart); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -103,6 +152,13 @@ func read7BitEncodedInt(r io.Reader) (int, error) {
|
|||||||
return 0, errors.New("invalid 7-bit encoded int")
|
return 0, errors.New("invalid 7-bit encoded int")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the shift above can carry past int32 where int is 32 bits, and a negative length would seek callers
|
||||||
|
// backwards and let a manifest walk re-read the same bytes for every file it claims
|
||||||
|
if result < 0 {
|
||||||
|
return 0, errors.New("negative 7-bit encoded int")
|
||||||
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
package bundle
|
package bundle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"debug/elf"
|
"debug/elf"
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/internal/elfutil"
|
"github.com/anchore/syft/syft/internal/elfutil"
|
||||||
"github.com/anchore/syft/syft/internal/unionreader"
|
"github.com/anchore/syft/syft/internal/unionreader"
|
||||||
@ -14,51 +10,13 @@ import (
|
|||||||
// ExtractDepsJSONFromELFBundle extracts the deps.json content from a .net singlefile
|
// ExtractDepsJSONFromELFBundle extracts the deps.json content from a .net singlefile
|
||||||
// bundle contained within an ELF bin
|
// bundle contained within an ELF bin
|
||||||
func ExtractDepsJSONFromELFBundle(r unionreader.UnionReader) (string, error) {
|
func ExtractDepsJSONFromELFBundle(r unionreader.UnionReader) (string, error) {
|
||||||
headerOffset, err := findBundleHeaderOffsetInELF(r)
|
|
||||||
if err != nil || headerOffset == 0 {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return ReadDepsJSONFromBundleHeader(r, headerOffset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func findBundleHeaderOffsetInELF(r unionreader.UnionReader) (int64, error) {
|
|
||||||
elfFile, err := elfutil.NewFile(r)
|
elfFile, err := elfutil.NewFile(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil
|
// not an ELF, so not an ELF bundle
|
||||||
|
return "", nil //nolint:nilerr
|
||||||
}
|
}
|
||||||
|
|
||||||
elfEndOffset := calculateELFEndOffset(elfFile)
|
return ExtractDepsJSON(r, calculateELFEndOffset(elfFile))
|
||||||
if elfEndOffset == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// clamp to the actual file size so a malformed ELF (with bogus segment/section
|
|
||||||
// offsets+sizes) can't drive an arbitrarily large allocation below.
|
|
||||||
fileSize, err := r.Seek(0, io.SeekEnd)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if elfEndOffset > fileSize {
|
|
||||||
elfEndOffset = fileSize
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
searchData := make([]byte, elfEndOffset)
|
|
||||||
n, err := io.ReadFull(r, searchData)
|
|
||||||
if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
searchData = searchData[:n]
|
|
||||||
|
|
||||||
idx := bytes.Index(searchData, dotNetBundleSignature)
|
|
||||||
if idx == -1 || idx < 8 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return int64(binary.LittleEndian.Uint64(searchData[idx-8 : idx])), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateELFEndOffset(f *elf.File) int64 {
|
func calculateELFEndOffset(f *elf.File) int64 {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package bundle
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"debug/elf"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -16,10 +17,36 @@ type readSeekCloser struct {
|
|||||||
|
|
||||||
func (readSeekCloser) Close() error { return nil }
|
func (readSeekCloser) Close() error { return nil }
|
||||||
|
|
||||||
// buildELFWithHugeFilesz returns a minimal, parseable ELF64 whose single program header
|
// buildELFWithHugeFilesz returns a minimal, parseable ELF64 whose single program header declares a
|
||||||
// declares an absurd p_filesz. calculateELFEndOffset will compute a huge end offset; the
|
// p_filesz far past the real file. calculateELFEndOffset will compute an 8GB end offset; the bound in
|
||||||
// clamp in findBundleHeaderOffsetInELF must keep the allocation bounded to the real file.
|
// FindSignatureOffset must keep the allocation tied to the real file.
|
||||||
|
//
|
||||||
|
// note: 8GB rather than something astronomical on purpose. `make([]byte, 1<<60)` panics on its own, so a
|
||||||
|
// test built on that value passes whether or not the bound exists; `make([]byte, 8<<30)` succeeds on any
|
||||||
|
// 64-bit host from untouched anonymous mmap, so only the requested read size distinguishes them.
|
||||||
func buildELFWithHugeFilesz() []byte {
|
func buildELFWithHugeFilesz() []byte {
|
||||||
|
return buildELFWithProgHeader(0, 8<<30)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readSizeRecorder records the largest single Read length requested of it, which is what tells a buffer
|
||||||
|
// sized from the file apart from one sized from the headers.
|
||||||
|
type readSizeRecorder struct {
|
||||||
|
*bytes.Reader
|
||||||
|
maxRead int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *readSizeRecorder) Read(p []byte) (int, error) {
|
||||||
|
if len(p) > r.maxRead {
|
||||||
|
r.maxRead = len(p)
|
||||||
|
}
|
||||||
|
return r.Reader.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *readSizeRecorder) Close() error { return nil }
|
||||||
|
|
||||||
|
// buildELFWithProgHeader returns a minimal, parseable ELF64 with a single PT_LOAD program header
|
||||||
|
// carrying the given p_offset and p_filesz. debug/elf does not validate either field.
|
||||||
|
func buildELFWithProgHeader(pOffset, pFilesz uint64) []byte {
|
||||||
const (
|
const (
|
||||||
ehSize = 64
|
ehSize = 64
|
||||||
phSize = 56
|
phSize = 56
|
||||||
@ -45,19 +72,47 @@ func buildELFWithHugeFilesz() []byte {
|
|||||||
// e_shoff/e_shnum left zero so no sections are parsed
|
// e_shoff/e_shnum left zero so no sections are parsed
|
||||||
|
|
||||||
ph := buf[phOff:]
|
ph := buf[phOff:]
|
||||||
le.PutUint32(ph[0:], 1) // p_type = PT_LOAD
|
le.PutUint32(ph[0:], 1) // p_type = PT_LOAD
|
||||||
le.PutUint64(ph[16:], 0) // p_offset
|
le.PutUint64(ph[8:], pOffset) // p_offset (user-controlled)
|
||||||
le.PutUint64(ph[32:], 1<<60) // p_filesz (bogus, attacker-controlled)
|
le.PutUint64(ph[32:], pFilesz) // p_filesz (user-controlled)
|
||||||
le.PutUint64(ph[40:], 1<<60) // p_memsz
|
le.PutUint64(ph[40:], pFilesz) // p_memsz
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExtractDepsJSONFromELFBundle_MalformedFileszDoesNotOverAllocate(t *testing.T) {
|
func TestExtractDepsJSONFromELFBundle_EndOffsetOverflowDoesNotPanic(t *testing.T) {
|
||||||
data := buildELFWithHugeFilesz()
|
// calculateELFEndOffset sums these two uint64 header fields into an int64 and adds 4096, which
|
||||||
|
// overflows to a negative search limit. A negative is not greater than the file size, so it slips
|
||||||
|
// unnoticed into the search window unless a non-positive limit reads nothing.
|
||||||
|
data := buildELFWithProgHeader(0x7FFFFFFFFFFFF000, 0)
|
||||||
|
require.Negative(t, calculateELFEndOffset(mustParseELF(t, data)),
|
||||||
|
"expected these header values to overflow the end offset; the guard below is what this test pins")
|
||||||
|
|
||||||
r := readSeekCloser{bytes.NewReader(data)}
|
r := readSeekCloser{bytes.NewReader(data)}
|
||||||
|
|
||||||
// must not OOM/panic on the bogus p_filesz, and find no bundle signature
|
|
||||||
content, err := ExtractDepsJSONFromELFBundle(r)
|
content, err := ExtractDepsJSONFromELFBundle(r)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Empty(t, content)
|
assert.Empty(t, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustParseELF(t *testing.T, data []byte) *elf.File {
|
||||||
|
t.Helper()
|
||||||
|
f, err := elf.NewFile(bytes.NewReader(data))
|
||||||
|
require.NoError(t, err)
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractDepsJSONFromELFBundle_MalformedFileszDoesNotOverAllocate(t *testing.T) {
|
||||||
|
data := buildELFWithHugeFilesz()
|
||||||
|
|
||||||
|
// sanity: the headers really do describe an end offset far past the file, so the bound is what keeps
|
||||||
|
// the allocation small rather than the input being small
|
||||||
|
require.Greater(t, calculateELFEndOffset(mustParseELF(t, data)), int64(8*1024*1024*1024))
|
||||||
|
|
||||||
|
r := &readSizeRecorder{Reader: bytes.NewReader(data)}
|
||||||
|
|
||||||
|
content, err := ExtractDepsJSONFromELFBundle(r)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, content)
|
||||||
|
assert.LessOrEqual(t, r.maxRead, len(data),
|
||||||
|
"the search must be bounded by the file, not by what the program headers claim")
|
||||||
|
}
|
||||||
|
|||||||
81
syft/pkg/cataloger/internal/dotnet/bundle/bundle_test.go
Normal file
81
syft/pkg/cataloger/internal/dotnet/bundle/bundle_test.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package bundle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fileWithSignatureAt returns a file of the given size carrying the bundle marker (8-byte header
|
||||||
|
// offset followed by the signature) starting at sigStart.
|
||||||
|
func fileWithSignatureAt(size, sigStart int, headerOffset uint64) []byte {
|
||||||
|
data := make([]byte, size)
|
||||||
|
binary.LittleEndian.PutUint64(data[sigStart-8:sigStart], headerOffset)
|
||||||
|
copy(data[sigStart:], dotNetBundleSignature)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindSignatureOffset(t *testing.T) { //nolint:funlen
|
||||||
|
const withMarker = 256
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
data []byte
|
||||||
|
searchLimit int64
|
||||||
|
want int64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no signature present",
|
||||||
|
data: make([]byte, 512),
|
||||||
|
searchLimit: 512,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero limit searches nothing",
|
||||||
|
data: fileWithSignatureAt(withMarker, 64, 0x1234),
|
||||||
|
searchLimit: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// callers sum unsigned header fields into an int64, which overflows to a negative limit for
|
||||||
|
// some header values. This must read nothing rather than sizing a buffer from it.
|
||||||
|
name: "negative limit reads nothing",
|
||||||
|
data: fileWithSignatureAt(withMarker, 64, 0x1234),
|
||||||
|
searchLimit: math.MinInt64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "limit past end of file reads only what exists",
|
||||||
|
data: fileWithSignatureAt(withMarker, 64, 0x1234),
|
||||||
|
searchLimit: math.MaxInt64 / 2,
|
||||||
|
want: 0x1234,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// the bound must not shrink the window below the real file, or a legitimate marker stops being found
|
||||||
|
name: "limit inside the file still finds the marker",
|
||||||
|
data: fileWithSignatureAt(withMarker, 64, 0x1234),
|
||||||
|
searchLimit: 128,
|
||||||
|
want: 0x1234,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "limit truncates the window before the marker",
|
||||||
|
data: fileWithSignatureAt(withMarker, 64, 0x1234),
|
||||||
|
searchLimit: 32,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// there is no room for the 8-byte header offset before the signature
|
||||||
|
name: "signature too close to the start to carry an offset",
|
||||||
|
data: append(append([]byte{0, 0}, dotNetBundleSignature...), make([]byte, 32)...),
|
||||||
|
searchLimit: 128,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := findSignatureOffset(bytes.NewReader(tt.data), tt.searchLimit)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,23 +1,12 @@
|
|||||||
package pe
|
package pe
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"debug/pe"
|
"debug/pe"
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/dotnet/bundle"
|
"github.com/anchore/syft/syft/pkg/cataloger/internal/dotnet/bundle"
|
||||||
)
|
)
|
||||||
|
|
||||||
// dotNetBundleSignature is the SHA-256 hash of ".net core bundle" used to identify single-file bundles.
|
|
||||||
var dotNetBundleSignature = []byte{
|
|
||||||
0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
|
|
||||||
0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
|
|
||||||
0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
|
|
||||||
0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtractDepsJSONFromBundle searches for an embedded deps.json file in a .NET single-file bundle.
|
// ExtractDepsJSONFromBundle searches for an embedded deps.json file in a .NET single-file bundle.
|
||||||
// When built with PublishSingleFile=true, .NET embeds the application and all dependencies into
|
// When built with PublishSingleFile=true, .NET embeds the application and all dependencies into
|
||||||
// the AppHost executable. The bundle marker (8-byte header offset + 32-byte signature) is placed
|
// the AppHost executable. The bundle marker (8-byte header offset + 32-byte signature) is placed
|
||||||
@ -51,41 +40,7 @@ var dotNetBundleSignature = []byte{
|
|||||||
// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/file_entry.h
|
// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/file_entry.h
|
||||||
// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/file_type.h
|
// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/file_type.h
|
||||||
func extractDepsJSONFromBundle(r io.ReadSeeker, sections []pe.SectionHeader32) (string, error) {
|
func extractDepsJSONFromBundle(r io.ReadSeeker, sections []pe.SectionHeader32) (string, error) {
|
||||||
headerOffset, err := findBundleHeaderOffset(r, sections)
|
return bundle.ExtractDepsJSON(r, calculatePEEndOffset(sections))
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if headerOffset == 0 {
|
|
||||||
return "", nil // not a .NET single-file bundle
|
|
||||||
}
|
|
||||||
|
|
||||||
return bundle.ReadDepsJSONFromBundleHeader(r, headerOffset)
|
|
||||||
}
|
|
||||||
|
|
||||||
// findBundleHeaderOffset locates the bundle marker within the PE structure and returns the header offset.
|
|
||||||
// Returns 0 if no bundle marker is found (not a single-file bundle).
|
|
||||||
func findBundleHeaderOffset(r io.ReadSeeker, sections []pe.SectionHeader32) (int64, error) {
|
|
||||||
peEndOffset := calculatePEEndOffset(sections)
|
|
||||||
|
|
||||||
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
peData := make([]byte, peEndOffset)
|
|
||||||
n, err := io.ReadFull(r, peData)
|
|
||||||
if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
peData = peData[:n]
|
|
||||||
|
|
||||||
idx := bytes.Index(peData, dotNetBundleSignature)
|
|
||||||
if idx == -1 || idx < 8 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// the header offset is stored in the 8 bytes immediately before the signature
|
|
||||||
headerOffset := int64(binary.LittleEndian.Uint64(peData[idx-8 : idx]))
|
|
||||||
return headerOffset, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculatePEEndOffset determines where the PE structure ends based on section headers,
|
// calculatePEEndOffset determines where the PE structure ends based on section headers,
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package pe
|
package pe
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"debug/pe"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -56,3 +58,37 @@ func Test_extractDepsJSONFromBundle_Versions(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readSizeRecorder records the largest single Read length requested of it. The worst end offset PE
|
||||||
|
// headers can describe is about 8.6GB, and `make([]byte, 8.6e9)` succeeds on any 64-bit host from fresh
|
||||||
|
// anonymous mmap without touching the pages, so asserting "no panic" would pass with or without the
|
||||||
|
// bound. The requested read size is what actually distinguishes them.
|
||||||
|
type readSizeRecorder struct {
|
||||||
|
*bytes.Reader
|
||||||
|
maxRead int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *readSizeRecorder) Read(p []byte) (int, error) {
|
||||||
|
if len(p) > r.maxRead {
|
||||||
|
r.maxRead = len(p)
|
||||||
|
}
|
||||||
|
return r.Reader.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractDepsJSONFromBundle_MalformedSectionSizesDoNotOverAllocate(t *testing.T) {
|
||||||
|
// PointerToRawData and SizeOfRawData are user-controlled and unrelated to the real file size
|
||||||
|
sections := []pe.SectionHeader32{{PointerToRawData: 0xFFFFFFFF, SizeOfRawData: 0xFFFFFFFF}}
|
||||||
|
|
||||||
|
// sanity: the headers really do describe an end offset far past the file, so the bound is what keeps
|
||||||
|
// the allocation small rather than the input being small
|
||||||
|
require.Greater(t, calculatePEEndOffset(sections), int64(8*1024*1024*1024))
|
||||||
|
|
||||||
|
const fileSize = 512 // a small "file" with no bundle signature
|
||||||
|
r := &readSizeRecorder{Reader: bytes.NewReader(make([]byte, fileSize))}
|
||||||
|
|
||||||
|
content, err := extractDepsJSONFromBundle(r, sections)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, content)
|
||||||
|
assert.LessOrEqual(t, r.maxRead, fileSize,
|
||||||
|
"the search must be bounded by the file, not by what the section headers claim")
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user