Repository: go
Issue: debug/elf: NewFile can be tricked into reading the entire file into memory if it is corrupted
Labels: NeedsDecision,compiler/runtime

### Go version

go version go1.22.2 linux/arm64

### Output of `go env` in your module/workspace:

```shell
GO111MODULE=''
GOARCH='arm64'
GOBIN=''
GOCACHE='/home/brennan/.cache/go-build'
GOENV='/home/brennan/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='arm64'
GOHOSTOS='linux'
GOINSECURE=''
GOMODCACHE='/home/brennan/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/brennan/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/gnu/store/vcp6w6z0syjwv383s0nywhy0svxk2d05-go-1.22.2/lib/go'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/gnu/store/vcp6w6z0syjwv383s0nywhy0svxk2d05-go-1.22.2/lib/go/pkg/tool/linux_arm64'
GOVCS=''
GOVERSION='go1.22.2'
GCCGO='gccgo'
AR='ar'
CC='gcc'
CXX='g++'
CGO_ENABLED='1'
GOMOD='/dev/null'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build369678066=/tmp/go-build -gno-record-gcc-switches
```


### What did you do?

Take a 64-bit little-endian ELF file at least 1GiB in size, and run the following script to corrupt it (by setting the shstrndx to point to a section of size 1GiB): 

```
package main

import (
	"encoding/binary"
	"fmt"
	"os"
)

func main() {
	if len(os.Args) < 2 {
		fmt.Println("Usage: go run main.go <filename>")
		return
	}

	filename := os.Args[1]
	file, err := os.OpenFile(filename, os.O_RDWR, 0)
	if err != nil {
		fmt.Println("Error opening file:", err)
		return
	}
	defer file.Close()

	// Write "1" to shstrndx
	_, err = file.WriteAt([]byte { 1, 0, 0, 0}, 62)
	if err != nil {
		fmt.Println("Error writing to file:", err)
		return
	}

	// Get offset of section 1
	var data [8]byte
	_, err = file.ReadAt(data[:], 40)
	if err != nil {
		fmt.Println("Error reading from file:", err)
		return
	}
	offset := int64(binary.LittleEndian.Uint64(data[:])) + 64

	// Change sec1 type to 3 (SHT_STRTAB), offset to 0, size to 1GiB
	file.WriteAt([]byte { 3, 0 }, offset + 4)
	file.WriteAt([]byte {0, 0, 0, 0, 0, 0, 0, 0}, offset + 24)
	file.WriteAt([]byte {0, 0, 0, 0x40, 0, 0, 0, 0}, offset + 32)
}
```

Then try to get the buildinfo, e.g. using https://github.com/kelseyhightower/buildinfo . This uses elf.NewFile under the hood.

### What did you see happen?

The program consumes 1GiB of memory before failing.

### What did you expect to see?

Memory use should be bounded.

The reason is that `debug/elf` reads the entire shstrtab into memory in order to get section names. It uses `saferio.ReadData` to do this.

`ReadData`'s doc comment reads as follows:

```
// ReadData reads n bytes from the input stream, but avoids allocating
// all n bytes if n is large. This avoids crashing the program by
// allocating all n bytes in cases where n is incorrect.
```

However, this is misleading: it does not actually prevent allocating more than `n` bytes (which is 10MB in this case). It just prevents it from allocating that many _at a time_. It will happily allocate chunks in a loop until it has read up to `n` or the total file size.