Overview
sol_get_return_data copies at most the caller's requested number of bytes and
returns the full available return-data length. Mithril instead returns
SyscallErrInvalidLength whenever a nonzero destination is shorter than the
available return data.
Minimal scenario:
available return data: 24 bytes
requested destination: 16 bytes
Expected: copy bytes [0..16], copy the program ID, return 24
Mithril: abort the VM invocation with SyscallErrInvalidLength
Observed result:
Agave official syscall test: success, 16-byte prefix copied, returns 24
Mithril 24-byte control: success, syscall returns 24
Mithril 16-byte read: VM error SyscallErrInvalidLength
The Go-only reproducer below constructs a verifier-valid V0 sbpf.Program,
executes it with Mithril's official interpreter and runtime syscall registry,
and compares a 16-byte read with a 24-byte control. Short-buffer reads are an
ordinary API use: a caller may retrieve a prefix or query data whose full size
exceeds its local buffer. This is a directly reachable syscall semantic error
that can turn a successful program invocation into failure.
Tested Version
Mithril: 6f31241ba6c48a37316ea23cc947bde395e0722f (v0.3.0)
Agave: f7db9dee441713162a70086cff23e84788f05386
Host: Linux x86-64
Go: 1.25.7
sBPF: V0
Root Cause
Mithril first computes the correct copy length:
length = min(len(returnData), requestedLength)
It translates a destination of exactly length bytes, but then compares that
translated destination length with the full, untruncated len(returnData) in
pkg/sealevel/syscalls_call.go, lines 39-58.
When
0 < requestedLength < len(returnData), the lengths necessarily differ and
Mithril returns SyscallErrInvalidLength; the prefix copy on line 60 is never
reached.
Agave compares and copies return_data[..length] and returns the original full
length
(syscalls/src/lib.rs, lines 1967-1997).
Sig implements the same min/copy/full-length behavior
(shared/vm/syscalls/lib.zig, lines 540-570),
as does Firedancer
(fd_vm_syscall_runtime.c, lines 450-486).
The syscall is unconditionally registered in Mithril
(pkg/sealevel/syscalls.go, lines 118-121),
and the normal upgradeable-program path reaches the official loader, verifier,
registry, and interpreter. No custom registry, invalid pointer, malformed ELF,
or feature mismatch is needed.
Suggested Fix
Compare and copy only the selected prefix, while returning the full available
length. In outline:
length := min(uint64(len(returnData)), requestedLength)
dst, err := translateBytes(..., length, ...)
if err != nil {
return 0, err
}
copy(dst, returnData[:length])
return uint64(len(returnData)), nil
Preserve the program-ID copy and the existing overlap checks.
Add regressions for zero-length, shorter, equal-length, and longer destination
buffers. For 24 bytes of return data and a 16-byte destination, assert the
16-byte prefix, program ID, and return value 24.
Reproduction
The reproducer is one Go file. It creates both sBPF programs from encoded Go
slots, verifies them through Program.Verify, and executes them through
NewInterpreter with Mithril's official runtime syscall registry. It requires
only Go 1.25.7 and a clean Mithril checkout at the tested revision.
package main
import (
"bytes"
"encoding/binary"
"fmt"
"strings"
"github.com/Overclock-Validator/mithril/pkg/cu"
"github.com/Overclock-Validator/mithril/pkg/features"
"github.com/Overclock-Validator/mithril/pkg/sbpf"
"github.com/Overclock-Validator/mithril/pkg/sbpf/sbpfver"
"github.com/Overclock-Validator/mithril/pkg/sealevel"
"github.com/gagliardetto/solana-go"
)
const (
meterLimit = uint64(1_000_000)
returnDataSize = 24
programIDOffset = 64
)
var returnData = []byte("abcdefghijklmnopqrstuvwx")
var returnProgramID = solana.PublicKey{1, 2, 3}
func slot(op, dst, src uint8, off int16, imm uint32) sbpf.Slot {
return sbpf.Slot(op) |
sbpf.Slot(dst)<<8 |
sbpf.Slot(src)<<12 |
sbpf.Slot(uint16(off))<<16 |
sbpf.Slot(imm)<<32
}
func slotsToBytes(slots []sbpf.Slot) []byte {
raw := make([]byte, len(slots)*sbpf.SlotSize)
for i, ins := range slots {
binary.LittleEndian.PutUint64(raw[i*sbpf.SlotSize:], uint64(ins))
}
return raw
}
func makeProgram(requestedLength uint32) *sbpf.Program {
hash := sbpf.SymbolHash("sol_get_return_data")
slots := []sbpf.Slot{
slot(sbpf.OpMov64Imm, 2, 0, 0, requestedLength),
slot(sbpf.OpMov64Reg, 3, 1, 0, 0),
slot(sbpf.OpAdd64Imm, 3, 0, 0, programIDOffset),
slot(sbpf.OpCall, 0, 0, 0, hash),
slot(sbpf.OpExit, 0, 0, 0, 0),
}
raw := slotsToBytes(slots)
return &sbpf.Program{
RO: raw,
TextBytes: raw,
Text: slots,
TextVA: sbpf.VaddrProgram,
Entrypoint: 0,
Funcs: map[uint32]int64{},
SbpfVersion: sbpfver.SbpfVersion{Version: sbpfver.SbpfVersionV0},
}
}
func run(caseName string, requestedLength uint32) (
input []byte, r0 uint64, count uint64, runErr error,
) {
program := makeProgram(requestedLength)
if err := program.Verify(); err != nil {
panic(fmt.Sprintf("%s verification failed: %v", caseName, err))
}
featureSet := features.NewFeaturesDefault()
syscalls := sbpf.SyscallRegistry(func(hash uint32) (sbpf.Syscall, bool) {
return sealevel.Syscalls(featureSet, false, hash)
})
txCtx := sealevel.NewTransactionCtx(sealevel.TransactionAccounts{}, 1, 1)
txCtx.SetReturnData(returnProgramID, returnData)
meter := cu.NewComputeMeter(meterLimit)
execCtx := &sealevel.ExecutionCtx{
TransactionContext: txCtx,
ComputeMeter: meter,
}
input = make([]byte, 128)
vm := sbpf.NewInterpreter(program, &sbpf.VMOpts{
HeapMax: 32 * 1024,
Syscalls: syscalls,
MaxCU: int(meterLimit),
ComputeMeter: &execCtx.ComputeMeter,
Context: execCtx,
Input: input,
InputDataVaddr: sbpf.VaddrInput,
})
defer vm.Finish()
r0, count, runErr = vm.Run()
errorLabel := "<nil>"
if runErr != nil {
errorLabel = runErr.Error()
if strings.Contains(errorLabel, "SyscallErrInvalidLength") {
errorLabel = "SyscallErrInvalidLength"
}
}
fmt.Printf(
"case=%s verify=accepted r0=%d count=%d err=%s data=%q program_id=%x\n",
caseName, r0, count, errorLabel,
input[:requestedLength], input[programIDOffset:programIDOffset+3],
)
return
}
func main() {
if len(returnData) != returnDataSize {
panic("locked return-data length changed")
}
input, r0, count, err := run("equal_length_control", 24)
if err != nil || r0 != 24 || count != 105 {
panic("equal-length control did not succeed")
}
if !bytes.Equal(input[:24], returnData) {
panic("equal-length control copied incorrect return data")
}
if !bytes.Equal(input[programIDOffset:programIDOffset+3], returnProgramID[:3]) {
panic("equal-length control copied incorrect program ID")
}
input, r0, count, err = run("truncating_read", 16)
if err == nil || !strings.Contains(err.Error(), "SyscallErrInvalidLength") {
panic("truncating read did not return SyscallErrInvalidLength")
}
if count != 0 {
panic(fmt.Sprintf("unexpected candidate instruction count: %d", count))
}
if !bytes.Equal(input[:16], make([]byte, 16)) {
panic("candidate unexpectedly reached the prefix copy")
}
}
Expected output:
case=equal_length_control verify=accepted r0=24 count=105 err=<nil> data="abcdefghijklmnopqrstuvwx" program_id=010203
case=truncating_read verify=accepted r0=0 count=0 err=SyscallErrInvalidLength data="\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" program_id=000000
count is the cuConsumed value returned by Interpreter.Run, not a raw
instruction count. The successful case includes the syscall's 100-CU charge;
the current error return reports zero together with the syscall error.
The first case proves that the verifier, interpreter, syscall registry,
transaction context, memory translation, and output buffers are configured
correctly. The only program difference in the second case is the requested
length. The reference implementations cited above establish that this shorter
destination must receive the 16-byte prefix and the syscall must return 24.
Overview
sol_get_return_datacopies at most the caller's requested number of bytes andreturns the full available return-data length. Mithril instead returns
SyscallErrInvalidLengthwhenever a nonzero destination is shorter than theavailable return data.
Minimal scenario:
Observed result:
The Go-only reproducer below constructs a verifier-valid V0
sbpf.Program,executes it with Mithril's official interpreter and runtime syscall registry,
and compares a 16-byte read with a 24-byte control. Short-buffer reads are an
ordinary API use: a caller may retrieve a prefix or query data whose full size
exceeds its local buffer. This is a directly reachable syscall semantic error
that can turn a successful program invocation into failure.
Tested Version
Root Cause
Mithril first computes the correct copy length:
It translates a destination of exactly
lengthbytes, but then compares thattranslated destination length with the full, untruncated
len(returnData)inpkg/sealevel/syscalls_call.go, lines 39-58.When
0 < requestedLength < len(returnData), the lengths necessarily differ andMithril returns
SyscallErrInvalidLength; the prefix copy on line 60 is neverreached.
Agave compares and copies
return_data[..length]and returns the original fulllength
(
syscalls/src/lib.rs, lines 1967-1997).Sig implements the same min/copy/full-length behavior
(
shared/vm/syscalls/lib.zig, lines 540-570),as does Firedancer
(
fd_vm_syscall_runtime.c, lines 450-486).The syscall is unconditionally registered in Mithril
(
pkg/sealevel/syscalls.go, lines 118-121),and the normal upgradeable-program path reaches the official loader, verifier,
registry, and interpreter. No custom registry, invalid pointer, malformed ELF,
or feature mismatch is needed.
Suggested Fix
Compare and copy only the selected prefix, while returning the full available
length. In outline:
Preserve the program-ID copy and the existing overlap checks.
Add regressions for zero-length, shorter, equal-length, and longer destination
buffers. For 24 bytes of return data and a 16-byte destination, assert the
16-byte prefix, program ID, and return value 24.
Reproduction
The reproducer is one Go file. It creates both sBPF programs from encoded Go
slots, verifies them through
Program.Verify, and executes them throughNewInterpreterwith Mithril's official runtime syscall registry. It requiresonly Go 1.25.7 and a clean Mithril checkout at the tested revision.
Expected output:
countis thecuConsumedvalue returned byInterpreter.Run, not a rawinstruction count. The successful case includes the syscall's 100-CU charge;
the current error return reports zero together with the syscall error.
The first case proves that the verifier, interpreter, syscall registry,
transaction context, memory translation, and output buffers are configured
correctly. The only program difference in the second case is the requested
length. The reference implementations cited above establish that this shorter
destination must receive the 16-byte prefix and the syscall must return 24.