Repository navigation
ssa: describe audited runtime function contracts #2510
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) | ||
| } |
| 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) { | ||
| contract, ok := runtimeContracts[name] | ||
| if !ok { | ||
| return | ||
| } | ||
| add := func(index int, name string, value uint64) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor readability: the closure parameter |
||
| 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) | ||
| } | ||
There was a problem hiding this comment.
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.
addRuntimeAttributeskeys purely on the resolved name and then blindly appliespointer(2, "writeonly"),pointer(3, "readonly"), etc. Thereflect.typedmemmoveentry is a barepkg.namereachable by the realreflectpackage; today its signature matches themovecontract, 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.