Skip to content

interp: improvements to interpret FIPS code - #5903

Merged
deadprogram merged 4 commits into
tinygo-org:devfrom
jakebailey:interp-init-evaluation
Oct 22, 2026
Merged

deadprogram merged 4 commits into
tinygo-org:devfrom
jakebailey:interp-init-evaluation

Conversation

@jakebailey

Copy link
Copy Markdown
Member

Fixes #5835

See individual commits for more info.

This is an alternative to #5883 that doesn't involve wholesale replacing the godebug package.

Combined with #5885, it should still let GODEBUG work but still eliminate FIPS stuff, since the FIPS stuff is determined by package initialization and is not affected by later runtime env calls, so, can be interp'd. Phew!

@jakebailey
jakebailey requested a review from davecheney October 17, 2026 01:49
@davecheney

Copy link
Copy Markdown
Contributor

@jakebailey TYVM for working on this. Adding general defer support to interp is useful independent of the FIPS yak shaving.

My agent has found one possible problem related to rollbacks

package main

var data = [4]byte{'A', 'B', 'C', 'D'}
var done = shift()

func shift() int {
	defer func() {}()
	copy(data[1:], data[:3])
	println("shift")
	return 1
}

func main() {
	println(string(data[:]), done)
}

expected

shift
AABC 1

with this PR

shift
AAAB 1

@davecheney

Copy link
Copy Markdown
Contributor

@jakebailey oops, it looks like the copy bug has existed for a while, its unrelated to your improvement so I sent #5904

davecheney added a commit to davecheney/tinygo that referenced this pull request Oct 17, 2026
… memory view

A value that a function loads during package initialization must not change when the caller later stores to the same object. Since f5bd750, a partial store changes the buffer of an object in place when the current memory view already owns that object. 3a0d724 made load copy the loaded slice in that case, but it checked only the current view. When a callee loads part of an object that its caller owns, the callee has its own view, so the returned value still shares the caller's buffer, and the caller's next partial store changes it. For example, an init function that sets a struct field to 1, reads it through a helper function, sets it to 2, and then copies the value it read gives 2 instead of 1. This needs no defer and no revert, and the 0.43.0 release has the same problem.

Objects in the runner's base table are never changed in place, so loads from objects that no view owns still return a slice of the buffer without a copy.

The problem was found during review of tinygo-org#5903. This change does not depend on tinygo-org#5903 or on the getWritable fix on dfc/interp-getwritable-rollback.
deadprogram pushed a commit that referenced this pull request Oct 17, 2026
… memory view

A value that a function loads during package initialization must not change when the caller later stores to the same object. Since f5bd750, a partial store changes the buffer of an object in place when the current memory view already owns that object. 3a0d724 made load copy the loaded slice in that case, but it checked only the current view. When a callee loads part of an object that its caller owns, the callee has its own view, so the returned value still shares the caller's buffer, and the caller's next partial store changes it. For example, an init function that sets a struct field to 1, reads it through a helper function, sets it to 2, and then copies the value it read gives 2 instead of 1. This needs no defer and no revert, and the 0.43.0 release has the same problem.

Objects in the runner's base table are never changed in place, so loads from objects that no view owns still return a slice of the buffer without a copy.

The problem was found during review of #5903. This change does not depend on #5903 or on the getWritable fix on dfc/interp-getwritable-rollback.
davecheney added a commit to davecheney/tinygo that referenced this pull request Oct 17, 2026
…er is reverted

When interp reverts a package initializer and runs it at run time, a memcpy or memmove that interp already evaluated must not change the initial value of the destination. getWritable put its clone in the runner's global object table instead of in the current memory view, so the copy stayed in the initial value after the revert, and the initializer then did the copy again at run time. For example, an initializer that does copy(data[1:], data[:3]) on "ABCD" and then calls println in a function with a defer gives "AAAB" instead of "AABC". The same program gives the correct result when it runs from main, and the 0.43.0 release has the same problem. getWritable has done this since the interp rewrite in 30df912.

The problem was found during review of tinygo-org#5903, which lets interp evaluate functions with defer. This change does not depend on tinygo-org#5903.
davecheney added a commit to davecheney/tinygo that referenced this pull request Oct 17, 2026
When interp leaves a call for run time, the objects that the call can reach must be marked so that later loads and stores of them are not done at compile time. For a global variable, markExternal followed the pointers in the LLVM initializer of the global. But interp writes its results to those initializers only after it has finished, so a pointer that was stored to a global at compile time was not followed. For example, an init function that sets a global to the address of another global, starts a goroutine with the address of the first global, waits for the goroutine on a channel, and then reads the second global gives the old value. The goroutine changed the value through the pointer before the read. The program has no data race, and the 0.43.0 release has the same problem at all optimization levels.

markExternal now follows the pointers in the current value of the object. This also reaches heap objects that interp allocated and that do not have an LLVM global yet. A global with no known value at compile time still uses its LLVM initializer.

The problem was found during review of tinygo-org#5903. This change does not depend on tinygo-org#5903 or on the other interp fixes on dfc/interp-getwritable-rollback and dfc/interp-load-alias.
davecheney added a commit that referenced this pull request Oct 19, 2026
…er is reverted (#5904)

When interp reverts a package initializer and runs it at run time, a memcpy or memmove that interp already evaluated must not change the initial value of the destination. getWritable put its clone in the runner's global object table instead of in the current memory view, so the copy stayed in the initial value after the revert, and the initializer then did the copy again at run time. For example, an initializer that does copy(data[1:], data[:3]) on "ABCD" and then calls println in a function with a defer gives "AAAB" instead of "AABC". The same program gives the correct result when it runs from main, and the 0.43.0 release has the same problem. getWritable has done this since the interp rewrite in 30df912.

The problem was found during review of #5903, which lets interp evaluate functions with defer. This change does not depend on #5903.
@davecheney

Copy link
Copy Markdown
Contributor

@jakebailey #5905 has landed, please rebase.

mklocdev pushed a commit to mklocdev/tinygo that referenced this pull request Oct 21, 2026
…er is reverted (tinygo-org#5904)

When interp reverts a package initializer and runs it at run time, a memcpy or memmove that interp already evaluated must not change the initial value of the destination. getWritable put its clone in the runner's global object table instead of in the current memory view, so the copy stayed in the initial value after the revert, and the initializer then did the copy again at run time. For example, an initializer that does copy(data[1:], data[:3]) on "ABCD" and then calls println in a function with a defer gives "AAAB" instead of "AABC". The same program gives the correct result when it runs from main, and the 0.43.0 release has the same problem. getWritable has done this since the interp rewrite in 30df912.

The problem was found during review of tinygo-org#5903, which lets interp evaluate functions with defer. This change does not depend on tinygo-org#5903.
Zeroing local storage should not force otherwise constant initializers
to run at startup. Evaluate nonvolatile fills when their destination is
known and has not escaped.
Atomic state can be initialized before it becomes visible to other
execution contexts. Evaluate operations on known storage while retaining
runtime accesses once that storage has escaped.
These runtime helpers no longer use a separate interpreter map model.
Keeping them opaque prevents constant callers from being evaluated.
@jakebailey
jakebailey force-pushed the interp-init-evaluation branch from 575abf3 to 90dd4df Compare October 22, 2026 00:19

@davecheney davecheney left a comment

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.

LGTM with one suggestion

Comment thread interp/interpreter.go Resolved
Defers currently keep otherwise constant initialization at runtime.
Evaluate complete calls without runtime recovery bookkeeping, and keep
the original call whenever any runtime execution is still required.
@jakebailey
jakebailey force-pushed the interp-init-evaluation branch from 90dd4df to 9cdd16d Compare October 22, 2026 01:19
@davecheney davecheney added this to the 0.44 milestone Oct 22, 2026
@deadprogram

Copy link
Copy Markdown
Member

Thanks for working on this @jakebailey and to @davecheney for all the reviewing. Now merging.

@deadprogram
deadprogram merged commit ab64fff into tinygo-org:dev Oct 22, 2026
31 checks passed
@jakebailey
jakebailey deleted the interp-init-evaluation branch October 22, 2026 06:05
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.

initAll retains crypto/internal/fips140/sha3.keccakF1600Generic from an unused HTTP import

3 participants