From feb121e14579368611e288fadcb6aea5f3e62d62 Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Thu, 22 Oct 2026 19:32:38 +0200 Subject: [PATCH 01/10] wit: fix loader paths and runtime cleanup Normalize guest paths so Windows separators do not break WIT loading. Close the embedded runtime after each load to release directory handles. --- wit/load.go | 5 +-- wit/load_test.go | 70 +++++++++++++++++++++++++++++++++++++++++ wit/load_tinygo_test.go | 25 +++++++++++++++ 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 wit/load_test.go create mode 100644 wit/load_tinygo_test.go diff --git a/wit/load.go b/wit/load.go index 342bc994..67da87c2 100644 --- a/wit/load.go +++ b/wit/load.go @@ -61,9 +61,9 @@ func loadWIT(path string, reader io.Reader) (*Resolve, error) { if path != "" { path = strings.TrimPrefix(path, "./") - args = append(args, path) + args = append(args, filepath.ToSlash(path)) dir := filepath.Dir(path) - fsMap[dir] = os.DirFS(dir) + fsMap[filepath.ToSlash(dir)] = os.DirFS(dir) } else { stdin = reader } @@ -71,6 +71,7 @@ func loadWIT(path string, reader io.Reader) (*Resolve, error) { if err != nil { return nil, err } + defer wasmTools.Close(ctx) stdout := &bytes.Buffer{} stderr := &bytes.Buffer{} err = wasmTools.Run(ctx, stdin, stdout, stderr, fsMap, args...) diff --git a/wit/load_test.go b/wit/load_test.go new file mode 100644 index 00000000..e92e92fe --- /dev/null +++ b/wit/load_test.go @@ -0,0 +1,70 @@ +//go:build !tinygo + +package wit + +import ( + "os" + "path/filepath" + "testing" +) + +func TestLoadWITPaths(t *testing.T) { + root := t.TempDir() + file := filepath.Join(root, "nested", "example.wit") + if err := os.MkdirAll(filepath.Dir(file), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(file, []byte("package example:paths; world example {}"), 0o644); err != nil { + t.Fatal(err) + } + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + rel, err := filepath.Rel(cwd, file) + if err != nil { + t.Fatal(err) + } + for _, input := range []struct { + name string + path string + }{ + {"absolute", file}, + {"absolute slash", filepath.ToSlash(file)}, + {"relative", rel}, + {"relative slash", filepath.ToSlash(rel)}, + } { + t.Run(input.name, func(t *testing.T) { + resolved, err := LoadWIT(input.path) + if err != nil { + t.Fatal(err) + } + if len(resolved.Worlds) != 1 || resolved.Worlds[0].Name != "example" { + t.Fatalf("resolved worlds: %v", resolved.Worlds) + } + }) + } +} + +func TestLoadWITDirectoryDependencies(t *testing.T) { + root := t.TempDir() + dependency := filepath.Join(root, "deps", "dep") + if err := os.MkdirAll(dependency, 0o755); err != nil { + t.Fatal(err) + } + for name, text := range map[string]string{ + filepath.Join(root, "main.wit"): "package example:paths; world example { import example:dep/api; }", + filepath.Join(dependency, "api.wit"): "package example:dep; interface api { ping: func(); }", + } { + if err := os.WriteFile(name, []byte(text), 0o644); err != nil { + t.Fatal(err) + } + } + resolved, err := LoadWIT(root) + if err != nil { + t.Fatal(err) + } + if len(resolved.Packages) != 2 || len(resolved.Worlds) != 1 || resolved.Worlds[0].Name != "example" { + t.Fatalf("resolved packages/worlds: %v / %v", resolved.Packages, resolved.Worlds) + } +} diff --git a/wit/load_tinygo_test.go b/wit/load_tinygo_test.go new file mode 100644 index 00000000..1cd78c25 --- /dev/null +++ b/wit/load_tinygo_test.go @@ -0,0 +1,25 @@ +//go:build tinygo + +package wit + +import ( + "strings" + "testing" +) + +func TestLoadWITUnsupportedBackend(t *testing.T) { + for _, input := range []struct { + name string + load func() (*Resolve, error) + }{ + {"file", func() (*Resolve, error) { return LoadWIT("example.wit") }}, + {"reader", func() (*Resolve, error) { return DecodeWIT(strings.NewReader("package example:paths;")) }}, + } { + t.Run(input.name, func(t *testing.T) { + resolved, err := input.load() + if resolved != nil || err == nil || !strings.Contains(err.Error(), "wasm-tools disabled under TinyGo") { + t.Fatalf("expected disabled-backend error, got %v, %v", resolved, err) + } + }) + } +} From c0fdd9e5431b0076cee9b3231d65eb97adc54948 Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Sat, 24 Oct 2026 21:29:06 +0200 Subject: [PATCH 02/10] fix(gen): stop module lookup at Windows roots Compare each directory with its parent to detect filesystem roots. The slash-only check looped forever at Windows drive roots when no go.mod existed. Keep nested package suffixes while ascending. --- internal/go/gen/module.go | 13 ++++--------- internal/go/gen/module_test.go | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/internal/go/gen/module.go b/internal/go/gen/module.go index 1553a6a4..4b781bc6 100644 --- a/internal/go/gen/module.go +++ b/internal/go/gen/module.go @@ -35,17 +35,12 @@ func PackagePath(dir string) (string, error) { file = filepath.Join(dir, "go.mod") info, err := os.Stat(file) if err != nil { - if dir == "/" { + parent := filepath.Dir(dir) + if parent == dir { return "", errors.New("unable to locate a go.mod file") } - // Pop up to parent dir - var rest string - dir, rest = filepath.Split(dir) - if dir == "" { - return "", errors.New("unable to locate a go.mod file") - } - dir = filepath.Clean(dir) - subdirs = path.Join(rest, subdirs) + subdirs = path.Join(filepath.Base(dir), subdirs) + dir = parent continue } if info.IsDir() { diff --git a/internal/go/gen/module_test.go b/internal/go/gen/module_test.go index 227fb496..125ebfa6 100644 --- a/internal/go/gen/module_test.go +++ b/internal/go/gen/module_test.go @@ -2,6 +2,7 @@ package gen import ( "os" + "path/filepath" "testing" "go.bytecodealliance.org/internal/relpath" @@ -28,3 +29,21 @@ func TestPackagePath(t *testing.T) { t.Errorf("PackagePath(%q): expected error, got nil", tmp) } } + +func TestPackagePathNestedModule(t *testing.T) { + root := t.TempDir() + if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module example.com/plugin\n\ngo 1.26.0\n"), 0o600); err != nil { + t.Fatal(err) + } + nested := filepath.Join(root, "internal", "bindings") + if err := os.MkdirAll(nested, 0o700); err != nil { + t.Fatal(err) + } + got, err := PackagePath(nested) + if err != nil { + t.Fatal(err) + } + if want := "example.com/plugin/internal/bindings"; got != want { + t.Errorf("PackagePath(%q) = %q, want %q", nested, got, want) + } +} From 8ff57fada0f57c2c9cc164ebef8576fd5f1314f7 Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Sat, 24 Oct 2026 21:36:16 +0200 Subject: [PATCH 03/10] test: fix Windows paths and permission checks Build fixture paths with filepath.Rel and filepath.Join. Test Windows directory creation permissions with an ACL and compare created permissions with a control directory on the same filesystem. Remove empty test directories before TinyGo cleanup. --- internal/witcli/dir_test.go | 67 +++++++++++++++++++++++++++++++++--- wit/bindgen/testdata_test.go | 9 +++-- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/internal/witcli/dir_test.go b/internal/witcli/dir_test.go index c3ad69c2..1a81a3c0 100644 --- a/internal/witcli/dir_test.go +++ b/internal/witcli/dir_test.go @@ -4,6 +4,7 @@ import ( "errors" "io/fs" "os" + "os/exec" "path/filepath" "runtime" "strings" @@ -14,6 +15,7 @@ func TestFindOrCreateDirFind(t *testing.T) { temp := t.TempDir() dir := filepath.Join(temp, "existing") + cleanupEmptyDirs(t, dir, temp) if err := os.Mkdir(dir, os.ModeDir|os.ModePerm); err != nil { t.Fatal(err) } @@ -48,8 +50,16 @@ func TestFindOrCreateDirFind(t *testing.T) { func TestFindOrCreateDirCreate(t *testing.T) { temp := t.TempDir() - + control := filepath.Join(temp, "control") dir := filepath.Join(temp, "new") + cleanupEmptyDirs(t, dir, control, temp) + if err := os.Mkdir(control, 0o755); err != nil { + t.Fatal(err) + } + controlInfo, err := os.Stat(control) + if err != nil { + t.Fatal(err) + } if _, err := os.Stat(dir); !os.IsNotExist(err) { t.Errorf("Expected %s to not exist, but it did.", dir) @@ -59,6 +69,16 @@ func TestFindOrCreateDirCreate(t *testing.T) { if err != nil { t.Fatal(err) } + if info == nil || !info.IsDir() { + t.Fatalf("FindOrCreateDir returned non-directory information: %v", info) + } + createdInfo, err := os.Stat(dir) + if err != nil { + t.Fatal(err) + } + if !createdInfo.IsDir() { + t.Fatal("FindOrCreateDir did not create a directory") + } // Skip remaining tests on incompatible platforms if runtime.Compiler == "tinygo" || strings.Contains(runtime.GOARCH, "wasm") { @@ -66,12 +86,25 @@ func TestFindOrCreateDirCreate(t *testing.T) { } gotPerm := info.Mode().Perm() - wantPerm := os.FileMode(0o755) + // Compare filesystem-applied permissions, including umask and Windows mode mapping. + wantPerm := controlInfo.Mode().Perm() if gotPerm != wantPerm { - t.Errorf("FindOrCreateDirExisting returned directory with file permissions %v; want %v", gotPerm, wantPerm) + t.Errorf("FindOrCreateDir returned directory with file permissions %v; want %v", gotPerm, wantPerm) } } +// TinyGo's Windows RemoveAll cannot enumerate directories; remove owned leaves first. +func cleanupEmptyDirs(t *testing.T, paths ...string) { + t.Helper() + t.Cleanup(func() { + for _, path := range paths { + if err := os.Remove(path); err != nil && !errors.Is(err, fs.ErrNotExist) { + t.Errorf("remove test directory %s: %v", path, err) + } + } + }) +} + func TestFindOrCreateDirPermissionDenied(t *testing.T) { // Skip test on incompatible platforms if runtime.Compiler == "tinygo" || strings.Contains(runtime.GOARCH, "wasm") { @@ -81,14 +114,38 @@ func TestFindOrCreateDirPermissionDenied(t *testing.T) { temp := t.TempDir() protected := filepath.Join(temp, "protected") - if err := os.Mkdir(protected, os.FileMode(0o400)); err != nil { + if err := os.Mkdir(protected, 0o755); err != nil { t.Fatal(err) } + if runtime.GOOS == "windows" { + // Windows directory creation uses ACLs; chmod does not remove this permission. + icacls := filepath.Join(os.Getenv("SystemRoot"), "System32", "icacls.exe") + if output, err := exec.Command(icacls, protected, "/deny", "*S-1-1-0:(AD)").CombinedOutput(); err != nil { + t.Fatalf("deny subdirectory creation: %v\n%s", err, output) + } + t.Cleanup(func() { + if output, err := exec.Command(icacls, protected, "/remove:d", "*S-1-1-0").CombinedOutput(); err != nil { + t.Errorf("restore directory permissions: %v\n%s", err, output) + } + }) + } else { + if err := os.Chmod(protected, 0o400); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := os.Chmod(protected, 0o755); err != nil { + t.Errorf("restore directory permissions: %v", err) + } + }) + } dir := filepath.Join(protected, "permission-denied") - _, err := FindOrCreateDir(dir) + info, err := FindOrCreateDir(dir) if !errors.Is(err, fs.ErrPermission) { t.Errorf("Expected creation within protected directory to fail, but it did not: %v", err) } + if info != nil { + t.Errorf("failed creation returned file information: %v", info) + } } diff --git a/wit/bindgen/testdata_test.go b/wit/bindgen/testdata_test.go index 5073c486..3ca11773 100644 --- a/wit/bindgen/testdata_test.go +++ b/wit/bindgen/testdata_test.go @@ -9,7 +9,6 @@ import ( "io/fs" "os" "os/exec" - "path" "path/filepath" "strings" "sync" @@ -86,7 +85,7 @@ func validateGeneratedGo(t *testing.T, res *wit.Resolve, origin string) { return } - dir := path.Join(generatedPath, origin) + dir := filepath.Join(generatedPath, origin) err := os.MkdirAll(dir, fs.ModePerm) if err != nil { t.Error(err) @@ -218,8 +217,12 @@ func TestGenerateTestdata(t *testing.T) { return } err := loadTestdata(func(path string, res *wit.Resolve) error { + relative, err := filepath.Rel(testdataPath, path) + if err != nil { + return err + } t.Run(path, func(t *testing.T) { - origin := strings.TrimSuffix(strings.TrimPrefix(path, testdataPath), ".wit.json") + origin := strings.TrimSuffix(relative, ".wit.json") validateGeneratedGo(t, res, origin) }) return nil From ef8688c794ac1634b925e029110a0080cf8cd945 Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Sat, 24 Oct 2026 21:47:25 +0200 Subject: [PATCH 04/10] test: clean generator fixtures on Windows TinyGo --- internal/go/gen/module_test.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/internal/go/gen/module_test.go b/internal/go/gen/module_test.go index 125ebfa6..18e5b6cc 100644 --- a/internal/go/gen/module_test.go +++ b/internal/go/gen/module_test.go @@ -32,10 +32,20 @@ func TestPackagePath(t *testing.T) { func TestPackagePathNestedModule(t *testing.T) { root := t.TempDir() - if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module example.com/plugin\n\ngo 1.26.0\n"), 0o600); err != nil { + module := filepath.Join(root, "go.mod") + internal := filepath.Join(root, "internal") + nested := filepath.Join(internal, "bindings") + // TinyGo's Windows RemoveAll cannot enumerate directories. + t.Cleanup(func() { + for _, path := range []string{nested, internal, module, root} { + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + t.Errorf("remove test fixture %s: %v", path, err) + } + } + }) + if err := os.WriteFile(module, []byte("module example.com/plugin\n\ngo 1.26.0\n"), 0o600); err != nil { t.Fatal(err) } - nested := filepath.Join(root, "internal", "bindings") if err := os.MkdirAll(nested, 0o700); err != nil { t.Fatal(err) } From 96b233ae342376f9ac9a5f8f29c2d5e051f646dc Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Sat, 24 Oct 2026 22:13:23 +0200 Subject: [PATCH 05/10] test: support Windows and WASI test environments Keep WIT path fixtures on the checkout volume and WIT goldens on LF endings. Remove owned temporary files explicitly for TinyGo. Run the existing Go version matrix on Windows as well as Linux. --- .gitattributes | 1 + .github/workflows/test.yaml | 3 ++- internal/go/gen/module_test.go | 7 +++++-- internal/witcli/dir_test.go | 12 +++++++++--- wit/load_test.go | 15 ++++++++++++++- 5 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..a5c7c744 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.wit text eol=lf diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f603626b..b899a233 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -40,10 +40,11 @@ jobs: # Test with Go test-go: name: Test with Go - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} timeout-minutes: 15 strategy: matrix: + os: [ubuntu-latest, windows-latest] go-version: ["1.23", "1.24"] steps: - name: Checkout repo diff --git a/internal/go/gen/module_test.go b/internal/go/gen/module_test.go index 18e5b6cc..b577f009 100644 --- a/internal/go/gen/module_test.go +++ b/internal/go/gen/module_test.go @@ -31,11 +31,14 @@ func TestPackagePath(t *testing.T) { } func TestPackagePathNestedModule(t *testing.T) { - root := t.TempDir() + root, err := os.MkdirTemp("", t.Name()+"-") + if err != nil { + t.Fatal(err) + } module := filepath.Join(root, "go.mod") internal := filepath.Join(root, "internal") nested := filepath.Join(internal, "bindings") - // TinyGo's Windows RemoveAll cannot enumerate directories. + // TinyGo lacks recursive removal on Windows and WASI. t.Cleanup(func() { for _, path := range []string{nested, internal, module, root} { if err := os.Remove(path); err != nil && !os.IsNotExist(err) { diff --git a/internal/witcli/dir_test.go b/internal/witcli/dir_test.go index 1a81a3c0..44188bdd 100644 --- a/internal/witcli/dir_test.go +++ b/internal/witcli/dir_test.go @@ -12,7 +12,10 @@ import ( ) func TestFindOrCreateDirFind(t *testing.T) { - temp := t.TempDir() + temp, err := os.MkdirTemp("", t.Name()+"-") + if err != nil { + t.Fatal(err) + } dir := filepath.Join(temp, "existing") cleanupEmptyDirs(t, dir, temp) @@ -49,7 +52,10 @@ func TestFindOrCreateDirFind(t *testing.T) { } func TestFindOrCreateDirCreate(t *testing.T) { - temp := t.TempDir() + temp, err := os.MkdirTemp("", t.Name()+"-") + if err != nil { + t.Fatal(err) + } control := filepath.Join(temp, "control") dir := filepath.Join(temp, "new") cleanupEmptyDirs(t, dir, control, temp) @@ -93,7 +99,7 @@ func TestFindOrCreateDirCreate(t *testing.T) { } } -// TinyGo's Windows RemoveAll cannot enumerate directories; remove owned leaves first. +// TinyGo lacks recursive removal on Windows and WASI; remove owned leaves first. func cleanupEmptyDirs(t *testing.T, paths ...string) { t.Helper() t.Cleanup(func() { diff --git a/wit/load_test.go b/wit/load_test.go index e92e92fe..08a73086 100644 --- a/wit/load_test.go +++ b/wit/load_test.go @@ -9,7 +9,20 @@ import ( ) func TestLoadWITPaths(t *testing.T) { - root := t.TempDir() + // Relative paths require the fixture and working directory to share a volume. + root, err := os.MkdirTemp(".", ".wit-load-paths-*") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := os.RemoveAll(root); err != nil { + t.Error(err) + } + }) + root, err = filepath.Abs(root) + if err != nil { + t.Fatal(err) + } file := filepath.Join(root, "nested", "example.wit") if err := os.MkdirAll(filepath.Dir(file), 0o755); err != nil { t.Fatal(err) From 1f01b14fb5e0de95c6929b58c950607eb3237f2b Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Sat, 24 Oct 2026 21:36:16 +0200 Subject: [PATCH 06/10] fix(deps)!: update vulnerable OCI dependencies Update regclient to 0.11.5, compress to 1.18.7 and xz to 0.5.15. These fix credential leakage, integer overflow and malformed-input memory exhaustion in dependencies used for OCI loading. Test Go 1.26 and 1.27 on Linux and Windows. Use TinyGo 0.43 for native Linux and WASI jobs, with local toolchain selection. BREAKING CHANGE: Building the root module now requires Go 1.26. --- .github/workflows/release.yaml | 10 ++++---- .github/workflows/test.yaml | 43 ++++++++++++++-------------------- go.mod | 12 +++++----- go.sum | 33 ++++++++++++++------------ go.work | 2 +- 5 files changed, 47 insertions(+), 53 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7649924c..7c2e2380 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -25,10 +25,10 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v7 with: go-version-file: go.mod @@ -49,7 +49,7 @@ jobs: rm wit-bindgen-go - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: wit-bindgen-go_${{ matrix.goos }}_${{ matrix.goarch }} path: build/wit-bindgen-go-${{ matrix.goos }}-${{ matrix.goarch }}.tgz @@ -61,10 +61,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Download Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: build/ merge-multiple: true # all artifacts are downloaded to this directory diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b899a233..6766a4cc 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -13,6 +13,7 @@ on: - 'LICENSE' env: + GOTOOLCHAIN: local go-modules: ./... ./cm/... wasm-tools-version: "1.230.0" wasmtime-version: "26.0.0" @@ -25,12 +26,12 @@ jobs: timeout-minutes: 2 steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: submodules: recursive - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v7 with: go-version-file: go.mod @@ -45,15 +46,15 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] - go-version: ["1.23", "1.24"] + go-version: ["1.26", "1.27"] steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: submodules: recursive - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v7 with: go-version: ${{ matrix.go-version }} @@ -78,24 +79,21 @@ jobs: timeout-minutes: 15 strategy: matrix: - go-version: ["1.23", "1.24"] - tinygo-version: ["0.35.0", "0.36.0", "0.37.0"] - exclude: - - go-version: "1.24" - tinygo-version: "0.35.0" + go-version: ["1.26", "1.27"] + tinygo-version: ["0.43.0"] steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: submodules: recursive - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v7 with: go-version: ${{ matrix.go-version }} - name: Set up TinyGo - uses: acifani/setup-tinygo@v2 + uses: acifani/setup-tinygo@v3 with: tinygo-version: ${{ matrix.tinygo-version }} @@ -112,24 +110,21 @@ jobs: timeout-minutes: 15 strategy: matrix: - go-version: ["1.23", "1.24"] - tinygo-version: ["0.35.0", "0.36.0", "0.37.0"] - exclude: - - go-version: "1.24" - tinygo-version: "0.35.0" + go-version: ["1.26", "1.27"] + tinygo-version: ["0.43.0"] steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: submodules: recursive - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v7 with: go-version: ${{ matrix.go-version }} - name: Set up TinyGo - uses: acifani/setup-tinygo@v2 + uses: acifani/setup-tinygo@v3 with: tinygo-version: ${{ matrix.tinygo-version }} @@ -145,11 +140,7 @@ jobs: version: ${{ env.wasmtime-version }} - name: Add Go wasm exec to $PATH - run: echo "$(go env GOROOT)/misc/wasm" >> $GITHUB_PATH - - # This moved in Go 1.24: https://github.com/golang/go/issues/68024 - - name: Add Go 1.24 wasm exec to $PATH - run: echo "$(go env GOROOT)/lib/wasm" >> $GITHUB_PATH + run: echo "$(go env GOROOT)/lib/wasm" >> "$GITHUB_PATH" - name: Regenerate Go run: make tests/generated diff --git a/go.mod b/go.mod index e3cf8eae..c99bc6f2 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module go.bytecodealliance.org -go 1.23.0 +go 1.26.0 require ( github.com/coreos/go-semver v0.3.1 - github.com/regclient/regclient v0.8.3 + github.com/regclient/regclient v0.11.5 github.com/sergi/go-diff v1.3.1 github.com/tetratelabs/wazero v1.9.0 github.com/urfave/cli/v3 v3.3.8 @@ -15,10 +15,10 @@ require ( require ( github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.7 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/ulikunitz/xz v0.5.12 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect golang.org/x/sync v0.14.0 // indirect - golang.org/x/sys v0.33.0 // indirect + golang.org/x/sys v0.45.0 // indirect ) diff --git a/go.sum b/go.sum index 55d8806d..7708fedc 100644 --- a/go.sum +++ b/go.sum @@ -5,51 +5,54 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw= +github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/olareg/olareg v0.1.2 h1:75G8X6E9FUlzL/CSjgFcYfMgNzlc7CxULpUUNsZBIvI= -github.com/olareg/olareg v0.1.2/go.mod h1:TWs+N6pO1S4bdB6eerzUm/ITRQ6kw91mVf9ZYeGtw+Y= +github.com/olareg/olareg v0.2.1 h1:RPHGIaqlVWPbKAsOYUj7e2WfEEW5M7F8I6hKMrwd4jU= +github.com/olareg/olareg v0.2.1/go.mod h1:dhr8QetC7U7jJ2m93oxDhEEOKCRbPgOK1oGyKfB4QNo= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/regclient/regclient v0.8.3 h1:AFAPu/vmOYGyY22AIgzdBUKbzH+83lEpRioRYJ/reCs= -github.com/regclient/regclient v0.8.3/go.mod h1:gjQh5uBVZoo/CngchghtQh9Hx81HOMKRRDd5WPcPkbk= +github.com/regclient/regclient v0.11.5 h1:OHRsXO0F3qHGfa4HEUv+EkMH9NXNcCTBKjNzyC/UhIA= +github.com/regclient/regclient v0.11.5/go.mod h1:DZUOfIT14WFTK2Pj4vjd93avy9O4Fdpjrf9ir23TbRE= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/sudo-bmitch/oci-digest v0.1.2 h1:are0qzWTsFZGZ3Uvdi9OSztJszSWaab6iqquMEEB7rw= +github.com/sudo-bmitch/oci-digest v0.1.2/go.mod h1:SH6l5OIe0islKBZBedjiPOeET/0QwGL+/oYfQt51uQo= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= -github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= -github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E= github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= go.bytecodealliance.org/cm v0.3.0 h1:VhV+4vjZPUGCozCg9+up+FNL3YU6XR+XKghk7kQ0vFc= go.bytecodealliance.org/cm v0.3.0/go.mod h1:JD5vtVNZv7sBoQQkvBvAAVKJPhR/bqBH7yYXTItMfZI= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.work b/go.work index d89b68f7..e2e8563d 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.23.0 +go 1.26.0 use ( . From 97ec12ee8d77b4aa5a17e75b118184d2da9590ae Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Sat, 24 Oct 2026 21:45:24 +0200 Subject: [PATCH 07/10] fix(ci): scope release token and quote shell args Grant contents write permission to the release job and read permission to the build job. Quote workspace and tag arguments; pass prerelease options through a Bash array. --- .github/workflows/release.yaml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7c2e2380..eb2bfe9f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -12,7 +12,7 @@ jobs: build: name: Build binaries permissions: - contents: write + contents: read runs-on: ubuntu-latest strategy: @@ -57,6 +57,8 @@ jobs: release: name: Create GitHub release + permissions: + contents: write needs: build runs-on: ubuntu-latest steps: @@ -72,8 +74,8 @@ jobs: - name: Extract release notes run: | TAG_NAME=${GITHUB_REF#refs/tags/} - cd $GITHUB_WORKSPACE - ./scripts/extract-changelog.sh $TAG_NAME > RELEASE_NOTES.md + cd "$GITHUB_WORKSPACE" + ./scripts/extract-changelog.sh "$TAG_NAME" > RELEASE_NOTES.md cat RELEASE_NOTES.md - name: Create release @@ -83,12 +85,12 @@ jobs: ls -R build/ TAG_NAME=${GITHUB_REF#refs/tags/} if [[ "$TAG_NAME" =~ .+-pre.* ]]; then - PRERELEASE_ARGS="--prerelease --latest=false" + PRERELEASE_ARGS=(--prerelease --latest=false) else - PRERELEASE_ARGS="" + PRERELEASE_ARGS=() fi gh release create "${{ github.ref_name }}" \ --notes-file RELEASE_NOTES.md \ --title "${{ github.ref_name }}" \ - $PRERELEASE_ARGS \ + "${PRERELEASE_ARGS[@]}" \ build/* From 9360a79513f7632c075a898446c59ac58c82a3b5 Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Sat, 24 Oct 2026 21:56:15 +0200 Subject: [PATCH 08/10] fix(cabi): allocate full aligned memory spans Typed slices used min(size/align, 1), allocating at most one element for aligned buffers. Allocate checked, padded byte storage so every requested byte fits after alignment. Copy the retained prefix when shrinking requires realignment. Test writable buffers and preserved bytes using real allocations. --- x/cabi/realloc.go | 44 ++++++------ x/cabi/realloc_test.go | 152 +++++++++++++++++++++++++++-------------- 2 files changed, 121 insertions(+), 75 deletions(-) diff --git a/x/cabi/realloc.go b/x/cabi/realloc.go index 6baf1333..3e4c6f8a 100644 --- a/x/cabi/realloc.go +++ b/x/cabi/realloc.go @@ -7,12 +7,15 @@ import "unsafe" // // Note: the use of uintptr assumes 32-bit pointers when compiled for wasm or wasm32. func realloc(ptr unsafe.Pointer, size, align, newsize uintptr) unsafe.Pointer { - if newsize <= size { - return unsafe.Add(ptr, offset(uintptr(ptr), align)) + if newsize == 0 { + return nil + } + if newsize <= size && offset(uintptr(ptr), align) == 0 { + return ptr } newptr := alloc(newsize, align) if size > 0 { - copy(unsafe.Slice((*byte)(newptr), newsize), unsafe.Slice((*byte)(ptr), size)) + copy(unsafe.Slice((*byte)(newptr), newsize), unsafe.Slice((*byte)(ptr), min(size, newsize))) } return newptr } @@ -20,30 +23,27 @@ func realloc(ptr unsafe.Pointer, size, align, newsize uintptr) unsafe.Pointer { // offset returns the delta between the aligned value of ptr and ptr // so it can be passed to unsafe.Add. The return value is guaranteed to be >= 0. func offset(ptr, align uintptr) uintptr { - newptr := (ptr + align - 1) &^ (align - 1) - return newptr - ptr + return -ptr & (allocationAlignment(align) - 1) } // alloc allocates a block of memory with size bytes. -// It attempts to align the allocated memory by allocating a slice of -// a type that matches the desired alignment. It aligns to 16 bytes for -// values of align other than 1, 2, 4, or 8. +// Padding allows alignment without reducing the usable allocation. +// Values of align other than 1, 2, 4, or 8 select 16-byte alignment. func alloc(size, align uintptr) unsafe.Pointer { + padding := allocationAlignment(align) - 1 + if size > uintptr(^uint(0)>>1)-padding { + panic("cabi allocation size overflows int") + } + s := make([]byte, max(size, 1)+padding) + ptr := unsafe.Pointer(unsafe.SliceData(s)) + return unsafe.Add(ptr, offset(uintptr(ptr), align)) +} + +func allocationAlignment(align uintptr) uintptr { switch align { - case 1: - s := make([]uint8, size) - return unsafe.Pointer(unsafe.SliceData(s)) - case 2: - s := make([]uint16, min(size/align, 1)) - return unsafe.Pointer(unsafe.SliceData(s)) - case 4: - s := make([]uint32, min(size/align, 1)) - return unsafe.Pointer(unsafe.SliceData(s)) - case 8: - s := make([]uint64, min(size/align, 1)) - return unsafe.Pointer(unsafe.SliceData(s)) + case 1, 2, 4, 8: + return align default: - s := make([][2]uint64, min(size/align, 1)) - return unsafe.Pointer(unsafe.SliceData(s)) + return 16 } } diff --git a/x/cabi/realloc_test.go b/x/cabi/realloc_test.go index 84d2fc8e..c8e711ae 100644 --- a/x/cabi/realloc_test.go +++ b/x/cabi/realloc_test.go @@ -1,72 +1,118 @@ package cabi import ( + "fmt" "slices" "testing" "unsafe" ) -func TestRealloc(t *testing.T) { - const threshold = 16 - - tests := []struct { - name string - ptr uintptr - size uintptr - align uintptr - newsize uintptr - want uintptr - }{ - {"nil", 0, 0, 1, 0, 0}, - {"nil with align", 0, 0, 2, 0, 0}, - {"align to 2", 1, 0, 2, 0, 2}, - {"align to 2", 1, 0, 2, 0, 2}, - {"align to 4", 1, 0, 4, 0, 4}, - {"align to 8", 3, 0, 8, 0, 8}, - {"align to 16", 9, 0, 16, 0, 16}, - {"alloc 100 bytes", 0, 0, 1, 100, sliceData(hundred)}, - {"preserve 5 bytes", stringData("hello"), 5, 1, 5, stringData("hello")}, - {"shrink 8 bytes to 4", stringData("fourfour"), 8, 1, 4, stringData("four")}, - {"expand 4 bytes to 8", stringData("zero"), 4, 1, 8, stringData("zero\u0000\u0000\u0000\u0000")}, - {"cut down lorem ipsum", stringData(lorem), 4, 1, 200, stringData(lorem[:200])}, +func TestOffset(t *testing.T) { + for _, align := range []uintptr{1, 2, 4, 8, 16} { + for _, ptr := range []uintptr{0, 1, 3, 9, 16, ^uintptr(0) - 1, ^uintptr(0)} { + want := (align - ptr%align) % align + if got := offset(ptr, align); got != want { + t.Errorf("offset(%d, %d) = %d, want %d", ptr, align, got, want) + } + } } +} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := realloc(unsafePointer(tt.ptr), tt.size, tt.align, tt.newsize) - if (tt.want < threshold && uintptr(got) != tt.want) || (tt.want >= threshold && uintptr(got) < threshold) { - t.Errorf("realloc(%d, %d, %d, %d): %d, expected %d", - tt.ptr, tt.size, tt.align, tt.newsize, got, tt.want) - } - if uintptr(got)%tt.align != 0 { - t.Errorf("expected ptr aligned to %d, got %d", tt.align, uintptr(got)&tt.align) - } - if uintptr(got) < threshold { - return // it didn’t allocate - } - if tt.ptr == 0 { - wants := unsafe.Slice((*byte)(unsafePointer(tt.want)), tt.newsize) - gots := unsafe.Slice((*byte)(got), tt.newsize) - if slices.Compare(wants, gots) != 0 { - t.Errorf("expected %v, got %v", wants, gots) +func TestAlloc(t *testing.T) { + for _, align := range []uintptr{0, 1, 2, 3, 4, 8, 16, 32} { + wantAlign := align + if align != 1 && align != 2 && align != 4 && align != 8 { + wantAlign = 16 + } + for _, size := range []uintptr{0, 1, 3, 7, 9, 17, 100, 257} { + t.Run(fmt.Sprintf("align%d/size%d", align, size), func(t *testing.T) { + ptr := alloc(size, align) + if uintptr(ptr)%wantAlign != 0 { + t.Fatalf("allocation %p is not aligned to %d", ptr, wantAlign) } - } - }) + span := unsafe.Slice((*byte)(ptr), size) + for i := range span { + span[i] = byte(i + 1) + } + for i, got := range span { + if want := byte(i + 1); got != want { + t.Fatalf("byte %d = %d, want %d", i, got, want) + } + } + }) + } } } -var hundred = make([]byte, 100) -var lorem = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut volutpat arcu eu est tristique suscipit. Nulla laoreet purus magna, at feugiat tortor fermentum non. Integer semper et magna id placerat. Quisque purus lorem, mollis vel convallis eu, ullamcorper sit amet justo. Duis tempus gravida lacus, vel dapibus augue. Nunc sed condimentum lacus. Cras vulputate cursus dictum. Etiam felis metus, volutpat id luctus ac, ultrices nec metus. Proin sagittis nulla a pretium sagittis. Nullam tristique sapien sed semper faucibus. Fusce condimentum nulla dui. Donec egestas nunc in blandit mollis.` - -// Appease vet, see https://github.com/golang/go/issues/58625 -func unsafePointer(p uintptr) unsafe.Pointer { - return *(*unsafe.Pointer)(unsafe.Pointer(&p)) +func TestAllocOverflow(t *testing.T) { + for _, align := range []uintptr{1, 2, 4, 8, 16} { + t.Run(fmt.Sprintf("align%d", align), func(t *testing.T) { + defer func() { + if recover() == nil { + t.Error("oversized allocation did not panic") + } + }() + alloc(^uintptr(0), align) + }) + } } -func sliceData[T any](s []T) uintptr { - return uintptr(unsafe.Pointer(unsafe.SliceData(s))) +func TestRealloc(t *testing.T) { + for _, align := range []uintptr{1, 2, 4, 8, 16} { + for _, sizes := range [][2]uintptr{{0, 0}, {0, 100}, {17, 17}, {17, 7}, {7, 100}, {17, 0}} { + size, newsize := sizes[0], sizes[1] + t.Run(fmt.Sprintf("align%d/%dto%d", align, size, newsize), func(t *testing.T) { + var ptr unsafe.Pointer + if size > 0 { + ptr = alloc(size, align) + } + original := unsafe.Slice((*byte)(ptr), size) + for i := range original { + original[i] = byte(i + 1) + } + want := slices.Clone(original[:min(size, newsize)]) + got := realloc(ptr, size, align, newsize) + if newsize == 0 { + if got != nil { + t.Fatalf("zero-sized reallocation returned %p", got) + } + return + } + if uintptr(got)%align != 0 { + t.Fatalf("reallocation %p is not aligned to %d", got, align) + } + span := unsafe.Slice((*byte)(got), newsize) + if !slices.Equal(span[:len(want)], want) { + t.Fatalf("reallocation prefix = %v, want %v", span[:len(want)], want) + } + for i := range span { + span[i] = byte(i + 1) + } + if newsize <= size && got != ptr { + t.Error("aligned shrinking allocation moved") + } + }) + } + } } -func stringData(s string) uintptr { - return uintptr(unsafe.Pointer(unsafe.StringData(s))) +func TestReallocRealignedShrink(t *testing.T) { + for _, align := range []uintptr{2, 4, 8, 16} { + t.Run(fmt.Sprintf("align%d", align), func(t *testing.T) { + ptr := unsafe.Add(alloc(33, align), 1) + original := unsafe.Slice((*byte)(ptr), 32) + for i := range original { + original[i] = byte(i + 1) + } + want := slices.Clone(original[:17]) + got := realloc(ptr, 32, align, 17) + if uintptr(got)%align != 0 { + t.Fatalf("reallocation %p is not aligned to %d", got, align) + } + span := unsafe.Slice((*byte)(got), 17) + if !slices.Equal(span, want) { + t.Fatalf("realigned allocation = %v, want %v", span, want) + } + }) + } } From 039d62d716431f7f3ba789301685fa303c7193c3 Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Sat, 24 Oct 2026 23:15:14 +0200 Subject: [PATCH 09/10] fix(ci): limit test workflow token permissions --- .github/workflows/test.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6766a4cc..676ef781 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,6 +12,9 @@ on: - 'docs/**' - 'LICENSE' +permissions: + contents: read + env: GOTOOLCHAIN: local go-modules: ./... ./cm/... From cf1618f677ea21080dd7986e02c77a393e8a05c1 Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Sat, 24 Oct 2026 23:36:18 +0200 Subject: [PATCH 10/10] fix(ci): allow 20 minutes for native Go tests --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 676ef781..40a7a361 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -45,7 +45,7 @@ jobs: test-go: name: Test with Go runs-on: ${{ matrix.os }} - timeout-minutes: 15 + timeout-minutes: 20 strategy: matrix: os: [ubuntu-latest, windows-latest]