When compiling a WASM plugin module (-buildmode=c-shared) with TinyGo 0.40.1 using its bundled Go 1.25.x toolchain, calling rsa.SignPKCS1v15 (which internally uses sha256.Sum256) causes a runtime panic:
panic: runtime error: nil pointer dereference
wasm stack trace:
main.runtime.runtimePanicAt(i32,i32)
main.runtime.nilPanic()
main.(*crypto/internal/fips140/sha256.Digest).Sum(i32,i32,i32,i32)
main.send() i32
The panic occurs inside the new crypto/internal/fips140/sha256 path that Go 1.24+ introduced as part of the FIPS 140-3 crypto module restructuring. This path appears to have uninitialized state when invoked through TinyGo's WASM c-shared build mode.
Key observations:
- c-shared mode panics, but command mode (
-target=wasi without -buildmode=c-shared) works fine with identical crypto code
- The panic is in
crypto/internal/fips140/sha256.Digest.Sum, not the old crypto/sha256 path. This is the Go 1.24+ FIPS internal restructuring
- Downgrading to Go 1.23.x (before the FIPS restructuring) avoids the panic entirely
This is likely related to #4777, which reported that crypto/sha256 became unimportable on the wasm target with Go 1.24+ due to crypto/internal/sysrand issues. The current issue is the runtime manifestation: even when the build succeeds, the FIPS 140 internal state appears uninitialized in c-shared/exported-function context.
Environment
| Component |
Version |
| TinyGo |
0.40.1 |
| Go (bundled) |
1.25.6 |
| LLVM |
20.1.1 |
| Target |
wasi |
| Build mode |
c-shared |
| WASM runtime |
Extism CLI (wazero-based) / also reproducible with wasmtime for command mode control |
| Build host |
darwin/arm64 |
$ tinygo version
tinygo version 0.40.1 darwin/arm64 (using go version go1.25.6 and LLVM version 20.1.1)
Reproducer
Repository: https://github.com/jeroenrinzema/tinygo-wasm-RS256
The reproducer contains two files with build tags to isolate the test cases:
Failing case: c-shared / plugin mode (-tags=reproplugin)
tinygo build -tags=reproplugin -target=wasi -buildmode=c-shared -opt=2 -no-debug -o repro_plugin.wasm .
extism call --wasi repro_plugin.wasm send
Output:
panic: runtime error: nil pointer dereference
Error: wasm error: unreachable
wasm stack trace:
main.runtime.runtimePanicAt(i32,i32)
main.runtime.nilPanic()
main.(*crypto/internal/fips140/sha256.Digest).Sum(i32,i32,i32,i32)
main.send() i32
returned non-zero exit code: 1
Passing case: command mode (-tags=reprocmd)
tinygo build -tags=reprocmd -target=wasi -opt=2 -no-debug -o repro_cmd.wasm .
wasmtime run repro_cmd.wasm
Output:
Minimal reproduction code (plugin mode)
//go:build reproplugin
package main
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
)
// Test RSA-1024 key in PKCS#8 DER format (base64-encoded, not sensitive)
const pkcs8PrivateKeyDERBase64 = "MIICdgIBADANBgkqhkiG9w0BAQEFAASC..."
//go:export send
func send() int32 {
der, _ := base64.StdEncoding.DecodeString(pkcs8PrivateKeyDERBase64)
keyAny, _ := x509.ParsePKCS8PrivateKey(der)
rsaKey := keyAny.(*rsa.PrivateKey)
signingInput := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzEwMDAwMDAwfQ"
digest := sha256.Sum256([]byte(signingInput)) // <-- panics here internally
_, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, crypto.SHA256, digest[:])
if err != nil {
panic(err)
}
return 0
}
func main() {}
The nil panic in crypto/internal/fips140/sha256.Digest.Sum suggests that the Digest struct's internal state (likely the hash function table or init vector) is not properly initialized when called from an exported WASM function in c-shared mode.
Go 1.24 moved SHA-256 (and other crypto primitives) behind crypto/internal/fips140/... as part of the FIPS 140-3 validation effort. These internal packages may rely on init() functions or module-level state that TinyGo doesn't fully wire up in the c-shared WASM execution path, particularly since _start/main() initialization may not run before exported functions are called.
The fact that command mode works (where main() runs first, executing all init() chains) but c-shared mode panics (where the exported function send() is called directly) strongly points to missing initialization of the FIPS 140 crypto internals in the c-shared startup path.
Workaround
We worked around this downstream by avoiding the affected stdlib paths entirely in WASM modules, using a local SHA-256 implementation and a local RSA PKCS#1 v1.5 signing helper. This avoids the FIPS 140 internal code path. We would prefer to use standard library crypto.
When compiling a WASM plugin module (
-buildmode=c-shared) with TinyGo 0.40.1 using its bundled Go 1.25.x toolchain, callingrsa.SignPKCS1v15(which internally usessha256.Sum256) causes a runtime panic:The panic occurs inside the new
crypto/internal/fips140/sha256path that Go 1.24+ introduced as part of the FIPS 140-3 crypto module restructuring. This path appears to have uninitialized state when invoked through TinyGo's WASM c-shared build mode.Key observations:
-target=wasiwithout-buildmode=c-shared) works fine with identical crypto codecrypto/internal/fips140/sha256.Digest.Sum, not the oldcrypto/sha256path. This is the Go 1.24+ FIPS internal restructuringThis is likely related to #4777, which reported that
crypto/sha256became unimportable on thewasmtarget with Go 1.24+ due tocrypto/internal/sysrandissues. The current issue is the runtime manifestation: even when the build succeeds, the FIPS 140 internal state appears uninitialized in c-shared/exported-function context.Environment
wasic-sharedReproducer
Repository: https://github.com/jeroenrinzema/tinygo-wasm-RS256
The reproducer contains two files with build tags to isolate the test cases:
Failing case: c-shared / plugin mode (
-tags=reproplugin)tinygo build -tags=reproplugin -target=wasi -buildmode=c-shared -opt=2 -no-debug -o repro_plugin.wasm . extism call --wasi repro_plugin.wasm sendOutput:
Passing case: command mode (
-tags=reprocmd)tinygo build -tags=reprocmd -target=wasi -opt=2 -no-debug -o repro_cmd.wasm . wasmtime run repro_cmd.wasmOutput:
Minimal reproduction code (plugin mode)
The nil panic in
crypto/internal/fips140/sha256.Digest.Sumsuggests that theDigeststruct's internal state (likely the hash function table or init vector) is not properly initialized when called from an exported WASM function in c-shared mode.Go 1.24 moved SHA-256 (and other crypto primitives) behind
crypto/internal/fips140/...as part of the FIPS 140-3 validation effort. These internal packages may rely oninit()functions or module-level state that TinyGo doesn't fully wire up in the c-shared WASM execution path, particularly since_start/main()initialization may not run before exported functions are called.The fact that command mode works (where
main()runs first, executing allinit()chains) but c-shared mode panics (where the exported functionsend()is called directly) strongly points to missing initialization of the FIPS 140 crypto internals in the c-shared startup path.Workaround
We worked around this downstream by avoiding the affected stdlib paths entirely in WASM modules, using a local SHA-256 implementation and a local RSA PKCS#1 v1.5 signing helper. This avoids the FIPS 140 internal code path. We would prefer to use standard library crypto.