From 931c4764ae0526a245e6e6d39dd1ca726d99dcc0 Mon Sep 17 00:00:00 2001 From: yohimik Date: Sun, 30 Aug 2026 15:42:40 +0400 Subject: [PATCH 1/2] runtime: implement weak.runtime_makeStrongFromWeak The runtime had weak.runtime_registerWeakPointer but not its counterpart, so a program that reads a weak pointer back did not link. crypto/tls does this in the certificate cache that it keeps behind a weak.Pointer. Weak pointers are not weak here. registerWeakPointer returns the pointer that it got, so the value it refers to stays and the way back to a strong pointer is the identity too. weak.Pointer.Value thus never reports a collected value, which the documented contract permits. testdata/weak.go does not link on the current dev branch and prints the expected value with this change. --- main_test.go | 1 + src/runtime/runtime.go | 7 +++++++ testdata/weak.go | 21 +++++++++++++++++++++ testdata/weak.txt | 1 + 4 files changed, 30 insertions(+) create mode 100644 testdata/weak.go create mode 100644 testdata/weak.txt diff --git a/main_test.go b/main_test.go index 17b2b5a572..a42afd2ee5 100644 --- a/main_test.go +++ b/main_test.go @@ -108,6 +108,7 @@ func TestBuild(t *testing.T) { } if minor >= 24 { tests = append(tests, "typealias.go") + tests = append(tests, "weak.go") } if *testTarget != "" { diff --git a/src/runtime/runtime.go b/src/runtime/runtime.go index 6ad14fe101..2a8b016731 100644 --- a/src/runtime/runtime.go +++ b/src/runtime/runtime.go @@ -139,6 +139,13 @@ func registerWeakPointer(ptr unsafe.Pointer) unsafe.Pointer { return ptr } +//go:linkname makeStrongFromWeak weak.runtime_makeStrongFromWeak +func makeStrongFromWeak(ptr unsafe.Pointer) unsafe.Pointer { + // Weak pointers are not weak here. registerWeakPointer above returns the + // pointer that it got, so the value stays and this is the identity too. + return ptr +} + var godebugUpdate func(string, string) //go:linkname godebug_setUpdate internal/godebug.setUpdate diff --git a/testdata/weak.go b/testdata/weak.go new file mode 100644 index 0000000000..0cc6d79e85 --- /dev/null +++ b/testdata/weak.go @@ -0,0 +1,21 @@ +package main + +import ( + "runtime" + "weak" +) + +type value struct { + n int +} + +func main() { + v := &value{n: 42} + p := weak.Make(v) + if got := p.Value(); got == nil { + println("weak pointer lost its value") + } else { + println("weak value:", got.n) + } + runtime.KeepAlive(v) +} diff --git a/testdata/weak.txt b/testdata/weak.txt new file mode 100644 index 0000000000..e89078851e --- /dev/null +++ b/testdata/weak.txt @@ -0,0 +1 @@ +weak value: 42 From 4b2c48c2e728a95ec1c1883f9cd4da84be62f4e2 Mon Sep 17 00:00:00 2001 From: yohimik Date: Sun, 30 Aug 2026 15:45:46 +0400 Subject: [PATCH 2/2] loader: use the real crypto/tls on hosted linux and darwin TinyGo replaces crypto/tls with a stub whose handshake does nothing, so a program that dials https gets a plaintext connection behind the TLS API. That stub is correct for a target with no OS below it, which has neither the code size for a full TLS implementation nor usually a socket to speak it over. Hosted linux and macOS have both, and there the crypto/tls of the Go standard library compiles and runs. Make the override conditional. Without an entry in the map, crypto/tls falls under the "crypto/" merge, which links the package of the standard library into the synthetic GOROOT. GOOS alone cannot decide this, because a baremetal target reports GOOS=linux, so the build tags decide as well. The goroot cache key is a hash of the merge links, so the two variants get separate cache entries. testdata/hostcryptotls.go does a TLS handshake over an in-memory pipe with a certificate that it makes at run time. On the current dev branch it prints "negotiated an unexpected version: 0", because the stub does no handshake. With this change the handshake completes, the data goes through, and a client that does not trust the certificate refuses it. loader/goroot_test.go covers the targets that keep the stub, the baremetal one that reports GOOS=linux included. --- loader/goroot.go | 31 ++++++++++-- loader/goroot_test.go | 27 ++++++++++ loader/loader.go | 2 +- main_test.go | 13 +++++ testdata/hostcryptotls.go | 100 +++++++++++++++++++++++++++++++++++++ testdata/hostcryptotls.txt | 2 + 6 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 loader/goroot_test.go create mode 100644 testdata/hostcryptotls.go create mode 100644 testdata/hostcryptotls.txt diff --git a/loader/goroot.go b/loader/goroot.go index 0aab0a0e13..54a3f096af 100644 --- a/loader/goroot.go +++ b/loader/goroot.go @@ -45,7 +45,7 @@ func GetCachedGoroot(config *compileopts.Config) (string, error) { } // Find the overrides needed for the goroot. - overrides := pathsToOverride(config.GoMinorVersion, needsSyscallPackage(config.BuildTags())) + overrides := pathsToOverride(config.GoMinorVersion, needsSyscallPackage(config.BuildTags()), needsTLSStubPackage(config.GOOS(), config.BuildTags())) // Resolve the merge links within the goroot. merge, err := listGorootMergeLinks(goroot, tinygoroot, overrides) @@ -225,14 +225,33 @@ func needsSyscallPackage(buildTags []string) bool { return false } +// needsTLSStubPackage returns whether the crypto/tls package should be +// overridden with the TinyGo stub version, whose handshake is a no-op. A target +// with no OS below it has neither the code size for a full TLS implementation +// nor usually a socket to speak it over. +// +// Hosted linux and macOS have both, so they use the real crypto/tls of the Go +// standard library. GOOS alone cannot decide this, because a baremetal target +// reports GOOS=linux, so the build tags decide as well. +func needsTLSStubPackage(goos string, buildTags []string) bool { + if goos != "linux" && goos != "darwin" { + return true + } + for _, tag := range buildTags { + if tag == "baremetal" || tag == "nintendoswitch" || tag == "tinygo.wasm" || tag == "wasm_unknown" { + return true + } + } + return false +} + // The boolean indicates whether to merge the subdirs. True means merge, false // means use the TinyGo version. -func pathsToOverride(goMinor int, needsSyscallPackage bool) map[string]bool { +func pathsToOverride(goMinor int, needsSyscallPackage, needsTLSStubPackage bool) map[string]bool { paths := map[string]bool{ "": true, "crypto/": true, "crypto/rand/": false, - "crypto/tls/": false, "crypto/x509/": true, "crypto/x509/internal/": true, "crypto/x509/internal/macos/": false, @@ -263,6 +282,12 @@ func pathsToOverride(goMinor int, needsSyscallPackage bool) map[string]bool { "unique/": false, } + if needsTLSStubPackage { + // Without this entry crypto/tls falls under the "crypto/" merge above, + // which links in the package of the standard library. + paths["crypto/tls/"] = false + } + if goMinor >= 19 { paths["crypto/internal/"] = true paths["crypto/internal/boring/"] = true diff --git a/loader/goroot_test.go b/loader/goroot_test.go new file mode 100644 index 0000000000..d8d6cf0bb8 --- /dev/null +++ b/loader/goroot_test.go @@ -0,0 +1,27 @@ +package loader + +import "testing" + +func TestNeedsTLSStubPackage(t *testing.T) { + tests := []struct { + name string + goos string + buildTags []string + want bool + }{ + {"hosted linux", "linux", []string{"linux", "amd64"}, false}, + {"hosted darwin", "darwin", []string{"darwin", "arm64"}, false}, + {"windows", "windows", []string{"windows", "amd64"}, true}, + {"wasip1", "wasip1", []string{"wasip1", "tinygo.wasm"}, true}, + // A baremetal target reports GOOS=linux, so the build tags have to + // keep the stub for it. + {"baremetal", "linux", []string{"linux", "arm", "baremetal"}, true}, + {"nintendoswitch", "linux", []string{"linux", "nintendoswitch"}, true}, + {"wasm_unknown", "linux", []string{"linux", "wasm_unknown"}, true}, + } + for _, test := range tests { + if got := needsTLSStubPackage(test.goos, test.buildTags); got != test.want { + t.Errorf("%s: wanted %v, got %v", test.name, test.want, got) + } + } +} diff --git a/loader/loader.go b/loader/loader.go index 5696abd065..7435e737e7 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -278,7 +278,7 @@ func (p *Program) getOriginalPath(path string) string { originalPath = realgorootPath } maybeInTinyGoRoot := false - for prefix := range pathsToOverride(p.config.GoMinorVersion, needsSyscallPackage(p.config.BuildTags())) { + for prefix := range pathsToOverride(p.config.GoMinorVersion, needsSyscallPackage(p.config.BuildTags()), needsTLSStubPackage(p.config.GOOS(), p.config.BuildTags())) { if runtime.GOOS == "windows" { prefix = strings.ReplaceAll(prefix, "/", "\\") } diff --git a/main_test.go b/main_test.go index a42afd2ee5..6a96b8e541 100644 --- a/main_test.go +++ b/main_test.go @@ -284,6 +284,19 @@ func TestTimerStopResetRace(t *testing.T) { runTest("timer_stop_reset_race.go", optionsFromTarget("", sema), t, nil, nil) } +// TestHostCryptoTLS checks that a hosted target gets the real crypto/tls and +// not the stub, whose handshake does nothing. Only linux and macOS do. +func TestHostCryptoTLS(t *testing.T) { + t.Parallel() + + switch runtime.GOOS { + case "darwin", "linux": + default: + t.Skipf("host GOOS %s keeps the crypto/tls stub", runtime.GOOS) + } + runTest("hostcryptotls.go", optionsFromTarget("", sema), t, nil, nil) +} + func TestESP32QEMU(t *testing.T) { t.Parallel() diff --git a/testdata/hostcryptotls.go b/testdata/hostcryptotls.go new file mode 100644 index 0000000000..462f19971c --- /dev/null +++ b/testdata/hostcryptotls.go @@ -0,0 +1,100 @@ +package main + +// A real TLS handshake over an in-memory pipe. The stub crypto/tls has a +// handshake that does nothing, so it cannot pass this. + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "io" + "math/big" + "net" + "time" +) + +func main() { + cert, pool := selfSigned() + + // A client that trusts the certificate completes the handshake and + // exchanges data. + client, server := net.Pipe() + go serve(server, cert) + conn := tls.Client(client, &tls.Config{RootCAs: pool, ServerName: "tinygo.test"}) + if err := conn.Handshake(); err != nil { + println("handshake failed:", err.Error()) + return + } + if v := conn.ConnectionState().Version; v < tls.VersionTLS12 { + println("negotiated an unexpected version:", v) + return + } + if _, err := conn.Write([]byte("ping")); err != nil { + println("write failed:", err.Error()) + return + } + buf := make([]byte, 4) + if _, err := io.ReadFull(conn, buf); err != nil { + println("read failed:", err.Error()) + return + } + println("got:", string(buf)) + conn.Close() + + // A client that does not trust the certificate must refuse it. + client, server = net.Pipe() + go serve(server, cert) + conn = tls.Client(client, &tls.Config{ServerName: "tinygo.test"}) + if err := conn.Handshake(); err == nil { + println("an unknown certificate was accepted") + return + } + conn.Close() + println("unknown certificate refused") +} + +func serve(conn net.Conn, cert tls.Certificate) { + server := tls.Server(conn, &tls.Config{Certificates: []tls.Certificate{cert}}) + if err := server.Handshake(); err != nil { + conn.Close() + return + } + buf := make([]byte, 4) + if _, err := io.ReadFull(server, buf); err != nil { + server.Close() + return + } + server.Write([]byte("pong")) +} + +func selfSigned() (tls.Certificate, *x509.CertPool) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + panic(err) + } + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "tinygo.test"}, + DNSNames: []string{"tinygo.test"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + } + der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + if err != nil { + panic(err) + } + leaf, err := x509.ParseCertificate(der) + if err != nil { + panic(err) + } + pool := x509.NewCertPool() + pool.AddCert(leaf) + return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key, Leaf: leaf}, pool +} diff --git a/testdata/hostcryptotls.txt b/testdata/hostcryptotls.txt new file mode 100644 index 0000000000..bc5c8865cf --- /dev/null +++ b/testdata/hostcryptotls.txt @@ -0,0 +1,2 @@ +got: pong +unknown certificate refused