mirror of
https://github.com/sliverarmory/reflektor
synced 2026-08-25 09:14:48 +00:00
c7ee9636e9
Enable BOF and ELF shared-library loading on FreeBSD, including recursive dependency handling and exact symbol-version resolution. Add strict no-skip FreeBSD 15.1 QEMU coverage, a 25-object semantic BOF corpus per architecture, platform documentation, and the verified Sliver compile exception.
317 lines
11 KiB
Go
317 lines
11 KiB
Go
//go:build (linux && !android && (386 || amd64 || (arm && arm.7) || arm64 || ppc64le || riscv64)) || (freebsd && (amd64 || arm64))
|
|
|
|
package linuxmem
|
|
|
|
import (
|
|
"debug/elf"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"unsafe"
|
|
|
|
"github.com/ebitengine/purego"
|
|
"github.com/sliverarmory/reflektor/native/internal/rejection"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
func TestLoadLibraryDefensivelyRejectsGoImage(t *testing.T) {
|
|
executable, err := os.Executable()
|
|
if err != nil {
|
|
t.Fatalf("resolve test executable: %v", err)
|
|
}
|
|
image, err := os.ReadFile(executable)
|
|
if err != nil {
|
|
t.Fatalf("read test executable: %v", err)
|
|
}
|
|
|
|
if _, err := LoadLibrary(image); !errors.Is(err, rejection.ErrGoSharedLibraryUnsupported) {
|
|
t.Fatalf("LoadLibrary(Go image) error = %v, want ErrGoSharedLibraryUnsupported", err)
|
|
}
|
|
}
|
|
|
|
func TestCollectELFFinalizersUsesABIOrder(t *testing.T) {
|
|
mapping, err := unix.Mmap(-1, 0, 4096, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_PRIVATE|unix.MAP_ANON)
|
|
if err != nil {
|
|
t.Fatalf("mmap test image: %v", err)
|
|
}
|
|
defer unix.Munmap(mapping)
|
|
base := uintptr(unsafe.Pointer(unsafe.SliceData(mapping)))
|
|
class := elf.ELFCLASS64
|
|
entrySize := 8
|
|
if unsafe.Sizeof(uintptr(0)) == 4 {
|
|
class = elf.ELFCLASS32
|
|
entrySize = 4
|
|
}
|
|
|
|
const arrayOffset = 8
|
|
arrayFunctions := []uintptr{base + 48, base + 64, base + 80}
|
|
for index, function := range arrayFunctions {
|
|
entry := base + arrayOffset + uintptr(index*entrySize)
|
|
if entrySize == 8 {
|
|
writeU64(entry, uint64(function))
|
|
} else {
|
|
writeU32(entry, uint32(function))
|
|
}
|
|
}
|
|
|
|
mapped := mappedELF{mapping: mapping, loadBias: base}
|
|
finalizers, err := collectELFFinalizers(mapped, class, dynamicInitInfo{
|
|
fini: 96,
|
|
finiArray: arrayOffset,
|
|
finiArraySz: uint64(len(arrayFunctions) * entrySize),
|
|
})
|
|
runtime.KeepAlive(mapping)
|
|
if err != nil {
|
|
t.Fatalf("collectELFFinalizers: %v", err)
|
|
}
|
|
want := []uintptr{base + 80, base + 64, base + 48, base + 96}
|
|
if len(finalizers) != len(want) {
|
|
t.Fatalf("finalizer count = %d, want %d", len(finalizers), len(want))
|
|
}
|
|
for index := range want {
|
|
if finalizers[index] != want[index] {
|
|
t.Fatalf("finalizers[%d] = %#x, want %#x", index, finalizers[index], want[index])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestModuleFreeRunsFinalizersOnce(t *testing.T) {
|
|
var order []string
|
|
first := purego.NewCallback(func() {
|
|
order = append(order, "fini-1")
|
|
})
|
|
second := purego.NewCallback(func() {
|
|
order = append(order, "fini-2")
|
|
})
|
|
module := &Module{
|
|
finalizers: []uintptr{first, second},
|
|
dynamicAPI: &linuxDynAPI{dlclose: 1},
|
|
ownedDlopen: []uintptr{10, 20},
|
|
closeDlopenHandle: func(_ *linuxDynAPI, handle uintptr) error {
|
|
order = append(order, fmt.Sprintf("close-%d", handle))
|
|
return nil
|
|
},
|
|
}
|
|
|
|
module.Free()
|
|
module.Free()
|
|
want := []string{"fini-1", "fini-2", "close-20", "close-10"}
|
|
if len(order) != len(want) {
|
|
t.Fatalf("lifecycle order = %v, want %v", order, want)
|
|
}
|
|
for index := range want {
|
|
if order[index] != want[index] {
|
|
t.Fatalf("lifecycle order = %v, want %v", order, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResolverOwnsNeededReferencesAndDeduplicatesAliases(t *testing.T) {
|
|
var openCalls []string
|
|
var closeCalls []uintptr
|
|
nextHandle := uintptr(100)
|
|
resolver := &symbolResolver{
|
|
api: &linuxDynAPI{dlopen: 1, dlclose: 1},
|
|
modules: []runtimeELFModule{{path: "/lib/libvisible.so"}},
|
|
resolved: make(map[string]uintptr),
|
|
misses: make(map[string]error),
|
|
opened: make(map[string]uintptr),
|
|
openLibrary: func(_ *linuxDynAPI, name string) (uintptr, error) {
|
|
openCalls = append(openCalls, name)
|
|
handle := nextHandle
|
|
nextHandle++
|
|
return handle, nil
|
|
},
|
|
closeLibrary: func(_ *linuxDynAPI, handle uintptr) error {
|
|
closeCalls = append(closeCalls, handle)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
if err := resolver.ensureLibraryLoaded("libvisible.so", false); err != nil {
|
|
t.Fatalf("optional visible dependency: %v", err)
|
|
}
|
|
if len(openCalls) != 0 {
|
|
t.Fatalf("optional visible dependency acquired a handle: %v", openCalls)
|
|
}
|
|
if err := resolver.ensureLibraryLoaded("libvisible.so", true); err != nil {
|
|
t.Fatalf("owned visible dependency: %v", err)
|
|
}
|
|
if err := resolver.ensureLibraryLoaded("/alternate/libvisible.so", true); err != nil {
|
|
t.Fatalf("owned alias: %v", err)
|
|
}
|
|
if err := resolver.ensureLibraryLoaded("libsecond.so", true); err != nil {
|
|
t.Fatalf("second owned dependency: %v", err)
|
|
}
|
|
if len(openCalls) != 2 {
|
|
t.Fatalf("dlopen calls = %v, want two distinct acquisitions", openCalls)
|
|
}
|
|
|
|
resolver.closeOwnedLibraries()
|
|
wantClose := []uintptr{101, 100}
|
|
if len(closeCalls) != len(wantClose) || closeCalls[0] != wantClose[0] || closeCalls[1] != wantClose[1] {
|
|
t.Fatalf("dlclose calls = %v, want %v", closeCalls, wantClose)
|
|
}
|
|
}
|
|
|
|
func TestResolverNeededFailureRetainsCleanupOwnership(t *testing.T) {
|
|
var closeCalls []uintptr
|
|
resolver := &symbolResolver{
|
|
api: &linuxDynAPI{dlopen: 1, dlclose: 1},
|
|
resolved: make(map[string]uintptr),
|
|
misses: make(map[string]error),
|
|
opened: make(map[string]uintptr),
|
|
openLibrary: func(_ *linuxDynAPI, name string) (uintptr, error) {
|
|
if name == "libavailable.so" {
|
|
return 77, nil
|
|
}
|
|
return 0, errors.New("not found")
|
|
},
|
|
closeLibrary: func(_ *linuxDynAPI, handle uintptr) error {
|
|
closeCalls = append(closeCalls, handle)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
err := resolver.primeNeededLibraries([]string{"libavailable.so", "libmissing.so"})
|
|
if err == nil || !strings.Contains(err.Error(), `load DT_NEEDED "libmissing.so"`) {
|
|
t.Fatalf("primeNeededLibraries error = %v, want missing DT_NEEDED", err)
|
|
}
|
|
resolver.closeOwnedLibraries()
|
|
if len(closeCalls) != 1 || closeCalls[0] != 77 {
|
|
t.Fatalf("failure cleanup dlclose calls = %v, want [77]", closeCalls)
|
|
}
|
|
}
|
|
|
|
func TestNewLinuxArchitectureRelocations(t *testing.T) {
|
|
t.Run("arm", func(t *testing.T) {
|
|
var word uint32
|
|
place := uintptr(unsafe.Pointer(&word))
|
|
if err := applyARMReloc(uint32(elf.R_ARM_RELATIVE), place, 0x1000, 0, 0x234, 0, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if word != 0x1234 {
|
|
t.Fatalf("R_ARM_RELATIVE = %#x, want %#x", word, uint32(0x1234))
|
|
}
|
|
if err := applyARMReloc(uint32(elf.R_ARM_GLOB_DAT), place, 0, 0x5678, 0x99, 0, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if word != 0x5678 {
|
|
t.Fatalf("R_ARM_GLOB_DAT = %#x, want %#x", word, uint32(0x5678))
|
|
}
|
|
if err := applyARMReloc(uint32(elf.R_ARM_TLS_TPOFF32), place, 0, 0, 0, 0, false); err == nil {
|
|
t.Fatal("R_ARM_TLS_TPOFF32 without a TLS slot succeeded")
|
|
}
|
|
})
|
|
|
|
t.Run("riscv64", func(t *testing.T) {
|
|
var word uint64
|
|
place := uintptr(unsafe.Pointer(&word))
|
|
if err := applyRISCV64Reloc(uint32(elf.R_RISCV_RELATIVE), place, 0x1000, 0, 0x234, 0, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if word != 0x1234 {
|
|
t.Fatalf("R_RISCV_RELATIVE = %#x, want %#x", word, uint64(0x1234))
|
|
}
|
|
if err := applyRISCV64Reloc(uint32(elf.R_RISCV_64), place, 0, 0x5000, 0x678, 0, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if word != 0x5678 {
|
|
t.Fatalf("R_RISCV_64 = %#x, want %#x", word, uint64(0x5678))
|
|
}
|
|
if err := applyRISCV64Reloc(uint32(elf.R_RISCV_TLS_TPREL64), place, 0, 0, 0, 0, false); err == nil {
|
|
t.Fatal("R_RISCV_TLS_TPREL64 without a TLS slot succeeded")
|
|
}
|
|
})
|
|
|
|
t.Run("ppc64le", func(t *testing.T) {
|
|
var word uint64
|
|
place := uintptr(unsafe.Pointer(&word))
|
|
if err := applyPPC64LEReloc(uint32(elf.R_PPC64_RELATIVE), place, 0x1000, 0, 0x234, 0, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if word != 0x1234 {
|
|
t.Fatalf("R_PPC64_RELATIVE = %#x, want %#x", word, uint64(0x1234))
|
|
}
|
|
if err := applyPPC64LEReloc(uint32(elf.R_PPC64_ADDR64), place, 0, 0x5000, 0x678, 0, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if word != 0x5678 {
|
|
t.Fatalf("R_PPC64_ADDR64 = %#x, want %#x", word, uint64(0x5678))
|
|
}
|
|
if err := applyPPC64LEReloc(uint32(elf.R_PPC64_TPREL64), place, 0, 0, 0, 0, false); err == nil {
|
|
t.Fatal("R_PPC64_TPREL64 without a TLS slot succeeded")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestNewLinuxArchitectureABIFlags(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
machine elf.Machine
|
|
flags uint32
|
|
wantErr bool
|
|
}{
|
|
{name: "arm EABI5 hard-float", machine: elf.EM_ARM, flags: armELFEABI5 | armELFFloatHard},
|
|
{name: "arm EABI5 soft-float", machine: elf.EM_ARM, flags: armELFEABI5 | armELFFloatSoft, wantErr: true},
|
|
{name: "riscv64 LP64D", machine: elf.EM_RISCV, flags: riscvELFFloatABIDouble},
|
|
{name: "riscv64 LP64D RVC", machine: elf.EM_RISCV, flags: riscvELFFloatABIDouble | riscvELFRVC},
|
|
{name: "riscv64 soft-float", machine: elf.EM_RISCV, flags: riscvELFRVC, wantErr: true},
|
|
{name: "riscv64 RVE", machine: elf.EM_RISCV, flags: riscvELFFloatABIDouble | riscvELFRVE, wantErr: true},
|
|
{name: "riscv64 TSO", machine: elf.EM_RISCV, flags: riscvELFFloatABIDouble | riscvELFTSO, wantErr: true},
|
|
{name: "riscv64 unknown", machine: elf.EM_RISCV, flags: riscvELFFloatABIDouble | 0x20, wantErr: true},
|
|
{name: "ppc64le ELFv2", machine: elf.EM_PPC64, flags: ppc64ELFABI2},
|
|
{name: "ppc64 ELFv1", machine: elf.EM_PPC64, flags: 1, wantErr: true},
|
|
{name: "ppc64 unspecified ABI", machine: elf.EM_PPC64, flags: 0, wantErr: true},
|
|
{name: "ppc64 unknown", machine: elf.EM_PPC64, flags: ppc64ELFABI2 | 0x4, wantErr: true},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
err := validateELFArchitectureFlags(test.machine, test.flags)
|
|
if (err != nil) != test.wantErr {
|
|
t.Fatalf("validateELFArchitectureFlags(%s, %#x) error = %v, wantErr=%v", test.machine, test.flags, err, test.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplySegmentProtectionsRejectsWritableExecutableSegment(t *testing.T) {
|
|
mapping, err := unix.Mmap(-1, 0, unix.Getpagesize(), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_PRIVATE|unix.MAP_ANON)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer unix.Munmap(mapping)
|
|
|
|
mapped := mappedELF{
|
|
mapping: mapping,
|
|
loadBias: uintptr(unsafe.Pointer(unsafe.SliceData(mapping))),
|
|
progs: []*elf.Prog{{ProgHeader: elf.ProgHeader{
|
|
Type: elf.PT_LOAD,
|
|
Flags: elf.PF_R | elf.PF_W | elf.PF_X,
|
|
Memsz: uint64(len(mapping)),
|
|
}}},
|
|
}
|
|
if err := applySegmentProtections(mapped); err == nil || !strings.Contains(err.Error(), "writable and executable") {
|
|
t.Fatalf("applySegmentProtections error = %v, want W+X rejection", err)
|
|
}
|
|
}
|
|
|
|
func TestMatchSymbolOffsetSkipsGNUIFUNCResolvers(t *testing.T) {
|
|
ifuncInfo := byte(elf.STB_GLOBAL)<<4 | byte(elf.STT_GNU_IFUNC)
|
|
funcInfo := byte(elf.STB_GLOBAL)<<4 | byte(elf.STT_FUNC)
|
|
symbols := []elf.Symbol{
|
|
{Name: "memcpy", Info: ifuncInfo, Value: 0x1000},
|
|
{Name: "memcpy", Info: funcInfo, Value: 0x2000},
|
|
}
|
|
|
|
if got, ok := matchSymbolOffset(symbols, "memcpy"); !ok || got != 0x2000 {
|
|
t.Fatalf("matchSymbolOffset(memcpy) = (%#x, %v), want (%#x, true)", got, ok, uintptr(0x2000))
|
|
}
|
|
if got, ok := matchSymbolOffset(symbols[:1], "memcpy"); ok || got != 0 {
|
|
t.Fatalf("matchSymbolOffset(IFUNC-only memcpy) = (%#x, %v), want (0, false)", got, ok)
|
|
}
|
|
}
|