Commit da681773 authored by mbaxter's avatar mbaxter Committed by GitHub

cannon: Update program loading for 64-bit programs (#12657)

* cannon: Add some unit tests for LoadELF

* cannon: Fix off-by-one boundary check

* cannon: Adapt LoadELF addr check for 64-bit

* cannon: Handle zero-length segments

* cannon: Restrict virtual address space to 48-bits for MIPS64
parent 005116d9
...@@ -25,7 +25,7 @@ func LoadELF[T mipsevm.FPVMState](f *elf.File, initState CreateInitialFPVMState[ ...@@ -25,7 +25,7 @@ func LoadELF[T mipsevm.FPVMState](f *elf.File, initState CreateInitialFPVMState[
s := initState(Word(f.Entry), HEAP_START) s := initState(Word(f.Entry), HEAP_START)
for i, prog := range f.Progs { for i, prog := range f.Progs {
if prog.Type == 0x70000003 { // MIPS_ABIFLAGS if prog.Type == elf.PT_MIPS_ABIFLAGS {
continue continue
} }
...@@ -42,12 +42,27 @@ func LoadELF[T mipsevm.FPVMState](f *elf.File, initState CreateInitialFPVMState[ ...@@ -42,12 +42,27 @@ func LoadELF[T mipsevm.FPVMState](f *elf.File, initState CreateInitialFPVMState[
} }
} }
// TODO(#12205) if prog.Memsz == 0 {
if prog.Vaddr+prog.Memsz >= uint64(1<<32) { // Nothing to do
return empty, fmt.Errorf("program %d out of 32-bit mem range: %x - %x (size: %x)", i, prog.Vaddr, prog.Vaddr+prog.Memsz, prog.Memsz) continue
}
// Calculate the architecture-specific last valid memory address
var lastMemoryAddr uint64
if arch.IsMips32 {
// 32-bit virtual address space
lastMemoryAddr = (1 << 32) - 1
} else {
// 48-bit virtual address space
lastMemoryAddr = (1 << 48) - 1
}
lastByteToWrite := prog.Vaddr + prog.Memsz - 1
if lastByteToWrite > lastMemoryAddr || lastByteToWrite < prog.Vaddr {
return empty, fmt.Errorf("program %d out of memory range: %x - %x (size: %x)", i, prog.Vaddr, lastByteToWrite, prog.Memsz)
} }
if prog.Vaddr+prog.Memsz >= HEAP_START { if lastByteToWrite >= HEAP_START {
return empty, fmt.Errorf("program %d overlaps with heap: %x - %x (size: %x). The heap start offset must be reconfigured", i, prog.Vaddr, prog.Vaddr+prog.Memsz, prog.Memsz) return empty, fmt.Errorf("program %d overlaps with heap: %x - %x (size: %x). The heap start offset must be reconfigured", i, prog.Vaddr, lastByteToWrite, prog.Memsz)
} }
if err := s.GetMemory().SetMemoryRange(Word(prog.Vaddr), r); err != nil { if err := s.GetMemory().SetMemoryRange(Word(prog.Vaddr), r); err != nil {
return empty, fmt.Errorf("failed to read program segment %d: %w", i, err) return empty, fmt.Errorf("failed to read program segment %d: %w", i, err)
......
package program
import (
"debug/elf"
"io"
"testing"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/arch"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/program/testutil"
)
func TestLoadELF(t *testing.T) {
data := []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}
dataSize := uint64(len(data))
lastValidAddr := uint64(HEAP_START - 1)
lastAddr := uint64(^uint32(0))
if !arch.IsMips32 {
lastAddr = (1 << 48) - 1
}
tests := []struct {
name string
progType elf.ProgType
memSize uint64
fileSize uint64
vAddr uint64
expectedErr string
shouldIgnore bool
}{
{name: "Zero length segment", progType: elf.PT_LOAD, fileSize: 0, memSize: 0, vAddr: 0},
{name: "Zero length segment, non-zero fileSize", progType: elf.PT_LOAD, fileSize: 2, memSize: 0, vAddr: 0, expectedErr: "file size (2) > mem size (0)"},
{name: "Loadable segment, fileSize > memSize", progType: elf.PT_LOAD, fileSize: dataSize * 2, memSize: dataSize, vAddr: 0x4000, expectedErr: "file size (16) > mem size (8)"},
{name: "Loadable segment, fileSize < memSize", progType: elf.PT_LOAD, fileSize: dataSize, memSize: dataSize * 2, vAddr: 0x4000},
{name: "Loadable segment, fileSize == memSize", progType: elf.PT_LOAD, fileSize: dataSize, memSize: dataSize, vAddr: 0x4000},
{name: "Loadable segment, segment out-of-range", progType: elf.PT_LOAD, fileSize: dataSize, memSize: dataSize, vAddr: lastAddr - 1, expectedErr: "out of memory range"},
{name: "Loadable segment, segment just out-of-range", progType: elf.PT_LOAD, fileSize: dataSize, memSize: dataSize, vAddr: lastAddr - dataSize + 2, expectedErr: "out of memory range"},
{name: "Loadable segment, segment just in-range", progType: elf.PT_LOAD, fileSize: dataSize, memSize: dataSize, vAddr: lastAddr - dataSize + 1, expectedErr: "overlaps with heap"},
{name: "Loadable segment, segment overlaps heap", progType: elf.PT_LOAD, fileSize: dataSize, memSize: dataSize, vAddr: lastValidAddr - 1, expectedErr: "overlaps with heap"},
{name: "Loadable segment, segment just overlaps heap", progType: elf.PT_LOAD, fileSize: dataSize, memSize: dataSize, vAddr: lastValidAddr - dataSize + 2, expectedErr: "overlaps with heap"},
{name: "Loadable segment, segment ends just before heap", progType: elf.PT_LOAD, fileSize: dataSize, memSize: dataSize, vAddr: lastValidAddr - dataSize + 1},
{name: "MIPS Flags segment, invalid file size", progType: elf.PT_MIPS_ABIFLAGS, fileSize: dataSize * 2, memSize: dataSize, vAddr: 0x4000, shouldIgnore: true},
{name: "MIPS Flags segment, out-of-range", progType: elf.PT_MIPS_ABIFLAGS, fileSize: dataSize, memSize: dataSize, vAddr: lastAddr, shouldIgnore: true},
{name: "MIPS Flags segment, overlaps heap", progType: elf.PT_MIPS_ABIFLAGS, fileSize: dataSize, memSize: dataSize, vAddr: lastValidAddr, shouldIgnore: true},
{name: "Other segment, fileSize > memSize", progType: elf.PT_DYNAMIC, fileSize: dataSize * 2, memSize: dataSize, vAddr: 0x4000, expectedErr: "filling for non PT_LOAD segments is not supported"},
{name: "Other segment, memSize > fileSize", progType: elf.PT_DYNAMIC, fileSize: dataSize, memSize: dataSize * 2, vAddr: 0x4000, expectedErr: "filling for non PT_LOAD segments is not supported"},
{name: "Other segment, out-of-range", progType: elf.PT_DYNAMIC, fileSize: dataSize, memSize: dataSize, vAddr: lastAddr, expectedErr: "out of memory range"},
{name: "Other segment, overlaps heap", progType: elf.PT_DYNAMIC, fileSize: dataSize, memSize: dataSize, vAddr: lastValidAddr, expectedErr: "overlaps with heap"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
prog, reader := testutil.MockProgWithReader(tt.progType, tt.fileSize, tt.memSize, tt.vAddr, data)
progs := []*elf.Prog{prog}
mockFile := testutil.MockELFFile(progs)
state, err := LoadELF(mockFile, testutil.MockCreateInitState)
if tt.expectedErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tt.expectedErr)
} else {
require.NoError(t, err)
if tt.shouldIgnore {
// No data should be read
require.Equal(t, reader.BytesRead, 0)
} else {
// Set up memory validation data
expectedData := make([]byte, tt.memSize)
copy(expectedData, data[:])
memReader := state.GetMemory().ReadMemoryRange(arch.Word(tt.vAddr), arch.Word(tt.memSize))
actualData, err := io.ReadAll(memReader)
require.NoError(t, err)
// Validate data was read into memory
require.Equal(t, reader.BytesRead, int(tt.fileSize))
require.Equal(t, actualData, expectedData)
}
}
})
}
}
package testutil
import (
"debug/elf"
"io"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/cannon/mipsevm"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/arch"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/memory"
)
// MockELFFile create a mock ELF file with custom program segments
func MockELFFile(progs []*elf.Prog) *elf.File {
return &elf.File{Progs: progs}
}
// MockProg sets up a elf.Prog structure for testing
func MockProg(progType elf.ProgType, filesz, memsz, vaddr uint64) *elf.Prog {
return &elf.Prog{
ProgHeader: elf.ProgHeader{
Type: progType,
Filesz: filesz,
Memsz: memsz,
Vaddr: vaddr,
},
}
}
// MockProgWithReader creates an elf.Prog with a TrackableReaderAt to track reads
func MockProgWithReader(progType elf.ProgType, filesz, memsz, vaddr uint64, data []byte) (*elf.Prog, *TrackableReaderAt) {
reader := &TrackableReaderAt{data: data}
prog := MockProg(progType, filesz, memsz, vaddr)
prog.ReaderAt = io.NewSectionReader(reader, 0, int64(filesz))
return prog, reader
}
// TrackableReaderAt tracks the number of bytes read
type TrackableReaderAt struct {
data []byte
BytesRead int
}
func (r *TrackableReaderAt) ReadAt(p []byte, offset int64) (int, error) {
if offset >= int64(len(r.data)) {
return 0, io.EOF
}
numBytesRead := copy(p, r.data[offset:])
r.BytesRead += numBytesRead
if numBytesRead < len(p) {
return numBytesRead, io.EOF
}
return numBytesRead, nil
}
// MockCreateInitState returns a mock FPVMState for testing
func MockCreateInitState(pc, heapStart arch.Word) *MockFPVMState {
return newMockFPVMState()
}
type MockFPVMState struct {
memory *memory.Memory
}
var _ mipsevm.FPVMState = (*MockFPVMState)(nil)
func newMockFPVMState() *MockFPVMState {
mem := memory.NewMemory()
state := MockFPVMState{mem}
return &state
}
func (m MockFPVMState) Serialize(out io.Writer) error {
panic("not implemented")
}
func (m MockFPVMState) GetMemory() *memory.Memory {
return m.memory
}
func (m MockFPVMState) GetHeap() arch.Word {
panic("not implemented")
}
func (m MockFPVMState) GetPreimageKey() common.Hash {
panic("not implemented")
}
func (m MockFPVMState) GetPreimageOffset() arch.Word {
panic("not implemented")
}
func (m MockFPVMState) GetPC() arch.Word {
panic("not implemented")
}
func (m MockFPVMState) GetCpu() mipsevm.CpuScalars {
panic("not implemented")
}
func (m MockFPVMState) GetRegistersRef() *[32]arch.Word {
panic("not implemented")
}
func (m MockFPVMState) GetStep() uint64 {
panic("not implemented")
}
func (m MockFPVMState) GetExited() bool {
panic("not implemented")
}
func (m MockFPVMState) GetExitCode() uint8 {
panic("not implemented")
}
func (m MockFPVMState) GetLastHint() hexutil.Bytes {
panic("not implemented")
}
func (m MockFPVMState) EncodeWitness() (witness []byte, hash common.Hash) {
panic("not implemented")
}
func (m MockFPVMState) CreateVM(logger log.Logger, po mipsevm.PreimageOracle, stdOut, stdErr io.Writer, meta mipsevm.Metadata) mipsevm.FPVM {
panic("not implemented")
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment