From a735a619ab97f905e12513e1623285dcb856c034 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Fri, 18 Sep 2026 13:43:50 +0800 Subject: [PATCH 1/3] translator: preserve raw foreign ABI ARM64 callbacks --- README.md | 21 +++ native_arm64_object.go | 304 ++++++++++++++++++++++++++++++++++++ native_arm64_object_test.go | 135 ++++++++++++++++ 3 files changed, 460 insertions(+) create mode 100644 native_arm64_object.go create mode 100644 native_arm64_object_test.go diff --git a/README.md b/README.md index fd8561ab9..ee97408d3 100644 --- a/README.md +++ b/README.md @@ -163,3 +163,24 @@ go run -C cmd/plan9asm . transpile \ -o /tmp/file.ll \ -goos=linux -goarch=amd64 ``` + +## Foreign-ABI ARM64 assembly + +Raw C callbacks and tail-call trampolines sometimes have no Go function +signature. Passing them through typed LLVM lowering cannot preserve an unknown +set of incoming or outgoing registers. `ForeignARM64Functions` recognizes the +restricted file-local, NOSPLIT, zero-Go-frame form (non-leaf callbacks must +explicitly use NOFRAME). + +A compiler driver can assemble such a file with `go tool asm -p ` and +pass the Darwin/ARM64 Go object to `TranslateNativeARM64Object`. The returned +Mach-O assembly preserves the encoded instructions and maps R_ADDR and +R_CALLARM64 relocations to selected local definitions or explicitly supplied +dynamic imports. The accompanying DATA list lets the driver bind Go globals to +their assembly definitions. The driver remains responsible for selecting the +target, running tools, and supplying library link arguments. + +This path does not translate Go ABI functions. Unsupported object formats, +relocations, and undeclared foreign calls return errors instead of inventing +function signatures. The object reader accepts the go120ld format used since +Go 1.20; the runtime tests execute native callbacks only on Darwin/ARM64. diff --git a/native_arm64_object.go b/native_arm64_object.go new file mode 100644 index 000000000..f18d43e94 --- /dev/null +++ b/native_arm64_object.go @@ -0,0 +1,304 @@ +package plan9asm + +import ( + "bytes" + "encoding/binary" + "fmt" + "regexp" + "sort" + "strconv" + "strings" +) + +// NativeData describes a Go global whose storage is supplied by native assembly. +type NativeData struct { + Name string + Size uint32 +} + +// TranslateNativeARM64Object preserves the machine code of foreign-ABI assembly. Unlike +// Go-declared functions, raw callbacks have no signature from which LLVM can +// reconstruct incoming registers. The Go assembler encodes those registers; +// this bridge only converts its address and branch relocations to Mach-O asm. +// +// The input format is cmd/internal/goobj's go120ld format. Only explicitly +// selected functions, their data, and declared dynamic imports are accepted. +// Unknown formats, symbol references and relocations are errors. +func TranslateNativeARM64Object(obj []byte, funcs map[string]bool, imports map[string]string, pkgPath string) (string, []NativeData, error) { + r, err := readNativeObject(obj) + if err != nil { + return "", nil, err + } + labels := make(map[int]string) + var data []NativeData + for i, s := range r.syms[:r.ndef] { + if funcs[s.name] { + if s.flags&16 == 0 { + return "", nil, fmt.Errorf("native function %s is not NOSPLIT", s.name) + } + labels[i] = fmt.Sprintf("Lllgo_native_%d", i) + } else if strings.HasPrefix(s.name, pkgPath+".") { + labels[i] = "_" + s.name + data = append(data, NativeData{s.name, s.size}) + } + } + for name := range funcs { + found := false + for i := range labels { + if r.syms[i].name == name { + found = true + break + } + } + if !found { + return "", nil, fmt.Errorf("missing native function %s", name) + } + } + var out strings.Builder + for i, s := range r.syms[:r.ndef] { + label, ok := labels[i] + if !ok { + continue + } + code := r.payload(i) + if uint64(len(code)) > uint64(s.size) { + return "", nil, fmt.Errorf("oversized native symbol %s", s.name) + } + code = append(append([]byte(nil), code...), make([]byte, int(s.size)-len(code))...) + if funcs[s.name] { + out.WriteString(".text\n.p2align 2\n") + } else { + out.WriteString(".data\n") + fmt.Fprintf(&out, ".globl %s\n", strconv.Quote(label)) + align := s.align + if align == 0 { + align = 8 + } + if align&(align-1) != 0 { + return "", nil, fmt.Errorf("invalid native alignment %d", align) + } + fmt.Fprintf(&out, ".balign %d\n", align) + } + fmt.Fprintf(&out, "%s:\n", strconv.Quote(label)) + rels := r.relocs(i) + sort.Slice(rels, func(i, j int) bool { return rels[i].off < rels[j].off }) + pos := uint32(0) + for _, rel := range rels { + if rel.off < pos || uint64(rel.off)+uint64(rel.size) > uint64(len(code)) { + return "", nil, fmt.Errorf("invalid native relocation in %s", s.name) + } + target, err := r.target(rel.pkg, rel.sym) + if err != nil { + return "", nil, err + } + dst, ok := labels[target] + if !ok { + alias, ok := imports[r.syms[target].name] + if !ok { + return "", nil, fmt.Errorf("native assembly references undeclared foreign symbol %s", r.syms[target].name) + } + dst = "_" + alias + } + expr := strconv.Quote(dst) + if rel.add != 0 { + expr += fmt.Sprintf("%+d", rel.add) + } + nativeBytes(&out, code[pos:rel.off]) + switch { + case rel.kind == 1 && rel.size == 8: // R_ADDR + fmt.Fprintf(&out, ".quad %s\n", expr) + case rel.kind == 9 && rel.size == 4 && funcs[s.name]: // R_CALLARM64: B or BL + word := binary.LittleEndian.Uint32(code[rel.off:]) + op := "b" + if word&0xfc000000 == 0x94000000 { + op = "bl" + } else if word&0xfc000000 != 0x14000000 { + return "", nil, fmt.Errorf("invalid native branch in %s", s.name) + } + fmt.Fprintf(&out, "%s %s\n", op, expr) + default: + return "", nil, fmt.Errorf("unsupported native relocation %d/%d in %s", rel.kind, rel.size, s.name) + } + pos = rel.off + uint32(rel.size) + } + nativeBytes(&out, code[pos:]) + } + return out.String(), data, nil +} + +func nativeBytes(out *strings.Builder, b []byte) { + for len(b) > 0 { + n := len(b) + if n > 16 { + n = 16 + } + out.WriteString(".byte ") + for i, x := range b[:n] { + if i != 0 { + out.WriteByte(',') + } + fmt.Fprintf(out, "%d", x) + } + out.WriteByte('\n') + b = b[n:] + } +} + +type nativeSym struct { + name string + size, align uint32 + flags byte +} +type nativeReloc struct { + off uint32 + size byte + kind uint16 + add int64 + pkg, sym uint32 +} +type nativeObject struct { + b []byte + blocks [19]uint32 + syms []nativeSym + ndef int + counts [5]int +} + +func readNativeObject(obj []byte) (*nativeObject, error) { + // cmd/asm prefixes the binary object with a target/version text header. + h := bytes.Index(obj, []byte("\n!\n")) + if h < 0 || !bytes.HasPrefix(obj, []byte("go object darwin arm64 ")) { + return nil, fmt.Errorf("expected darwin/arm64 Go assembler object") + } + b := obj[h+3:] + if len(b) < 96 || string(b[:8]) != "\x00go120ld" { + return nil, fmt.Errorf("unsupported Go assembler object format") + } + r := &nativeObject{b: b} + for i := range r.blocks { + r.blocks[i] = binary.LittleEndian.Uint32(b[20+i*4:]) + if r.blocks[i] > uint32(len(b)) || (i > 0 && r.blocks[i] < r.blocks[i-1]) { + return nil, fmt.Errorf("invalid native object block %d", i) + } + } + if r.blocks[0] < 96 { + return nil, fmt.Errorf("invalid native object header") + } + for block := 3; block <= 7; block++ { + buf := r.block(block) + if len(buf)%21 != 0 { + return nil, fmt.Errorf("invalid native symbol table") + } + r.counts[block-3] = len(buf) / 21 + for len(buf) > 0 { + s := buf[:21] + n, off := binary.LittleEndian.Uint32(s), binary.LittleEndian.Uint32(s[4:]) + if uint64(off)+uint64(n) > uint64(len(b)) { + return nil, fmt.Errorf("invalid native symbol name") + } + size := binary.LittleEndian.Uint32(s[13:]) + if size > 64<<20 { + return nil, fmt.Errorf("native symbol too large") + } + r.syms = append(r.syms, nativeSym{string(b[off : off+n]), size, binary.LittleEndian.Uint32(s[17:]), s[11]}) + buf = buf[21:] + } + } + r.ndef = len(r.syms) - r.counts[4] + if len(r.block(11)) != (r.ndef+1)*4 || len(r.block(13)) != (r.ndef+1)*4 || len(r.block(14))%23 != 0 { + return nil, fmt.Errorf("invalid native object indices") + } + for _, pair := range [][2]int{{11, len(r.block(14)) / 23}, {13, len(r.block(16))}} { + prev := uint32(0) + buf := r.block(pair[0]) + for len(buf) > 0 { + v := binary.LittleEndian.Uint32(buf) + if v < prev || uint64(v) > uint64(pair[1]) { + return nil, fmt.Errorf("invalid native object index") + } + prev = v + buf = buf[4:] + } + } + return r, nil +} +func (r *nativeObject) block(i int) []byte { return r.b[r.blocks[i]:r.blocks[i+1]] } +func (r *nativeObject) payload(i int) []byte { + idx := r.block(13) + return r.block(16)[binary.LittleEndian.Uint32(idx[i*4:]):binary.LittleEndian.Uint32(idx[(i+1)*4:])] +} +func (r *nativeObject) relocs(i int) []nativeReloc { + idx := r.block(11) + a, z := binary.LittleEndian.Uint32(idx[i*4:]), binary.LittleEndian.Uint32(idx[(i+1)*4:]) + var out []nativeReloc + for n := a; n < z; n++ { + v := r.block(14)[int(n)*23:] + out = append(out, nativeReloc{binary.LittleEndian.Uint32(v), v[4], binary.LittleEndian.Uint16(v[5:]), int64(binary.LittleEndian.Uint64(v[7:])), binary.LittleEndian.Uint32(v[15:]), binary.LittleEndian.Uint32(v[19:])}) + } + return out +} +func (r *nativeObject) target(pkg, sym uint32) (int, error) { + // Reserved package indices from cmd/internal/goobj. Package imports and + // runtime builtins have Go ABI and cannot be called by these foreign stubs. + var start, count int + switch pkg { + case 0x7ffffffb: + count = r.counts[0] + case 0x7ffffffe: + start = r.counts[0] + count = r.counts[1] + case 0x7ffffffd: + start = r.counts[0] + r.counts[1] + count = r.counts[2] + case 0x7fffffff: + start = r.counts[0] + r.counts[1] + r.counts[2] + count = r.counts[3] + r.counts[4] + default: + return 0, fmt.Errorf("native assembly references Go package index %#x", pkg) + } + if uint64(sym) >= uint64(count) { + return 0, fmt.Errorf("invalid native symbol reference") + } + return start + int(sym), nil +} + +var nativeTextRE = regexp.MustCompile(`(?m)^\s*TEXT\s+([^\s(),]+)<>\(SB\),\s*([^,]+),\s*\$0(?:-0)?\s*(?://[^\n]*)?$`) + +// ForeignARM64Functions selects files made entirely of raw foreign-ABI +// callbacks/trampolines. Go-declared functions continue through typed LLVM +// lowering. NOSPLIT and zero Go frames are required; the callback may manage +// its own native frame explicitly with NOFRAME. +func ForeignARM64Functions(src []byte) map[string]bool { + matches := nativeTextRE.FindAllSubmatchIndex(src, -1) + if len(matches) == 0 || len(reTextLines.FindAll(src, -1)) != len(matches) { + return nil + } + result := make(map[string]bool) + for i, m := range matches { + flags := string(src[m[4]:m[5]]) + if !hasAsmFlag(flags, "NOSPLIT") { + return nil + } + end := len(src) + if i+1 < len(matches) { + end = matches[i+1][0] + } + if !hasAsmFlag(flags, "NOFRAME") && nativeCallRE.Match(src[m[1]:end]) { + return nil + } + result[string(src[m[2]:m[3]])] = true + } + return result +} + +var reTextLines = regexp.MustCompile(`(?m)^\s*TEXT\b`) +var nativeCallRE = regexp.MustCompile(`(?m)^\s*(?:CALL|BL)\s`) + +func hasAsmFlag(flags, want string) bool { + for _, f := range strings.Split(flags, "|") { + if strings.TrimSpace(f) == want { + return true + } + } + return false +} diff --git a/native_arm64_object_test.go b/native_arm64_object_test.go new file mode 100644 index 000000000..86b3a38e5 --- /dev/null +++ b/native_arm64_object_test.go @@ -0,0 +1,135 @@ +package plan9asm + +import ( + "encoding/binary" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" +) + +func nativeObjectFixture(t *testing.T) []byte { + t.Helper() + dir := t.TempDir() + src := filepath.Join(dir, "native.s") + out := filepath.Join(dir, "native.o") + code := `#include "textflag.h" +TEXT callback<>(SB), NOSPLIT|NOFRAME, $0 + SUB $16, RSP + MOVD R30, (RSP) + BL imported_strlen(SB) + MOVD (RSP), R30 + ADD $16, RSP + RET +GLOBL ·entry(SB), RODATA, $8 +DATA ·entry(SB)/8, $callback<>(SB) +TEXT mixedtramp<>(SB), NOSPLIT, $0-0 + FMOVD R1, F0 + JMP imported_mixed(SB) +GLOBL ·mixedEntry(SB), RODATA, $8 +DATA ·mixedEntry(SB)/8, $mixedtramp<>(SB) +` + if err := os.WriteFile(src, []byte(code), 0600); err != nil { + t.Fatal(err) + } + cmd := exec.Command("go", "tool", "asm", "-p", "probe", "-I", filepath.Join(runtime.GOROOT(), "pkg", "include"), "-o", out, src) + cmd.Env = append(os.Environ(), "GOOS=darwin", "GOARCH=arm64") + if b, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("asm: %v\n%s", err, b) + } + b, err := os.ReadFile(out) + if err != nil { + t.Fatal(err) + } + return b +} + +func TestNativeARM64Object(t *testing.T) { + obj := nativeObjectFixture(t) + funcs := map[string]bool{"callback": true, "mixedtramp": true} + imports := map[string]string{"imported_strlen": "strlen", "imported_mixed": "mixed"} + asm, data, err := TranslateNativeARM64Object(obj, funcs, imports, "probe") + if err != nil { + t.Fatal(err) + } + if len(data) != 2 || data[0].Name != "probe.entry" || data[0].Size != 8 { + t.Fatalf("data = %v", data) + } + if !strings.Contains(asm, `bl "_strlen"`) || !strings.Contains(asm, `.quad "Lllgo_native_`) { + t.Fatalf("missing relocations:\n%s", asm) + } + if _, _, err := TranslateNativeARM64Object(obj, funcs, nil, "probe"); err == nil { + t.Fatal("accepted undeclared foreign call") + } + if _, _, err := TranslateNativeARM64Object(obj, map[string]bool{"missing": true}, imports, "probe"); err == nil { + t.Fatal("accepted missing function") + } + for n := 0; n < len(obj); n++ { + if _, _, err := TranslateNativeARM64Object(obj[:n], funcs, imports, "probe"); err == nil { + t.Fatalf("accepted truncated object at %d", n) + } + } + t.Run("reject unsupported relocation", func(t *testing.T) { + damaged := append([]byte(nil), obj...) + r, err := readNativeObject(damaged) + if err != nil { + t.Fatal(err) + } + for i, sym := range r.syms[:r.ndef] { + if sym.name != "callback" { + continue + } + first := binary.LittleEndian.Uint32(r.block(11)[i*4:]) + rel := r.block(14)[first*23:] + binary.LittleEndian.PutUint16(rel[5:], 0xffff) + break + } + if _, _, err := TranslateNativeARM64Object(damaged, funcs, imports, "probe"); err == nil { + t.Fatal("accepted unknown relocation") + } + }) + if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" { + return + } + dir := t.TempDir() + s := filepath.Join(dir, "native.s") + c := filepath.Join(dir, "main.c") + exe := filepath.Join(dir, "probe") + os.WriteFile(s, []byte(asm), 0600) + os.WriteFile(c, []byte(`extern void *entry __asm("_probe.entry"); +extern void *mixedEntry __asm("_probe.mixedEntry"); +unsigned long mixed(unsigned long x, double y) { return x + (unsigned long)y; } +int main(void) { + if (((unsigned long (*)(const char *))entry)("native ABI") != 10) return 1; + return ((unsigned long (*)(unsigned long, unsigned long))mixedEntry)(5, 0x4000000000000000UL) != 7; +} +`), 0600) + cmd := exec.Command("clang", s, c, "-o", exe) + if b, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("link: %v\n%s", err, b) + } + if b, err := exec.Command(exe).CombinedOutput(); err != nil { + t.Fatalf("callback: %v\n%s", err, b) + } +} + +func TestForeignARM64Selection(t *testing.T) { + for _, tc := range []struct { + src string + want bool + }{ + {"TEXT raw<>(SB), NOSPLIT, $0-0\n JMP imported(SB)\n", true}, + {"TEXT raw<>(SB), NOSPLIT|NOFRAME, $0\n BL imported(SB)\n RET\n", true}, + {"TEXT ·declared(SB), NOSPLIT, $0-0\n RET\n", false}, + {"TEXT raw<>(SB), 0, $0-0\n RET\n", false}, + {"TEXT raw<>(SB), NOSPLIT, $8-0\n RET\n", false}, + {"TEXT raw<>(SB), NOSPLIT, $0-0\n BL imported(SB)\n RET\n", false}, + {"TEXT raw<>(SB), NOSPLIT, $0-0\n RET\nTEXT ·goFunc(SB),NOSPLIT,$0\nRET\n", false}, + } { + if got := len(ForeignARM64Functions([]byte(tc.src))) != 0; got != tc.want { + t.Errorf("selection(%q) = %v", tc.src, got) + } + } +} From 882a36bbcc1a4e58f868e7a5b52c92a7992b1678 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Fri, 18 Sep 2026 20:04:02 +0800 Subject: [PATCH 2/3] test: harden native ARM64 object validation after review --- native_arm64_object.go | 88 +++++++++++++++++++--------- native_arm64_object_test.go | 111 +++++++++++++++++++++++++++++++++++- 2 files changed, 170 insertions(+), 29 deletions(-) diff --git a/native_arm64_object.go b/native_arm64_object.go index f18d43e94..c4281302b 100644 --- a/native_arm64_object.go +++ b/native_arm64_object.go @@ -10,6 +10,35 @@ import ( "strings" ) +// Layout constants from Go 1.27 cmd/internal/goobj/objfile.go (go120ld) +// and cmd/internal/objabi/reloctype.go. Reject other object formats below. +const ( + nativeBlockCount = 19 // includes the end offset + nativeHeaderSize = 20 + nativeBlockCount*4 + nativeSymSize = 21 + nativeSymFlags = 11 + nativeSymSizeOffset = 13 + nativeSymAlign = 17 + nativeRelocSize = 23 + nativeRelocKind = 5 + nativeRelocAddend = 7 + nativeRelocPkg = 15 + nativeRelocSym = 19 + nativeNoSplit = 1 << 4 + nativeRAddr = 1 + nativeRCallARM64 = 9 + nativeBlkSymdef = 3 + nativeBlkNonpkgref = 7 + nativeBlkRelocIdx = 11 + nativeBlkDataIdx = 13 + nativeBlkReloc = 14 + nativeBlkData = 16 + nativePkgSelf = 0x7ffffffb + nativePkgHashed64 = 0x7ffffffe + nativePkgHashed = 0x7ffffffd + nativePkgNone = 0x7fffffff +) + // NativeData describes a Go global whose storage is supplied by native assembly. type NativeData struct { Name string @@ -21,7 +50,7 @@ type NativeData struct { // reconstruct incoming registers. The Go assembler encodes those registers; // this bridge only converts its address and branch relocations to Mach-O asm. // -// The input format is cmd/internal/goobj's go120ld format. Only explicitly +// The input must be a darwin/arm64 cmd/asm object in go120ld format. Only explicitly // selected functions, their data, and declared dynamic imports are accepted. // Unknown formats, symbol references and relocations are errors. func TranslateNativeARM64Object(obj []byte, funcs map[string]bool, imports map[string]string, pkgPath string) (string, []NativeData, error) { @@ -33,7 +62,7 @@ func TranslateNativeARM64Object(obj []byte, funcs map[string]bool, imports map[s var data []NativeData for i, s := range r.syms[:r.ndef] { if funcs[s.name] { - if s.flags&16 == 0 { + if s.flags&nativeNoSplit == 0 { return "", nil, fmt.Errorf("native function %s is not NOSPLIT", s.name) } labels[i] = fmt.Sprintf("Lllgo_native_%d", i) @@ -105,9 +134,12 @@ func TranslateNativeARM64Object(obj []byte, funcs map[string]bool, imports map[s } nativeBytes(&out, code[pos:rel.off]) switch { - case rel.kind == 1 && rel.size == 8: // R_ADDR + case rel.kind == nativeRAddr && rel.size == 8: // R_ADDR fmt.Fprintf(&out, ".quad %s\n", expr) - case rel.kind == 9 && rel.size == 4 && funcs[s.name]: // R_CALLARM64: B or BL + case rel.kind == nativeRCallARM64 && rel.size == 4 && funcs[s.name]: // R_CALLARM64: B or BL + if rel.add != 0 { + return "", nil, fmt.Errorf("unsupported native branch addend %d in %s", rel.add, s.name) + } word := binary.LittleEndian.Uint32(code[rel.off:]) op := "b" if word&0xfc000000 == 0x94000000 { @@ -127,6 +159,7 @@ func TranslateNativeARM64Object(obj []byte, funcs map[string]bool, imports map[s } func nativeBytes(out *strings.Builder, b []byte) { + var digits [3]byte // largest byte value is 255 for len(b) > 0 { n := len(b) if n > 16 { @@ -137,7 +170,7 @@ func nativeBytes(out *strings.Builder, b []byte) { if i != 0 { out.WriteByte(',') } - fmt.Fprintf(out, "%d", x) + out.Write(strconv.AppendUint(digits[:0], uint64(x), 10)) } out.WriteByte('\n') b = b[n:] @@ -158,7 +191,7 @@ type nativeReloc struct { } type nativeObject struct { b []byte - blocks [19]uint32 + blocks [nativeBlockCount]uint32 syms []nativeSym ndef int counts [5]int @@ -171,7 +204,7 @@ func readNativeObject(obj []byte) (*nativeObject, error) { return nil, fmt.Errorf("expected darwin/arm64 Go assembler object") } b := obj[h+3:] - if len(b) < 96 || string(b[:8]) != "\x00go120ld" { + if len(b) < nativeHeaderSize || string(b[:8]) != "\x00go120ld" { return nil, fmt.Errorf("unsupported Go assembler object format") } r := &nativeObject{b: b} @@ -181,34 +214,34 @@ func readNativeObject(obj []byte) (*nativeObject, error) { return nil, fmt.Errorf("invalid native object block %d", i) } } - if r.blocks[0] < 96 { + if r.blocks[0] < nativeHeaderSize { return nil, fmt.Errorf("invalid native object header") } - for block := 3; block <= 7; block++ { + for block := nativeBlkSymdef; block <= nativeBlkNonpkgref; block++ { buf := r.block(block) - if len(buf)%21 != 0 { + if len(buf)%nativeSymSize != 0 { return nil, fmt.Errorf("invalid native symbol table") } - r.counts[block-3] = len(buf) / 21 + r.counts[block-nativeBlkSymdef] = len(buf) / nativeSymSize for len(buf) > 0 { - s := buf[:21] + s := buf[:nativeSymSize] n, off := binary.LittleEndian.Uint32(s), binary.LittleEndian.Uint32(s[4:]) if uint64(off)+uint64(n) > uint64(len(b)) { return nil, fmt.Errorf("invalid native symbol name") } - size := binary.LittleEndian.Uint32(s[13:]) + size := binary.LittleEndian.Uint32(s[nativeSymSizeOffset:]) if size > 64<<20 { return nil, fmt.Errorf("native symbol too large") } - r.syms = append(r.syms, nativeSym{string(b[off : off+n]), size, binary.LittleEndian.Uint32(s[17:]), s[11]}) - buf = buf[21:] + r.syms = append(r.syms, nativeSym{string(b[off : off+n]), size, binary.LittleEndian.Uint32(s[nativeSymAlign:]), s[nativeSymFlags]}) + buf = buf[nativeSymSize:] } } r.ndef = len(r.syms) - r.counts[4] - if len(r.block(11)) != (r.ndef+1)*4 || len(r.block(13)) != (r.ndef+1)*4 || len(r.block(14))%23 != 0 { + if len(r.block(nativeBlkRelocIdx)) != (r.ndef+1)*4 || len(r.block(nativeBlkDataIdx)) != (r.ndef+1)*4 || len(r.block(nativeBlkReloc))%nativeRelocSize != 0 { return nil, fmt.Errorf("invalid native object indices") } - for _, pair := range [][2]int{{11, len(r.block(14)) / 23}, {13, len(r.block(16))}} { + for _, pair := range [][2]int{{nativeBlkRelocIdx, len(r.block(nativeBlkReloc)) / nativeRelocSize}, {nativeBlkDataIdx, len(r.block(nativeBlkData))}} { prev := uint32(0) buf := r.block(pair[0]) for len(buf) > 0 { @@ -222,18 +255,21 @@ func readNativeObject(obj []byte) (*nativeObject, error) { } return r, nil } + +// block accepts only the block constants above, all strictly before the end +// offset. readNativeObject validates every offset before calling it. func (r *nativeObject) block(i int) []byte { return r.b[r.blocks[i]:r.blocks[i+1]] } func (r *nativeObject) payload(i int) []byte { - idx := r.block(13) - return r.block(16)[binary.LittleEndian.Uint32(idx[i*4:]):binary.LittleEndian.Uint32(idx[(i+1)*4:])] + idx := r.block(nativeBlkDataIdx) + return r.block(nativeBlkData)[binary.LittleEndian.Uint32(idx[i*4:]):binary.LittleEndian.Uint32(idx[(i+1)*4:])] } func (r *nativeObject) relocs(i int) []nativeReloc { - idx := r.block(11) + idx := r.block(nativeBlkRelocIdx) a, z := binary.LittleEndian.Uint32(idx[i*4:]), binary.LittleEndian.Uint32(idx[(i+1)*4:]) var out []nativeReloc for n := a; n < z; n++ { - v := r.block(14)[int(n)*23:] - out = append(out, nativeReloc{binary.LittleEndian.Uint32(v), v[4], binary.LittleEndian.Uint16(v[5:]), int64(binary.LittleEndian.Uint64(v[7:])), binary.LittleEndian.Uint32(v[15:]), binary.LittleEndian.Uint32(v[19:])}) + v := r.block(nativeBlkReloc)[int(n)*nativeRelocSize:] + out = append(out, nativeReloc{binary.LittleEndian.Uint32(v), v[4], binary.LittleEndian.Uint16(v[nativeRelocKind:]), int64(binary.LittleEndian.Uint64(v[nativeRelocAddend:])), binary.LittleEndian.Uint32(v[nativeRelocPkg:]), binary.LittleEndian.Uint32(v[nativeRelocSym:])}) } return out } @@ -242,15 +278,15 @@ func (r *nativeObject) target(pkg, sym uint32) (int, error) { // runtime builtins have Go ABI and cannot be called by these foreign stubs. var start, count int switch pkg { - case 0x7ffffffb: + case nativePkgSelf: count = r.counts[0] - case 0x7ffffffe: + case nativePkgHashed64: start = r.counts[0] count = r.counts[1] - case 0x7ffffffd: + case nativePkgHashed: start = r.counts[0] + r.counts[1] count = r.counts[2] - case 0x7fffffff: + case nativePkgNone: start = r.counts[0] + r.counts[1] + r.counts[2] count = r.counts[3] + r.counts[4] default: diff --git a/native_arm64_object_test.go b/native_arm64_object_test.go index 86b3a38e5..97c4dc0c7 100644 --- a/native_arm64_object_test.go +++ b/native_arm64_object_test.go @@ -97,15 +97,19 @@ func TestNativeARM64Object(t *testing.T) { s := filepath.Join(dir, "native.s") c := filepath.Join(dir, "main.c") exe := filepath.Join(dir, "probe") - os.WriteFile(s, []byte(asm), 0600) - os.WriteFile(c, []byte(`extern void *entry __asm("_probe.entry"); + if err := os.WriteFile(s, []byte(asm), 0600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(c, []byte(`extern void *entry __asm("_probe.entry"); extern void *mixedEntry __asm("_probe.mixedEntry"); unsigned long mixed(unsigned long x, double y) { return x + (unsigned long)y; } int main(void) { if (((unsigned long (*)(const char *))entry)("native ABI") != 10) return 1; return ((unsigned long (*)(unsigned long, unsigned long))mixedEntry)(5, 0x4000000000000000UL) != 7; } -`), 0600) +`), 0600); err != nil { + t.Fatal(err) + } cmd := exec.Command("clang", s, c, "-o", exe) if b, err := cmd.CombinedOutput(); err != nil { t.Fatalf("link: %v\n%s", err, b) @@ -121,6 +125,8 @@ func TestForeignARM64Selection(t *testing.T) { want bool }{ {"TEXT raw<>(SB), NOSPLIT, $0-0\n JMP imported(SB)\n", true}, + {"TEXT raw<>(SB), NOSPLIT, $0-0\r\n JMP imported(SB)\r\n", true}, + {"TEXT raw<>(SB), NOSPLIT, $0-0\n RET\nTEXT raw2<>(SB),NOSPLIT,$0\nRET\n", true}, {"TEXT raw<>(SB), NOSPLIT|NOFRAME, $0\n BL imported(SB)\n RET\n", true}, {"TEXT ·declared(SB), NOSPLIT, $0-0\n RET\n", false}, {"TEXT raw<>(SB), 0, $0-0\n RET\n", false}, @@ -133,3 +139,102 @@ func TestForeignARM64Selection(t *testing.T) { } } } + +// Exercise the parser's rejection contract using valid assembler output with +// one damaged field at a time, so each diagnostic identifies the broken field. +func TestNativeARM64MalformedFields(t *testing.T) { + obj := nativeObjectFixture(t) + funcs := map[string]bool{"callback": true, "mixedtramp": true} + imports := map[string]string{"imported_strlen": "strlen", "imported_mixed": "mixed"} + cases := []struct { + name, want string + damage func(*nativeObject) + }{ + {"header", "invalid native object header", func(r *nativeObject) { binary.LittleEndian.PutUint32(r.b[20:], nativeHeaderSize-1) }}, + {"symbol table", "invalid native symbol table", func(r *nativeObject) { + binary.LittleEndian.PutUint32(r.b[20+nativeBlkNonpkgref*4:], r.blocks[nativeBlkNonpkgref]-1) + }}, + {"symbol name", "invalid native symbol name", func(r *nativeObject) { binary.LittleEndian.PutUint32(r.block(nativeBlkSymdef), ^uint32(0)) }}, + {"symbol size", "native symbol too large", func(r *nativeObject) { + binary.LittleEndian.PutUint32(r.block(nativeBlkSymdef)[nativeSymSizeOffset:], 1<<30) + }}, + {"symbol payload", "oversized native symbol", func(r *nativeObject) { + binary.LittleEndian.PutUint32(r.block(nativeBlkSymdef)[nativeSymSizeOffset:], 0) + }}, + {"alignment", "invalid native alignment", func(r *nativeObject) { + binary.LittleEndian.PutUint32(nativeFixtureSym(r, "probe.entry")[nativeSymAlign:], 3) + }}, + {"indices length", "invalid native object indices", func(r *nativeObject) { + binary.LittleEndian.PutUint32(r.b[20+nativeBlkRelocIdx*4:], r.blocks[nativeBlkRelocIdx]+1) + }}, + {"index range", "invalid native object index", func(r *nativeObject) { binary.LittleEndian.PutUint32(r.block(nativeBlkDataIdx), ^uint32(0)) }}, + {"split function", "not NOSPLIT", func(r *nativeObject) { nativeFixtureSym(r, "callback")[nativeSymFlags] &^= nativeNoSplit }}, + {"relocation bounds", "invalid native relocation", func(r *nativeObject) { binary.LittleEndian.PutUint32(nativeCallbackReloc(r), ^uint32(0)) }}, + {"Go ABI reference", "Go package index", func(r *nativeObject) { binary.LittleEndian.PutUint32(nativeCallbackReloc(r)[nativeRelocPkg:], 1) }}, + {"reference range", "invalid native symbol reference", func(r *nativeObject) { + binary.LittleEndian.PutUint32(nativeCallbackReloc(r)[nativeRelocSym:], ^uint32(0)) + }}, + {"branch addend", "unsupported native branch addend", func(r *nativeObject) { binary.LittleEndian.PutUint64(nativeCallbackReloc(r)[nativeRelocAddend:], 8) }}, + {"branch opcode", "invalid native branch", func(r *nativeObject) { + for i, s := range r.syms { + if s.name == "callback" { + binary.LittleEndian.PutUint32(r.payload(i)[binary.LittleEndian.Uint32(nativeCallbackReloc(r)):], 0) + } + } + }}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + damaged := append([]byte(nil), obj...) + r, err := readNativeObject(damaged) + if err != nil { + t.Fatal(err) + } + tc.damage(r) + _, _, err = TranslateNativeARM64Object(damaged, funcs, imports, "probe") + if err == nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("got %v, want %q", err, tc.want) + } + }) + } +} + +func nativeFixtureSym(r *nativeObject, name string) []byte { + for i, s := range r.syms { + if s.name == name { + for block, count := range r.counts { + if i < count { + return r.block(nativeBlkSymdef + block)[i*nativeSymSize:] + } + i -= count + } + } + } + panic("fixture missing symbol " + name) +} + +func nativeCallbackReloc(r *nativeObject) []byte { + for i, s := range r.syms { + if s.name == "callback" { + first := binary.LittleEndian.Uint32(r.block(nativeBlkRelocIdx)[i*4:]) + return r.block(nativeBlkReloc)[first*nativeRelocSize:] + } + } + panic("fixture missing callback") +} + +func TestNativeARM64SymbolReferences(t *testing.T) { + r := &nativeObject{counts: [5]int{2, 3, 4, 5, 6}} + for _, tc := range []struct { + pkg uint32 + sym uint32 + want int + }{ + {nativePkgSelf, 1, 1}, {nativePkgHashed64, 2, 4}, {nativePkgHashed, 3, 8}, {nativePkgNone, 10, 19}, + } { + got, err := r.target(tc.pkg, tc.sym) + if err != nil || got != tc.want { + t.Fatalf("target(%x,%d) = %d, %v; want %d", tc.pkg, tc.sym, got, err, tc.want) + } + } +} From 8339e6aebda3c53bc51f200ae66d124a6458a9b0 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Fri, 18 Sep 2026 20:04:02 +0800 Subject: [PATCH 3/3] test: refresh lz4 corpus to v4.1.30 --- doc/plan9asm-corpus.md | 2 +- testdata/corpus/go.mod | 2 +- testdata/corpus/go.sum | 2 ++ testdata/corpus/reported-libraries.json | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/plan9asm-corpus.md b/doc/plan9asm-corpus.md index 77f79e358..80ee4bc5c 100644 --- a/doc/plan9asm-corpus.md +++ b/doc/plan9asm-corpus.md @@ -229,7 +229,7 @@ The ecosystem scan also pins these latest modules: - `github.com/klauspost/reedsolomon v1.14.2` - `github.com/minio/highwayhash v1.0.4` - `github.com/modern-go/gls v0.0.0-20250215024828-78308f6bb19d` -- `github.com/pierrec/lz4/v4 v4.1.29` +- `github.com/pierrec/lz4/v4 v4.1.30` - `github.com/stevvooe/resumable v0.0.0-20180830230917-22b14a53ba50` - `github.com/tmthrgd/go-bitwise v0.0.0-20190904053232-1430ee983fca` - `github.com/tmthrgd/go-popcount v0.0.0-20190904054823-afb1ace8b04f` diff --git a/testdata/corpus/go.mod b/testdata/corpus/go.mod index d46191f19..3fa2be21c 100644 --- a/testdata/corpus/go.mod +++ b/testdata/corpus/go.mod @@ -19,7 +19,7 @@ require ( github.com/klauspost/reedsolomon v1.14.2 github.com/minio/highwayhash v1.0.4 github.com/modern-go/gls v0.0.0-20250215024828-78308f6bb19d - github.com/pierrec/lz4/v4 v4.1.29 + github.com/pierrec/lz4/v4 v4.1.30 github.com/stevvooe/resumable v0.0.0-20180830230917-22b14a53ba50 github.com/tmthrgd/go-bitwise v0.0.0-20190904053232-1430ee983fca github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc diff --git a/testdata/corpus/go.sum b/testdata/corpus/go.sum index dc017377b..8e7ff4706 100644 --- a/testdata/corpus/go.sum +++ b/testdata/corpus/go.sum @@ -41,6 +41,8 @@ github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= github.com/pierrec/lz4/v4 v4.1.29 h1:CDQY6qZOLI4DW0Nx6R1vRrifrCeQHnNXkMb0hZWXFjg= github.com/pierrec/lz4/v4 v4.1.29/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pierrec/lz4/v4 v4.1.30 h1:cchX8N2DVP668WkElI9QMwVyoNabLkq1LofDHFeIrdg= +github.com/pierrec/lz4/v4 v4.1.30/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= diff --git a/testdata/corpus/reported-libraries.json b/testdata/corpus/reported-libraries.json index 0c1cb29bb..b11dcad44 100644 --- a/testdata/corpus/reported-libraries.json +++ b/testdata/corpus/reported-libraries.json @@ -360,7 +360,7 @@ "id": "pierrec-lz4-v4", "origin": "ecosystem-scan", "module": "github.com/pierrec/lz4/v4", - "version": "v4.1.29", + "version": "v4.1.30", "inventory": { "darwin/amd64": { "asm_files": 1,