feat: add FreeBSD amd64 and arm64 support
Reflektor
Reflektor is a Go library and CLI for loading shared libraries from bytes and
invoking exported functions. Its bof subpackage loads and executes native
Beacon Object Files.
It exposes a stable root package (reflektor) so other projects can import it directly, while platform-specific loading is handled behind memmod.
Platform Support
The table below describes the root reflektor shared-library loader, the
native package, and recursive shared-library loading. BOF support is tracked
separately because it can cover a host without implying shared-library parity.
| OS | Architectures | Shared Library Format | Status | Loader Notes |
|---|---|---|---|---|
| Windows | 386, amd64, arm64 |
PE (.dll) |
Supported | In-memory PE loader |
| Darwin | amd64, arm64 |
Mach-O (.dylib, bundle) |
Supported | Dyld4 root-image loader with system dependencies registered through public dyld; supports no-cgo builds and avoids temp-file legacy NS APIs. |
| FreeBSD | amd64, arm64 |
ELF (.so) |
Supported | Pure Go in-memory ELF loader with FreeBSD rtld symbol resolution and real FreeBSD 15.1 QEMU runtime proof. C and Rust loading support both CGO modes; Go c-shared is supported on amd64 and unavailable in the pinned Go toolchain on arm64. |
| Linux | 386, amd64, arm64, ppc64le, riscv64; ARMv7 hard-float (GOARM=7) |
ELF (.so) |
Supported | Pure Go in-memory ELF loader (maps PT_LOAD segments, applies relocations, resolves externals from runtime modules/dlsym); no memfd, no /dev/shm, no temp-file disk writes. ARMv7, ppc64le, and riscv64 receive QEMU runtime proof. |
| Other | - | - | Unsupported | Returns an explicit unsupported-platform error. |
The machine-readable platform capability manifest
lists every non-Wasm go tool dist list target and records BOF format,
endianness, CGO requirements, shared-library surfaces, Go c-shared
availability, architecture variants where required, and the runtime proof for
each target. runtime means CI executes
the surface, runtime-emulated means CI executes it through emulation,
unsupported means Reflektor has not implemented that surface, and n-a
means the pinned Go toolchain does not provide the applicable build mode.
Public API
Import path:
import "github.com/sliverarmory/reflektor"
Example:
payload := []byte{}
lib, err := reflektor.LoadLibrary(payload)
if err != nil {
return err
}
defer lib.Close()
if err := lib.CallExport("StartW"); err != nil {
return err
}
Native C and Rust exports can also receive up to three machine-word arguments and return a machine-word value:
result, err := lib.CallExportWithArgs(
"Run",
uintptr(unsafe.Pointer(unsafe.SliceData(input))),
uintptr(uint32(len(input))),
callbackPointer,
)
runtime.KeepAlive(input)
This matches extension entry points such as
Run(char *buffer, uint32_t size, callback_fn callback). Convert a Go pointer
to uintptr directly in the method call, as above, and keep the pointed-to
object alive until the export returns. CGO-free Darwin, FreeBSD, and Linux callers can
create C-callable Go callbacks with purego.NewCallback; Windows callers can
use syscall.NewCallback.
You can also load from a path:
lib, err := reflektor.LoadLibraryFile("./payload.dylib")
To read and map a library's non-system dependencies through Reflektor as well, use recursive mode:
lib, err := reflektor.LoadLibraryFileRecursive("./payload.dylib")
The byte-oriented equivalent resolves relative dependency names from the current working directory:
lib, err := reflektor.LoadLibraryRecursive(payload)
Both recursive APIs read the complete custom dependency graph before the root
export is invoked. LoadLibraryFileRecursive is preferred for libraries that
use origin-relative names such as $ORIGIN, @loader_path, or @rpath.
Beacon Object Files
BOF loading is provided by a separate, tag-free package:
import "github.com/sliverarmory/reflektor/bof"
No CGO or custom build tags are required. The package boundary keeps the BOF
parser, relocator, native callback bridge, and Beacon compatibility layer out
of shared-library-only consumers. BOF-only consumers can likewise import this
subpackage without pulling in the root package's memmod shared-library
backend.
bof.Load accepts the native relocatable-object convention used by each host.
Linux/ARMv7 hard-float (GOARM=7), Linux/ppc64le, and Linux/riscv64 also have
full root, native, recursive, C, Rust, and Go c-shared test coverage. ARMv5,
ARMv6, soft-float, and Thumb-compiled BOFs are not included in the ARM support
claim. PowerPC64 objects must use the little-endian ELFv2 ABI; ELFv1,
big-endian, NOTOC/P9NOTOC calls, and external tail branches are rejected.
| Host | BOF object format | Entry ABI |
|---|---|---|
Windows 386, amd64, arm64 |
COFF (.o) |
go(char *, int32) using the Windows ABI |
FreeBSD amd64, arm64 |
ELF ET_REL (.o) |
go(char *, int32) using the host ABI |
Linux 386, amd64, arm64, ppc64le, riscv64; ARMv7 hard-float (GOARM=7, ARM-state objects) |
ELF ET_REL (.o) |
go(char *, int32) using the host ABI |
Darwin amd64, arm64 |
Mach-O MH_OBJECT (.o); legacy ELF ET_REL accepted for compatibility |
go(char *, int32) using the host ABI |
Compile native Darwin objects with Zig's x86_64-macos-none or
aarch64-macos-none targets while using only host-compatible types and
imported Beacon/system functions. The native arm64 target follows Apple's x18
platform-register reservation automatically. The earlier constrained ELF
Darwin interchange format remains accepted for backwards compatibility; those
Linux-targeted arm64 objects must explicitly reserve x18.
Windows COFF machine code is not portable to FreeBSD, Linux, or Darwin.
The FreeBSD amd64 and arm64 rows run in FreeBSD 15.1 full-system QEMU
guests. Each executes generated BOFs, load options, repeated and concurrent
calls, close behavior, and all 25 objects from the pinned external BOF corpus
without CGO.
The ARMv7 hard-float, ppc64le, and riscv64 rows run BOFs without CGO under QEMU,
including generated fixtures, load options, repeated and concurrent execution,
close behavior, and the portable external BOF corpus. The same target images
exercise C and Rust shared libraries through root, native, recursive, and CLI
lifecycles with both CGO modes. Go c-shared exercises the cgo-enabled root,
recursive, and CLI lifecycles, while native proves its intentional Go-image
rejection. ARM uses a linux/arm/v7 image; ppc64le and riscv64 use native Debian
images with checksummed target Go toolchains under user-mode emulation.
var arguments bof.Arguments
_ = arguments.AddString("example")
loaded, err := bof.Load(objectBytes)
if err != nil {
return err
}
defer loaded.Close()
records, err := loaded.Execute(arguments.Bytes())
Use bof.LoadWithOptions (or bof.LoadFileWithOptions) when the host needs an
exact entry symbol or an import boundary. ValidateImports receives a sorted,
owned snapshot before image allocation, callback registration, or dynamic
library lookup. ResolveSymbol can then supply stable native function or data
addresses using the platform ABI for custom imports. Reflektor's built-in
callbacks always take precedence and
cannot be replaced through the custom resolver.
External data must use the target's native indirection convention when the
object format requires it. Windows data declarations should use
__declspec(dllimport) so COFF emits an import-pointer reference rather than a
range-limited direct reference.
Unsupported Beacon integration APIs—including token, temporary-process, and
process-injection callbacks—are marked RequiresHost and fail explicitly
unless ResolveSymbol supplies them; they are never silent stubs or system
symbol lookups. The built-in compatibility set also includes the bounded
BeaconDataExtractOrNull helper and toWideChar, whose destination is always
UTF-16LE and whose maximum length is measured in bytes.
The loader supplies the Beacon data, format, and output callbacks, preserves each output record's channel, uses page-level W^X protections, and serializes execution around the process-wide native callback bridge. A BOF is still arbitrary native code in the current process: malformed code can corrupt or terminate the host, so untrusted objects need a subprocess boundary. Object images are capped at 64 MiB and packed argument buffers at 16 MiB. Callback capture is synchronous: a BOF that starts native worker threads must join them before its entry point returns.
BeaconPrintf and BeaconFormatPrintf accept at most ten machine-word
arguments and implement bounded string, character, integer, and pointer
conversions. Floating-point conversions are rejected on 64-bit Unix because
its variadic ABI does not expose those values to the fixed callback bridge.
Native Mach-O Darwin/arm64 objects must use BeaconOutput instead: imports of
BeaconPrintf and BeaconFormatPrintf are rejected because Apple's variadic
ABI is incompatible with the fixed callback bridge. Legacy Darwin ELF objects
retain the existing callback behavior.
Native-only Package
Hosts that load only native C or Rust extensions can use the tag-free
github.com/sliverarmory/reflektor/native package:
import "github.com/sliverarmory/reflektor/native"
lib, err := native.LoadLibrary(payload)
It exposes the same CallExport, CallExportWithArgs, and Close lifecycle
for byte-backed native images. On FreeBSD and Linux, its import graph
deliberately excludes the root loader's Go c-shared TLS reservation. FreeBSD
amd64 and arm64, and Linux amd64, arm64, ppc64le, and riscv64 use
the PureGo call bridge in both CGO modes; ARMv7
uses its hard-float bridge, while Linux 386 uses Reflektor's integer-only
runtime.cgocall dispatcher. Valid Go c-shared payloads are rejected before
mapping with native.ErrGoSharedLibraryUnsupported; use the cgo-enabled root
reflektor package when Go c-shared loading is required. File and recursive
loading remain root-package features.
CLI
The CLI is in reflektor/cli and uses Cobra.
Build:
go build -o reflektor ./cli
Usage:
./reflektor <shared-library-path> [--call-export StartW]
--call-export defaults to StartW.
Behavior Notes
CallExportpreserves the original zero-argument API.CallExportWithArgsaccepts zero through threeuintptrarguments and returns the platform's primary machine-word return value.CallExportWithArgssupports native C and Rust images. Go c-shared images returnErrGoExportArgumentsUnsupported; their zero-argument exports remain available throughCallExport.- On Linux, the runtime-aware foreign-call bridges keep C-to-Go callbacks safe.
Consequently,
CGO_ENABLED=0hosts importing either Reflektor package are dynamically linked against the platform's glibc loader; this is not a fully static or musl-portable build mode. - CGO-free FreeBSD builds import PureGo's pinned fake-CGO bridge and require
-gcflags=github.com/ebitengine/purego/internal/fakecgo=-std. Native CGO-enabled FreeBSD builds do not require that flag. - Reflektor normalizes common symbol naming differences where possible (for example underscore-prefixed forms).
- The root
reflektor.LibraryAPI remains intentionally small:CallExport(),CallExportWithArgs(), andClose(). - Recursive mode maps file-backed application dependencies from their bytes and resolves imports within the in-memory graph. Platform runtime libraries remain delegated to the native loader: Darwin shared-cache libraries, Windows System32/API-set libraries, and FreeBSD/Linux libraries in trusted system roots. Those libraries require OS-managed TLS, symbol versioning, loader registration, and other facilities that cannot be reproduced by simply reading a file—and some Darwin shared-cache images do not exist as standalone readable files.
- FreeBSD and Linux custom images reject general ELF TLS, IFUNC/IRELATIVE, and RELR with explicit errors; those features remain available through the system-library carveout. Windows custom dependency cycles and delay-load import tables are also rejected explicitly. Darwin, FreeBSD, and Linux graph cycles are deduplicated.
- After the first export call, Darwin recursive mappings remain process-resident
because dyld retains their loader records. Reusing a Darwin install-name in a
later load follows dyld's first-loaded identity semantics. Calls made through
the same
Libraryreuse one mapped root, so an initializer and later argument-bearing exports share module state. LoadLibraryandLoadLibraryFileretain their original behavior and API.
Test Data And Validation
C test shared libraries are generated from:
reflektor/testdata/c/args.creflektor/testdata/c/basic.creflektor/testdata/c/freebsd_ld_library_path_dependency.creflektor/testdata/c/freebsd_ld_library_path_root.creflektor/testdata/c/freebsd_system_dependency.creflektor/testdata/c/native_lifecycle.creflektor/testdata/c/recursive_leaf.creflektor/testdata/c/recursive_middle.creflektor/testdata/c/recursive_root.c
The recursive C fixture is a transitive root -> middle -> leaf graph. Its test
checks that the graph is absent from FreeBSD procstat -v mappings,
Linux /proc/self/maps, or the Windows loader module registry, then renames
the dependency directory before calling StartW. On Darwin the rename happens
before the lazy dyld transaction. These
checks prove the custom dependencies came from bytes captured by Reflektor.
The Rust fixtures are built from reflektor/testdata/rust and
reflektor/testdata/rust/native_args.rs. The HTTPS fixture exports StartW,
performs a bounded GET https://example.com/ through libcurl on
Darwin/FreeBSD/Linux or
WinHTTP on Windows, and records ok:200 after receiving a non-empty successful
response. Both fixtures are dependency-free Rust (no_std) so they do not
require unsupported thread-local runtime state from the in-memory loaders.
The generated Linux Go c-shared fixture resolves the current user through
libc NSS before starting its scheduler work. The FreeBSD/amd64 fixture exercises
the same Go-runtime loader paths without the Linux-only NSS call. Together they
exercise native loader selection in both the legacy and recursive load paths,
including system GNU IFUNC symbols on Linux (such as RISC-V memcpy) and
versioned FBSD_1.0 imports on FreeBSD.
Build test shared libraries for the full matrix:
./testdata/build_c_shared_libs.sh
Run tests:
go test ./...
The Rust fixture test requires Cargo with Rust 1.85 or newer and outbound HTTPS access; the native GitHub jobs currently pin Rust 1.94.0. FreeBSD and Linux also require libcurl development files so the fixture can link against the system TLS client.
Linux cross-architecture Docker/QEMU harnesses:
- Shared-library matrix:
testdata/docker/linux-memmod.Dockerfileandtestdata/docker/run-linux-memmod-matrix.sh - ARMv7 BOFs and shared libraries:
testdata/docker/linux-bof.Dockerfileandtestdata/docker/run-linux-bof-tests.sh - RISC-V BOFs and shared libraries:
testdata/docker/linux-riscv64-bof.Dockerfileandtestdata/docker/run-linux-riscv64-bof-tests.sh - PPC64LE BOFs and shared libraries:
testdata/docker/linux-ppc64le-bof.Dockerfileandtestdata/docker/run-linux-ppc64le-bof-tests.sh
FreeBSD amd64 and arm64 use testdata/freebsd/run-freebsd-tests.sh inside
FreeBSD 15.1 QEMU guests. The harness validates BOF plus C/Rust root, native,
recursive, and CLI lifecycles in both CGO modes. It additionally validates Go
c-shared on amd64; Go 1.26.6 reports that build mode as unavailable on
FreeBSD/arm64, which the capability manifest records as n-a. The guests use
checksum-pinned Go 1.26.6 and Zig 0.16.0 archives.
The emulated runners reject every unexpected Go test skip and require named
passes for the C, Rust, Go c-shared, legacy byte, file, recursive dependency,
native, BOF fixture, and 25-object BOF corpus lifecycles. CLI cases assert their
output and side-effect markers directly. The Sliver E2E workflow
builds and runs a real shared implant and native extension for every target
whose pinned Sliver server can compile. FreeBSD is the explicit exception: the
pinned Sliver revision fails both FreeBSD server builds in its WireGuard
transport (net, device, and errors are undefined) and lacks FreeBSD
assetsFs bindings; its E2E driver also lacks FreeBSD process helpers.
Repository Layout
reflektor/reflektor.go: root importable package (reflektor).reflektor/bof: public, tag-free BOF API and argument encoder.reflektor/internal/bofloader: internal COFF/ELF/Mach-O loader and Beacon bridge.reflektor/native: tag-free native C/Rust-only package.reflektor/memmod: OS-specific loader backends.reflektor/cli: CLI entrypoint.reflektor/integration: external-package loader and native API tests.reflektor/testdata: portable shared-library fixtures and build/test harnesses.