Skip to content

interp: keep compile time stores from changing objects that a runtime store exposes - #5908

Draft
davecheney wants to merge 1 commit into
tinygo-org:devfrom
davecheney:davecheney-interp-runtime-store-escape
Draft

davecheney wants to merge 1 commit into
tinygo-org:devfrom
davecheney:davecheney-interp-runtime-store-escape

Conversation

@davecheney

@davecheney davecheney commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

When interp cannot run a store at compile time, the store goes into runtime.initAll. Runtime code can then read the object that the stored pointer points to. Interp did not know this, so a later store to that object at compile time changed the value that runtime code read.

This program prints before: 2 with TinyGo 0.43.0 and dev, but before: 1 with Go.

package main

var value = 1
var pointer *int

func observe() {
	defer func() {}()
	println("before:", *pointer)
}

func init() {
	println("pointer:", &pointer)
	pointer = &value
	observe()
	value = 2
}

func main() {
	println("after:", value)
}

println(&pointer) runs at runtime, so interp marks pointer as external. Thus pointer = &value also runs at runtime, but interp did not mark value. Then value = 2 runs at compile time and changes the initializer of value to 2.

Why the fix is small

The general rule is that each object in the operands of a runtime instruction must be marked as external. Other runtime instructions, for example insertvalue and integer arithmetic on a ptrtoint constant, have the same problem. This PR applies the rule only to stores, so that the fix for the reported bug is small and easy to review. A stacked follow up PR applies the rule to all instructions that runAtRuntime makes. That PR has its own failing tests.

Notes for reviewers

  • interp/testdata/escape.ll stores a pointer at runtime and then stores to the object at compile time. Without the fix, the initializer of store.value changes to 2.
  • testdata/init.go does the same with atomic.Pointer.Store, because an atomic store runs at runtime. Without the fix TinyGo prints escape: 2. With the fix it prints escape: 1, the same as Go. The test does not print an address, so the output is deterministic.
  • go test ./interp and go test -run TestBuild/Host -only-current-os . pass on macOS arm64 with LLVM 22.
  • Code size on -target=cortex-m-qemu does not change for alias, atomic, calls, channel, float, gc, goroutines, interface, map, math, print, reflect, slice, sort, stdlib, string, structs, zeroalloc and init.

… store exposes

When interp cannot run a store at compile time, the store goes into runtime.initAll. Runtime code can then read the object that the stored pointer points to. Interp did not know this, so a later store to that object at compile time changed the value that runtime code read. TinyGo 0.43.0 and dev print a different value from Go for the program in the PR description.

The general rule is that each object in the operands of a runtime instruction must be marked as external. This change applies that rule only to stores, so that the fix for the reported bug is small and easy to review. A follow up PR applies the rule to the other instructions that runAtRuntime makes, for example insertvalue and ptrtoint arithmetic. That PR has its own failing test.
@davecheney
davecheney force-pushed the davecheney-interp-runtime-store-escape branch from a1e537a to c64532a Compare October 18, 2026 08:54
@davecheney davecheney changed the title interp: mark all operands of instructions that run at runtime interp: keep compile time stores from changing objects that a runtime store exposes Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant