Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ require (
)

replace github.com/xgo-dev/llgo/runtime => ./runtime

// Temporary dependency on https://github.com/xgo-dev/llvm/pull/52.
replace github.com/xgo-dev/llvm => github.com/zhouguangyuan0718/go-llvm v0.0.0-20260905230337-4de18f5844ba
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ github.com/xgo-dev/llvm v0.9.9 h1:u4RZvy0vxu8nNt4oTrhttyMSyscYtmiy9geP+Kl/RYg=
github.com/xgo-dev/llvm v0.9.9/go.mod h1:42vav2/cI5BAIcL543DZSMO9do8/aCK2z7JERH+AE+M=
github.com/xgo-dev/plan9asm v0.5.2 h1:P5eGHZiuKNcL2tlkkICa2XcxghUSnp6QzfN52x0HW1g=
github.com/xgo-dev/plan9asm v0.5.2/go.mod h1:ZzybDk59y/AXoQeYUEqxVkKWqalmCfmcfCtM9y85kQU=
github.com/zhouguangyuan0718/go-llvm v0.0.0-20260905230337-4de18f5844ba h1:P+HZNghDqxC307G1+EhqlSj6m6PL7lRzjtVCUaDQdLs=
github.com/zhouguangyuan0718/go-llvm v0.0.0-20260905230337-4de18f5844ba/go.mod h1:42vav2/cI5BAIcL543DZSMO9do8/aCK2z7JERH+AE+M=
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
Expand Down
128 changes: 128 additions & 0 deletions runtime/internal/test/runtime_attrs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//go:build llgo

package test

import (
"testing"
"unsafe"

rt "github.com/xgo-dev/llgo/runtime/internal/runtime"
)

//go:linkname runtimeMemequal github.com/xgo-dev/llgo/runtime/internal/runtime.memequal
func runtimeMemequal(a, b unsafe.Pointer, size uintptr) bool

//go:linkname reflectTypedmemmove reflect.typedmemmove
func reflectTypedmemmove(typ *rt.Type, dst, src unsafe.Pointer)

func TestRuntimeCheckedPointer(t *testing.T) {
x := 42
p := unsafe.Pointer(&x)
if rt.AssertNilDerefPtr(p) != p {
t.Fatal("checked pointer identity changed")
}
defer func() {
if recover() == nil {
t.Fatal("nil pointer check did not raise a recoverable panic")
}
}()
rt.AssertNilDerefPtr(nil)
t.Fatal("nil pointer check returned")
}

func TestRuntimeMemoryContracts(t *testing.T) {
if !runtimeMemequal(nil, nil, 0) {
t.Fatal("zero-byte equality must accept nil")
}
a, b := [4]byte{1, 2, 3, 4}, [4]byte{1, 2, 3, 4}
if !runtimeMemequal(unsafe.Pointer(&a), unsafe.Pointer(&b), 4) {
t.Fatal("equal bytes differ")
}
b[2] = 9
if runtimeMemequal(unsafe.Pointer(&a), unsafe.Pointer(&b), 4) {
t.Fatal("equality did not observe an intervening write")
}
// memmove permits overlap in both directions. The reflect linkname entry
// must retain the same contract as the compiler's runtime entry.
for _, move := range []func(*rt.Type, unsafe.Pointer, unsafe.Pointer){rt.Typedmemmove, reflectTypedmemmove} {
x := [5]byte{1, 2, 3, 4, 5}
typ := rt.Type{Size_: 4}
move(&typ, unsafe.Pointer(&x[1]), unsafe.Pointer(&x[0]))
if x != [5]byte{1, 1, 2, 3, 4} {
t.Fatalf("forward overlap: %v", x)
}
move(&typ, unsafe.Pointer(&x[0]), unsafe.Pointer(&x[1]))
if x != [5]byte{1, 2, 3, 4, 4} {
t.Fatalf("backward overlap: %v", x)
}
move(nil, nil, nil) // equality fast path does not inspect the type
}
rt.Typedmemclr(&rt.Type{Size_: 4}, unsafe.Pointer(&a))
if a != [4]byte{} {
t.Fatalf("clear: %v", a)
}
buf := [8]byte{7, 7, 7, 7, 7, 7, 7, 7}
for _, s := range []string{"", "abc"} {
result := rt.CStrCopy(unsafe.Pointer(&buf[0]), *(*rt.String)(unsafe.Pointer(&s)))
if unsafe.Pointer(result) != unsafe.Pointer(&buf[0]) || buf[len(s)] != 0 || string(buf[:len(s)]) != s {
t.Fatalf("C string copy %q: %v", s, buf)
}
}
}

func TestRuntimeReadContracts(t *testing.T) {
if rt.MapLen(nil) != 0 || rt.ChanCap(nil) != 0 {
t.Fatal("nil length/capacity must be zero")
}
m := map[int]int{1: 10}
mp := (*rt.Map)(*(*unsafe.Pointer)(unsafe.Pointer(&m)))
if rt.MapLen(mp) != 1 {
t.Fatal("map length")
}
m[2] = 20
if rt.MapLen(mp) != 2 {
t.Fatal("map length did not observe insertion")
}
ch := make(chan int, 3)
cp := (*rt.Chan)(*(*unsafe.Pointer)(unsafe.Pointer(&ch)))
if rt.ChanCap(cp) != 3 {
t.Fatal("channel capacity")
}
for _, pair := range []struct {
a, b string
equal, less bool
}{
{"", "", true, false}, {"", "x", false, true},
{"ab", "abc", false, true}, {"abc", "abd", false, true},
{"abc", "abc", true, false}, {"abd", "abc", false, false},
} {
ra, rb := *(*rt.String)(unsafe.Pointer(&pair.a)), *(*rt.String)(unsafe.Pointer(&pair.b))
if rt.StringEqual(ra, rb) != pair.equal || rt.StringLess(ra, rb) != pair.less {
t.Fatalf("string comparison: %q %q", pair.a, pair.b)
}
}
}

func TestRuntimePanicContracts(t *testing.T) {
for _, raise := range []func(){
func() { rt.Panic("test") },
func() { rt.Panic(nil) },
func() { rt.PanicErrorString("test") },
func() { rt.PanicIndex(3, 2) },
func() { rt.PanicIndexU(3, 2) },
func() { rt.PanicSliceConvert(3, 2) },
func() { rt.PanicTypeAssertionError("test") },
} {
func() {
defer func() {
if recover() == nil {
t.Fatal("panic was not recoverable")
}
}()
raise()
t.Fatal("panic entry returned normally")
}()
}
// Rethrow is also used with no active panic and must return normally.
rt.Rethrow(nil)
}
1 change: 1 addition & 0 deletions ssa/decl.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ func (p Package) newFunc(
llvmName = p.Prog.stdcallSymbolName(name)
}
fn := llvm.AddFunction(p.mod, llvmName, t.ll)
p.Prog.addRuntimeAttributes(fn, name)
if bg == InStdcall {
fn.SetFunctionCallConv(p.Prog.stdcallCallConv())
}
Expand Down
111 changes: 111 additions & 0 deletions ssa/runtime_attrs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package ssa

import "github.com/xgo-dev/llvm"

// runtimeContracts names the fixed runtime entries whose implementations obey
// these contracts, including declarations imported into a different module.
// Match resolved symbols so go:linkname callers get the same attributes.
var runtimeContracts = map[string]string{
PkgRuntime + ".AssertNilDerefPtr": "checked",
PkgRuntime + ".CStrCopy": "cstrcopy",
PkgRuntime + ".memequal": "compare",
PkgRuntime + ".StringEqual": "read",
PkgRuntime + ".StringLess": "read",
PkgRuntime + ".Typedmemmove": "move",
"reflect.typedmemmove": "move",
PkgRuntime + ".Typedmemclr": "clear",
PkgRuntime + ".MapLen": "length",
PkgRuntime + ".ChanCap": "length",
PkgRuntime + ".Memhash": "hash",
PkgRuntime + ".Memhash32": "hash",
PkgRuntime + ".Memhash64": "hash",

PkgRuntime + ".Panic": "panic",
PkgRuntime + ".PanicErrorString": "panic",
PkgRuntime + ".PanicIndex": "panic",
PkgRuntime + ".PanicIndexU": "panic",
PkgRuntime + ".PanicSliceConvert": "panic",
PkgRuntime + ".PanicTypeAssert": "panic",
PkgRuntime + ".PanicTypeAssertionError": "panic",
PkgRuntime + ".PanicExtendIndex": "panic",
PkgRuntime + ".PanicExtendIndexU": "panic",
}

// LLVM 22 MemoryEffects uses two ModRef bits per location, with argument
// memory first. Ref=1, ModRef=3; there are six locations (including target
// memory). CaptureInfo stores return components in the low four bits.
const (
runtimeArgRead = 1
runtimeArgReadWrite = 3
runtimeMemoryRead = 0x555
runtimeCaptureReturn = 0xf
)

func (p Program) addRuntimeAttributes(fn llvm.Value, name string) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardening: no signature/arity validation before applying per-parameter pointer attributes.

addRuntimeAttributes keys purely on the resolved name and then blindly applies pointer(2, "writeonly"), pointer(3, "readonly"), etc. The reflect.typedmemmove entry is a bare pkg.name reachable by the real reflect package; today its signature matches the move contract, but nothing enforces that. If a contract-named symbol ever has a different shape (signature drift, or a future name collision), pointer attributes would be attached to a non-pointer parameter or an out-of-range index, producing a silent miscompile that is very hard to trace.

Consider asserting parameter count and that the targeted indices are pointer-typed before adding pointer attributes (mirroring the width guard already in addNonNegativeReturnRange). Cheap, and it turns a latent silent-miscompile into a loud panic.

contract, ok := runtimeContracts[name]
if !ok {
return
}
add := func(index int, name string, value uint64) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor readability: the closure parameter name string shadows the enclosing function's name argument (the symbol name). Harmless since the outer name isn't used after the map lookup, but shadowing an attribute-kind string over a symbol-name string is easy to misread. Renaming the closure param to attr/kind would remove the ambiguity.

fn.AddAttributeAtIndex(index, p.ctx.CreateEnumAttribute(llvm.AttributeKindID(name), value))
}
pointer := func(index int, access string) {
add(index, access, 0)
add(index, "captures", 0)
}
switch contract {
case "checked":
// nil is a valid input: the runtime must still raise a recoverable
// panic. Only a normal return guarantees non-nullness.
add(0, "nonnull", 0)
add(1, "returned", 0)
return
case "panic":
add(-1, "cold", 0)
add(-1, "noreturn", 0)
return
}
for _, attr := range []string{"nofree", "nosync", "nounwind", "willreturn"} {
add(-1, attr, 0)
}
switch contract {
case "cstrcopy":
add(1, "returned", 0)
add(1, "writeonly", 0)
add(1, "captures", runtimeCaptureReturn)
// The source string is an aggregate, not a pointer argument.
add(-1, "memory", runtimeMemoryRead|runtimeArgReadWrite)
case "compare":
pointer(1, "readonly")
pointer(2, "readonly")
add(-1, "memory", runtimeArgRead)
case "read":
// String data is carried in aggregate parameters.
add(-1, "memory", runtimeMemoryRead)
case "move", "clear":
pointer(1, "readonly")
pointer(2, "writeonly")
if contract == "move" {
pointer(3, "readonly")
}
// No noalias or nonnull: overlap and zero-byte copies are valid.
add(-1, "memory", runtimeArgReadWrite)
case "length":
pointer(1, "readonly")
add(-1, "memory", runtimeArgRead)
addNonNegativeReturnRange(p.ctx, fn, p.Int().ll.IntTypeWidth())
case "hash":
pointer(1, "readonly")
// Hashing also reads the process-global hashkey seed.
add(-1, "memory", runtimeMemoryRead)
}
}

func addNonNegativeReturnRange(ctx llvm.Context, fn llvm.Value, bits int) {
if bits != 32 && bits != 64 {
panic("ssa: unsupported runtime integer width")
}
attr := ctx.CreateConstantRangeAttribute(llvm.AttributeKindID("range"), bits,
[]uint64{0}, []uint64{uint64(1) << (bits - 1)})
fn.AddAttributeAtIndex(0, attr)
}
Loading
Loading