Repository: cilium/ebpf
Pull request #1757 (2025-04-17 07:28:42)

ci: ensure build works with GOARCH=wasm
Add wasm to list of little-endian arches for NativeEndian as well as bpf2go output.
Make sure at least everything builds against the wasm target of the official Go toolchain.


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fca526a..3243523 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,7 +8,7 @@ on:
 env:
   TMPDIR: /tmp
   CI_MAX_KERNEL_VERSION: '6.11'
-  CI_MAX_EFW_VERSION: '0.20.0'
+  CI_MAX_EFW_VERSION: '0.21.0'
   CI_MIN_CLANG_VERSION: '13'
   go_version: '~1.24'
   prev_go_version: '~1.23'
@@ -67,6 +67,14 @@ jobs:
           go build -v ./...
           go test -c -o /dev/null ./... >/dev/null
 
+      - name: Cross build wasm
+        env:
+          GOOS: js
+          GOARCH: wasm
+        run: |
+          go build -v ./...
+          go test -c -o /dev/null ./... >/dev/null
+
   build-docs:
     name: Build Documentation
     runs-on: ubuntu-22.04
@@ -238,7 +246,7 @@ jobs:
       GOMODCACHE: D:\gomodcache
       # Avoid putting temp on slow C:
       TEMP: D:\temp
-      CI_EFW_VERSION: "0.20.0"
+      CI_EFW_VERSION: "0.21.0"
 
     steps:
       - run: mkdir D:\temp
diff --git a/README.md b/README.md
index 8238256..d471ae7 100644
--- a/README.md
+++ b/README.md
@@ -59,8 +59,10 @@ This library includes the following packages:
 
 * A version of Go that is [supported by
   upstream](https://golang.org/doc/devel/release.html#policy)
-* CI is run against kernel.org LTS releases. >= 4.4 should work but EOL'ed versions
-  are not supported.
+* Linux: CI is run against kernel.org LTS releases. >= 4.4 should work but EOL'ed
+  versions are not supported.
+* Windows: CI is run against Windows Server 2022. Only the latest eBPF for Windows
+  release is supported.
 
 ## License
 
diff --git a/btf/btf.go b/btf/btf.go
index eb5a23c..2f9b211 100644
--- a/btf/btf.go
+++ b/btf/btf.go
@@ -542,16 +542,6 @@ func (s *Spec) Copy() *Spec {
 	}
 }
 
-type sliceWriter []byte
-
-func (sw sliceWriter) Write(p []byte) (int, error) {
-	if len(p) != len(sw) {
-		return 0, errors.New("size doesn't match")
-	}
-
-	return copy(sw, p), nil
-}
-
 // nextTypeID returns the next unallocated type ID or an error if there are no
 // more type IDs.
 func (s *Spec) nextTypeID() (TypeID, error) {
diff --git a/btf/btf_types.go b/btf/btf_types.go
index 320311b..95bd756 100644
--- a/btf/btf_types.go
+++ b/btf/btf_types.go
@@ -268,30 +268,14 @@ func (bt *btfType) SetSize(size uint32) {
 	bt.SizeType = size
 }
 
-func (bt *btfType) Marshal(w io.Writer, bo binary.ByteOrder) error {
-	buf := make([]byte, unsafe.Sizeof(*bt))
+func (bt *btfType) Encode(buf []byte, bo binary.ByteOrder) (int, error) {
+	if len(buf) < btfTypeSize {
+		return 0, fmt.Errorf("not enough bytes to marshal btfType")
+	}
 	bo.PutUint32(buf[0:], bt.NameOff)
 	bo.PutUint32(buf[4:], bt.Info)
 	bo.PutUint32(buf[8:], bt.SizeType)
-	_, err := w.Write(buf)
-	return err
-}
-
-type rawType struct {
-	btfType
-	data interface{}
-}
-
-func (rt *rawType) Marshal(w io.Writer, bo binary.ByteOrder) error {
-	if err := rt.btfType.Marshal(w, bo); err != nil {
-		return err
-	}
-
-	if rt.data == nil {
-		return nil
-	}
-
-	return binary.Write(w, bo, rt.data)
+	return btfTypeSize, nil
 }
 
 // btfInt encodes additional data for integers.
diff --git a/btf/marshal.go b/btf/marshal.go
index 585ef4a..308ce8d 100644
--- a/btf/marshal.go
+++ b/btf/marshal.go
@@ -1,7 +1,6 @@
 package btf
 
 import (
-	"bytes"
 	"encoding/binary"
 	"errors"
 	"fmt"
@@ -9,6 +8,7 @@ import (
 	"math"
 	"slices"
 	"sync"
+	"unsafe"
 
 	"github.com/cilium/ebpf/internal"
 )
@@ -45,7 +45,6 @@ type encoder struct {
 	MarshalOptions
 
 	pending internal.Deque[Type]
-	buf     *bytes.Buffer
 	strings *stringTableBuilder
 	ids     map[Type]TypeID
 	visited map[Type]struct{}
@@ -166,12 +165,8 @@ func (b *Builder) Marshal(buf []byte, opts *MarshalOptions) ([]byte, error) {
 	// Reserve space for the BTF header.
 	buf = slices.Grow(buf, btfHeaderLen)[:btfHeaderLen]
 
-	w := internal.NewBuffer(buf)
-	defer internal.PutBuffer(w)
-
 	e := encoder{
 		MarshalOptions: *opts,
-		buf:            w,
 		strings:        stb,
 		lastID:         TypeID(len(b.types)),
 		visited:        make(map[Type]struct{}, len(b.types)),
@@ -199,15 +194,16 @@ func (b *Builder) Marshal(buf []byte, opts *MarshalOptions) ([]byte, error) {
 		e.pending.Push(typ)
 	}
 
-	if err := e.deflatePending(); err != nil {
+	buf, err := e.deflatePending(buf)
+	if err != nil {
 		return nil, err
 	}
 
-	length := e.buf.Len()
+	length := len(buf)
 	typeLen := uint32(length - btfHeaderLen)
 
 	stringLen := e.strings.Length()
-	buf = e.strings.AppendEncoded(e.buf.Bytes())
+	buf = e.strings.AppendEncoded(buf)
 
 	// Fill out the header, and write it out.
 	header := &btfHeader{
@@ -221,7 +217,7 @@ func (b *Builder) Marshal(buf []byte, opts *MarshalOptions) ([]byte, error) {
 		StringLen: uint32(stringLen),
 	}
 
-	err := binary.Write(sliceWriter(buf[:btfHeaderLen]), e.Order, header)
+	_, err = binary.Encode(buf[:btfHeaderLen], e.Order, header)
 	if err != nil {
 		return nil, fmt.Errorf("write header: %v", err)
 	}
@@ -280,7 +276,7 @@ func (e *encoder) id(typ Type) TypeID {
 	return id
 }
 
-func (e *encoder) deflatePending() error {
+func (e *encoder) deflatePending(buf []byte) ([]byte, error) {
 	// Declare root outside of the loop to avoid repeated heap allocations.
 	var root Type
 
@@ -288,20 +284,22 @@ func (e *encoder) deflatePending() error {
 		root = e.pending.Shift()
 
 		// Allocate IDs for all children of typ, including transitive dependencies.
-		if err := e.allocateIDs(root); err != nil {
-			return err
+		err := e.allocateIDs(root)
+		if err != nil {
+			return nil, err
 		}
 
-		if err := e.deflateType(root); err != nil {
+		buf, err = e.deflateType(buf, root)
+		if err != nil {
 			id := e.ids[root]
-			return fmt.Errorf("deflate %v with ID %d: %w", root, id, err)
+			return nil, fmt.Errorf("deflate %v with ID %d: %w", root, id, err)
 		}
 	}
 
-	return nil
+	return buf, nil
 }
 
-func (e *encoder) deflateType(typ Type) (err error) {
+func (e *encoder) deflateType(buf []byte, typ Type) (_ []byte, err error) {
 	defer func() {
 		if r := recover(); r != nil {
 			var ok bool
@@ -312,18 +310,22 @@ func (e *encoder) deflateType(typ Type) (err error) {
 		}
 	}()
 
-	var raw rawType
+	var raw btfType
 	raw.NameOff, err = e.strings.Add(typ.TypeName())
 	if err != nil {
-		return err
+		return nil, err
 	}
 
+	// Reserve space for the btfType header.
+	start := len(buf)
+	buf = append(buf, make([]byte, unsafe.Sizeof(raw))...)
+
 	switch v := typ.(type) {
 	case *Void:
-		return errors.New("Void is implicit in BTF wire format")
+		return nil, errors.New("Void is implicit in BTF wire format")
 
 	case *Int:
-		e.deflateInt(&raw, v)
+		buf, err = e.deflateInt(buf, &raw, v)
 
 	case *Pointer:
 		raw.SetKind(kindPointer)
@@ -331,25 +333,25 @@ func (e *encoder) deflateType(typ Type) (err error) {
 
 	case *Array:
 		raw.SetKind(kindArray)
-		raw.data = &btfArray{
+		buf, err = binary.Append(buf, e.Order, &btfArray{
 			e.id(v.Type),
 			e.id(v.Index),
 			v.Nelems,
-		}
+		})
 
 	case *Struct:
 		raw.SetKind(kindStruct)
 		raw.SetSize(v.Size)
-		raw.data, err = e.convertMembers(&raw.btfType, v.Members)
+		buf, err = e.deflateMembers(buf, &raw, v.Members)
 
 	case *Union:
-		err = e.deflateUnion(&raw, v)
+		buf, err = e.deflateUnion(buf, &raw, v)
 
 	case *Enum:
 		if v.Size == 8 {
-			err = e.deflateEnum64(&raw, v)
+			buf, err = e.deflateEnum64(buf, &raw, v)
 		} else {
-			err = e.deflateEnum(&raw, v)
+			buf, err = e.deflateEnum(buf, &raw, v)
 		}
 
 	case *Fwd:
@@ -382,41 +384,46 @@ func (e *encoder) deflateType(typ Type) (err error) {
 		raw.SetKind(kindFuncProto)
 		raw.SetType(e.id(v.Return))
 		raw.SetVlen(len(v.Params))
-		raw.data, err = e.deflateFuncParams(v.Params)
+		buf, err = e.deflateFuncParams(buf, v.Params)
 
 	case *Var:
 		raw.SetKind(kindVar)
 		raw.SetType(e.id(v.Type))
-		raw.data = btfVariable{uint32(v.Linkage)}
+		buf, err = binary.Append(buf, e.Order, btfVariable{uint32(v.Linkage)})
 
 	case *Datasec:
 		raw.SetKind(kindDatasec)
 		raw.SetSize(v.Size)
 		raw.SetVlen(len(v.Vars))
-		raw.data = e.deflateVarSecinfos(v.Vars)
+		buf, err = e.deflateVarSecinfos(buf, v.Vars)
 
 	case *Float:
 		raw.SetKind(kindFloat)
 		raw.SetSize(v.Size)
 
 	case *declTag:
-		err = e.deflateDeclTag(&raw, v)
+		buf, err = e.deflateDeclTag(buf, &raw, v)
 
 	case *TypeTag:
 		err = e.deflateTypeTag(&raw, v)
 
 	default:
-		return fmt.Errorf("don't know how to deflate %T", v)
+		return nil, fmt.Errorf("don't know how to deflate %T", v)
 	}
 
 	if err != nil {
-		return err
+		return nil, err
 	}
 
-	return raw.Marshal(e.buf, e.Order)
+	header := buf[start : start+int(unsafe.Sizeof(raw))]
+	if _, err = raw.Encode(header, e.Order); err != nil {
+		return nil, err
+	}
+
+	return buf, nil
 }
 
-func (e *encoder) deflateInt(raw *rawType, i *Int) {
+func (e *encoder) deflateInt(buf []byte, raw *btfType, i *Int) ([]byte, error) {
 	raw.SetKind(kindInt)
 	raw.SetSize(i.Size)
 
@@ -425,40 +432,47 @@ func (e *encoder) deflateInt(raw *rawType, i *Int) {
 	// We need to set bits in addition to size, since btf_type_int_is_regular
 	// otherwise flags this as a bitfield.
 	bi.SetBits(byte(i.Size) * 8)
-	raw.data = bi
+	return binary.Append(buf, e.Order, bi)
 }
 
-func (e *encoder) deflateDeclTag(raw *rawType, tag *declTag) (err error) {
+func (e *encoder) deflateDeclTag(buf []byte, raw *btfType, tag *declTag) ([]byte, error) {
 	// Replace a decl tag with an integer for compatibility with <5.16 kernels,
 	// following libbpf behaviour.
 	if e.ReplaceDeclTags {
 		typ := &Int{"decl_tag_placeholder", 1, Unsigned}
-		e.deflateInt(raw, typ)
+		buf, err := e.deflateInt(buf, raw, typ)
+		if err != nil {
+			return nil, err
+		}
 
 		// Add the placeholder type name to the string table. The encoder added the
 		// original type name before this call.
 		raw.NameOff, err = e.strings.Add(typ.TypeName())
-		return
+		return buf, err
 	}
 
+	var err error
 	raw.SetKind(kindDeclTag)
 	raw.SetType(e.id(tag.Type))
-	raw.data = &btfDeclTag{uint32(tag.Index)}
 	raw.NameOff, err = e.strings.Add(tag.Value)
-	return
+	if err != nil {
+		return nil, err
+	}
+
+	return binary.Append(buf, e.Order, btfDeclTag{uint32(tag.Index)})
 }
 
-func (e *encoder) deflateConst(raw *rawType, c *Const) {
+func (e *encoder) deflateConst(raw *btfType, c *Const) {
 	raw.SetKind(kindConst)
 	raw.SetType(e.id(c.Type))
 }
 
-func (e *encoder) deflateTypeTag(raw *rawType, tag *TypeTag) (err error) {
+func (e *encoder) deflateTypeTag(raw *btfType, tag *TypeTag) (err error) {
 	// Replace a type tag with a const qualifier for compatibility with <5.17
 	// kernels, following libbpf behaviour.
 	if e.ReplaceTypeTags {
 		e.deflateConst(raw, &Const{tag.Type})
-		return
+		return nil
 	}
 
 	raw.SetKind(kindTypeTag)
@@ -467,16 +481,17 @@ func (e *encoder) deflateTypeTag(raw *rawType, tag *TypeTag) (err error) {
 	return
 }
 
-func (e *encoder) deflateUnion(raw *rawType, union *Union) (err error) {
+func (e *encoder) deflateUnion(buf []byte, raw *btfType, union *Union) ([]byte, error) {
 	raw.SetKind(kindUnion)
 	raw.SetSize(union.Size)
-	raw.data, err = e.convertMembers(&raw.btfType, union.Members)
-	return
+	return e.deflateMembers(buf, raw, union.Members)
 }
 
-func (e *encoder) convertMembers(header *btfType, members []Member) ([]btfMember, error) {
-	bms := make([]btfMember, 0, len(members))
+func (e *encoder) deflateMembers(buf []byte, header *btfType, members []Member) ([]byte, error) {
+	var bm btfMember
 	isBitfield := false
+
+	buf = slices.Grow(buf, len(members)*int(unsafe.Sizeof(bm)))
 	for _, member := range members {
 		isBitfield = isBitfield || member.BitfieldSize > 0
 
@@ -490,30 +505,35 @@ func (e *encoder) convertMembers(header *btfType, members []Member) ([]btfMember
 			return nil, err
 		}
 
-		bms = append(bms, btfMember{
+		bm = btfMember{
 			nameOff,
 			e.id(member.Type),
 			uint32(offset),
-		})
+		}
+
+		buf, err = binary.Append(buf, e.Order, &bm)
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	header.SetVlen(len(members))
 	header.SetBitfield(isBitfield)
-	return bms, nil
+	return buf, nil
 }
 
-func (e *encoder) deflateEnum(raw *rawType, enum *Enum) (err error) {
+func (e *encoder) deflateEnum(buf []byte, raw *btfType, enum *Enum) ([]byte, error) {
 	raw.SetKind(kindEnum)
 	raw.SetSize(enum.Size)
 	raw.SetVlen(len(enum.Values))
 	// Signedness appeared together with ENUM64 support.
 	raw.SetSigned(enum.Signed && !e.ReplaceEnum64)
-	raw.data, err = e.deflateEnumValues(enum)
-	return
+	return e.deflateEnumValues(buf, enum)
 }
 
-func (e *encoder) deflateEnumValues(enum *Enum) ([]btfEnum, error) {
-	bes := make([]btfEnum, 0, len(enum.Values))
+func (e *encoder) deflateEnumValues(buf []byte, enum *Enum) ([]byte, error) {
+	var be btfEnum
+	buf = slices.Grow(buf, len(enum.Values)*int(unsafe.Sizeof(be)))
 	for _, value := range enum.Values {
 		nameOff, err := e.strings.Add(value.Name)
 		if err != nil {
@@ -530,16 +550,21 @@ func (e *encoder) deflateEnumValues(enum *Enum) ([]btfEnum, error) {
 			}
 		}
 
-		bes = append(bes, btfEnum{
+		be = btfEnum{
 			nameOff,
 			uint32(value.Value),
-		})
+		}
+
+		buf, err = binary.Append(buf, e.Order, &be)
+		if err != nil {
+			return nil, err
+		}
 	}
 
-	return bes, nil
+	return buf, nil
 }
 
-func (e *encoder) deflateEnum64(raw *rawType, enum *Enum) (err error) {
+func (e *encoder) deflateEnum64(buf []byte, raw *btfType, enum *Enum) ([]byte, error) {
 	if e.ReplaceEnum64 {
 		// Replace the ENUM64 with a union of fields with the correct size.
 		// This matches libbpf behaviour on purpose.
@@ -552,7 +577,7 @@ func (e *encoder) deflateEnum64(raw *rawType, enum *Enum) (err error) {
 			placeholder.Encoding = Signed
 		}
 		if err := e.allocateIDs(placeholder); err != nil {
-			return fmt.Errorf("add enum64 placeholder: %w", err)
+			return nil, fmt.Errorf("add enum64 placeholder: %w", err)
 		}
 
 		members := make([]Member, 0, len(enum.Values))
@@ -563,61 +588,79 @@ func (e *encoder) deflateEnum64(raw *rawType, enum *Enum) (err error) {
 			})
 		}
 
-		return e.deflateUnion(raw, &Union{enum.Name, enum.Size, members, nil})
+		return e.deflateUnion(buf, raw, &Union{enum.Name, enum.Size, members, nil})
 	}
 
 	raw.SetKind(kindEnum64)
 	raw.SetSize(enum.Size)
 	raw.SetVlen(len(enum.Values))
 	raw.SetSigned(enum.Signed)
-	raw.data, err = e.deflateEnum64Values(enum.Values)
-	return
+	return e.deflateEnum64Values(buf, enum.Values)
 }
 
-func (e *encoder) deflateEnum64Values(values []EnumValue) ([]btfEnum64, error) {
-	bes := make([]btfEnum64, 0, len(values))
+func (e *encoder) deflateEnum64Values(buf []byte, values []EnumValue) ([]byte, error) {
+	var be btfEnum64
+	buf = slices.Grow(buf, len(values)*int(unsafe.Sizeof(be)))
 	for _, value := range values {
 		nameOff, err := e.strings.Add(value.Name)
 		if err != nil {
 			return nil, err
 		}
 
-		bes = append(bes, btfEnum64{
+		be = btfEnum64{
 			nameOff,
 			uint32(value.Value),
 			uint32(value.Value >> 32),
-		})
+		}
+
+		buf, err = binary.Append(buf, e.Order, &be)
+		if err != nil {
+			return nil, err
+		}
 	}
 
-	return bes, nil
+	return buf, nil
 }
 
-func (e *encoder) deflateFuncParams(params []FuncParam) ([]btfParam, error) {
-	bps := make([]btfParam, 0, len(params))
+func (e *encoder) deflateFuncParams(buf []byte, params []FuncParam) ([]byte, error) {
+	var bp btfParam
+	buf = slices.Grow(buf, len(params)*int(unsafe.Sizeof(bp)))
 	for _, param := range params {
 		nameOff, err := e.strings.Add(param.Name)
 		if err != nil {
 			return nil, err
 		}
 
-		bps = append(bps, btfParam{
+		bp = btfParam{
 			nameOff,
 			e.id(param.Type),
-		})
+		}
+
+		buf, err = binary.Append(buf, e.Order, &bp)
+		if err != nil {
+			return nil, err
+		}
 	}
-	return bps, nil
+	return buf, nil
 }
 
-func (e *encoder) deflateVarSecinfos(vars []VarSecinfo) []btfVarSecinfo {
-	vsis := make([]btfVarSecinfo, 0, len(vars))
+func (e *encoder) deflateVarSecinfos(buf []byte, vars []VarSecinfo) ([]byte, error) {
+	var vsi btfVarSecinfo
+	var err error
+	buf = slices.Grow(buf, len(vars)*int(unsafe.Sizeof(vsi)))
 	for _, v := range vars {
-		vsis = append(vsis, btfVarSecinfo{
+		vsi = btfVarSecinfo{
 			e.id(v.Type),
 			v.Offset,
 			v.Size,
-		})
+		}
+
+		buf, err = binary.Append(buf, e.Order, vsi)
+		if err != nil {
+			return nil, err
+		}
 	}
-	return vsis
+	return buf, nil
 }
 
 // MarshalMapKV creates a BTF object containing a map key and value.
diff --git a/cmd/bpf2go/gen/target.go b/cmd/bpf2go/gen/target.go
index 1e0247c..5e0b954 100644
--- a/cmd/bpf2go/gen/target.go
+++ b/cmd/bpf2go/gen/target.go
@@ -14,19 +14,20 @@ import (
 var ErrInvalidTarget = errors.New("unsupported target")
 
 var targetsByGoArch = map[GoArch]Target{
-	"386":      {"bpfel", "x86"},
-	"amd64":    {"bpfel", "x86"},
-	"arm":      {"bpfel", "arm"},
-	"arm64":    {"bpfel", "arm64"},
-	"loong64":  {"bpfel", "loongarch"},
-	"mips":     {"bpfeb", "mips"},
-	"mipsle":   {"bpfel", ""},
-	"mips64":   {"bpfeb", ""},
-	"mips64le": {"bpfel", ""},
-	"ppc64":    {"bpfeb", "powerpc"},
-	"ppc64le":  {"bpfel", "powerpc"},
-	"riscv64":  {"bpfel", "riscv"},
-	"s390x":    {"bpfeb", "s390"},
+	"386":      {"bpfel", "x86", ""},
+	"amd64":    {"bpfel", "x86", ""},
+	"arm":      {"bpfel", "arm", ""},
+	"arm64":    {"bpfel", "arm64", ""},
+	"loong64":  {"bpfel", "loongarch", ""},
+	"mips":     {"bpfeb", "mips", ""},
+	"mipsle":   {"bpfel", "", ""},
+	"mips64":   {"bpfeb", "", ""},
+	"mips64le": {"bpfel", "", ""},
+	"ppc64":    {"bpfeb", "powerpc", ""},
+	"ppc64le":  {"bpfel", "powerpc", ""},
+	"riscv64":  {"bpfel", "riscv", ""},
+	"s390x":    {"bpfeb", "s390", ""},
+	"wasm":     {"bpfel", "", "js"},
 }
 
 type Target struct {
@@ -36,6 +37,8 @@ type Target struct {
 	// Linux arch string, used to define __TARGET_ARCH_xzy macros used by
 	// https://github.com/libbpf/libbpf/blob/master/src/bpf_tracing.h
 	linux string
+	// GOOS override for use during tests.
+	goos string
 }
 
 // TargetsByGoArch returns all supported targets.
@@ -118,7 +121,7 @@ func FindTarget(id string) (Target, GoArches, error) {
 			}
 		}
 		slices.Sort(goarches)
-		return Target{id, ""}, goarches, nil
+		return Target{id, "", ""}, goarches, nil
 
 	case "native":
 		id = runtime.GOARCH
diff --git a/docs/examples/getting_started/counter_bpfel.go b/docs/examples/getting_started/counter_bpfel.go
index 2855422..745d639 100644
--- a/docs/examples/getting_started/counter_bpfel.go
+++ b/docs/examples/getting_started/counter_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm) && linux
 
 package main
 
diff --git a/docs/examples/variables/variables_bpfel.go b/docs/examples/variables/variables_bpfel.go
index f091b89..469e6f7 100644
--- a/docs/examples/variables/variables_bpfel.go
+++ b/docs/examples/variables/variables_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64
+//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm
 
 package main
 
diff --git a/examples/cgroup_skb/bpf_bpfel.go b/examples/cgroup_skb/bpf_bpfel.go
index dc89696..ace40f2 100644
--- a/examples/cgroup_skb/bpf_bpfel.go
+++ b/examples/cgroup_skb/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm) && linux
 
 package main
 
diff --git a/examples/fentry/bpf_bpfel.go b/examples/fentry/bpf_bpfel.go
index 8cd2a09..96515ee 100644
--- a/examples/fentry/bpf_bpfel.go
+++ b/examples/fentry/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm) && linux
 
 package main
 
diff --git a/examples/kprobe/bpf_bpfel.go b/examples/kprobe/bpf_bpfel.go
index b82cd79..ec2bd06 100644
--- a/examples/kprobe/bpf_bpfel.go
+++ b/examples/kprobe/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm) && linux
 
 package main
 
diff --git a/examples/kprobe_percpu/bpf_bpfel.go b/examples/kprobe_percpu/bpf_bpfel.go
index b82cd79..ec2bd06 100644
--- a/examples/kprobe_percpu/bpf_bpfel.go
+++ b/examples/kprobe_percpu/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm) && linux
 
 package main
 
diff --git a/examples/kprobepin/bpf_bpfel.go b/examples/kprobepin/bpf_bpfel.go
index b82cd79..ec2bd06 100644
--- a/examples/kprobepin/bpf_bpfel.go
+++ b/examples/kprobepin/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm) && linux
 
 package main
 
diff --git a/examples/ringbuffer/bpf_bpfel.go b/examples/ringbuffer/bpf_bpfel.go
index 3a0825c..897a938 100644
--- a/examples/ringbuffer/bpf_bpfel.go
+++ b/examples/ringbuffer/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm) && linux
 
 package main
 
diff --git a/examples/tcprtt/bpf_bpfel.go b/examples/tcprtt/bpf_bpfel.go
index e40abf6..2efbd26 100644
--- a/examples/tcprtt/bpf_bpfel.go
+++ b/examples/tcprtt/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm) && linux
 
 package main
 
diff --git a/examples/tcprtt_sockops/bpf_bpfel.go b/examples/tcprtt_sockops/bpf_bpfel.go
index c371fe5..dd04d6f 100644
--- a/examples/tcprtt_sockops/bpf_bpfel.go
+++ b/examples/tcprtt_sockops/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm) && linux
 
 package main
 
diff --git a/examples/tcx/bpf_bpfel.go b/examples/tcx/bpf_bpfel.go
index d23feed..09261fb 100644
--- a/examples/tcx/bpf_bpfel.go
+++ b/examples/tcx/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm) && linux
 
 package main
 
diff --git a/examples/tracepoint_in_c/bpf_bpfel.go b/examples/tracepoint_in_c/bpf_bpfel.go
index 691d541..b461b4d 100644
--- a/examples/tracepoint_in_c/bpf_bpfel.go
+++ b/examples/tracepoint_in_c/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm) && linux
 
 package main
 
diff --git a/examples/xdp/bpf_bpfel.go b/examples/xdp/bpf_bpfel.go
index 45810c6..21bf4d3 100644
--- a/examples/xdp/bpf_bpfel.go
+++ b/examples/xdp/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm) && linux
 
 package main
 
diff --git a/info.go b/info.go
index 667b0cc..c5a6dd1 100644
--- a/info.go
+++ b/info.go
@@ -857,4 +857,4 @@ var haveProgramInfoMapIDs = internal.NewFeatureTest("map IDs in program info", f
 	}
 
 	return err
-}, "4.15", "windows:0.20.0")
+}, "4.15", "windows:0.21.0")
diff --git a/internal/buffer.go b/internal/buffer.go
deleted file mode 100644
index 81c6544..0000000
--- a/internal/buffer.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package internal
-
-import (
-	"bytes"
-	"sync"
-)
-
-var bytesBufferPool = sync.Pool{
-	New: func() interface{} {
-		return new(bytes.Buffer)
-	},
-}
-
-// NewBuffer retrieves a [bytes.Buffer] from a pool an re-initialises it.
-//
-// The returned buffer should be passed to [PutBuffer].
-func NewBuffer(buf []byte) *bytes.Buffer {
-	wr := bytesBufferPool.Get().(*bytes.Buffer)
-	// Reinitialize the Buffer with a new backing slice since it is returned to
-	// the caller by wr.Bytes() below. Pooling is faster despite calling
-	// NewBuffer. The pooled alloc is still reused, it only needs to be zeroed.
-	*wr = *bytes.NewBuffer(buf)
-	return wr
-}
-
-// PutBuffer releases a buffer to the pool.
-func PutBuffer(buf *bytes.Buffer) {
-	// Release reference to the backing buffer.
-	*buf = *bytes.NewBuffer(nil)
-	bytesBufferPool.Put(buf)
-}
diff --git a/internal/endian_le.go b/internal/endian_le.go
index 6dcd916..d833ea7 100644
--- a/internal/endian_le.go
+++ b/internal/endian_le.go
@@ -1,4 +1,4 @@
-//go:build 386 || amd64 || amd64p32 || arm || arm64 || loong64 || mipsle || mips64le || mips64p32le || ppc64le || riscv64
+//go:build 386 || amd64 || amd64p32 || arm || arm64 || loong64 || mipsle || mips64le || mips64p32le || ppc64le || riscv64 || wasm
 
 package internal
 
diff --git a/internal/sysenc/marshal.go b/internal/sysenc/marshal.go
index 9a0e10c..3f7deb8 100644
--- a/internal/sysenc/marshal.go
+++ b/internal/sysenc/marshal.go
@@ -51,11 +51,7 @@ func Marshal(data any, size int) (Buffer, error) {
 			return newBuffer(buf), nil
 		}
 
-		wr := internal.NewBuffer(make([]byte, 0, size))
-		defer internal.PutBuffer(wr)
-
-		err = binary.Write(wr, internal.NativeEndian, value)
-		buf = wr.Bytes()
+		buf, err = binary.Append(nil, internal.NativeEndian, value)
 	}
 	if err != nil {
 		return Buffer{}, err
diff --git a/prog.go b/prog.go
index ebad9cf..c20edbb 100644
--- a/prog.go
+++ b/prog.go
@@ -856,13 +856,13 @@ func (p *Program) run(opts *RunOptions) (uint32, time.Duration, error) {
 		return 0, 0, err
 	}
 
-	var ctxBytes []byte
+	var ctxIn []byte
 	if opts.Context != nil {
-		ctx := new(bytes.Buffer)
-		if err := binary.Write(ctx, internal.NativeEndian, opts.Context); err != nil {
+		var err error
+		ctxIn, err = binary.Append(nil, internal.NativeEndian, opts.Context)
+		if err != nil {
 			return 0, 0, fmt.Errorf("cannot serialize context: %v", err)
 		}
-		ctxBytes = ctx.Bytes()
 	}
 
 	var ctxOut []byte
@@ -877,9 +877,9 @@ func (p *Program) run(opts *RunOptions) (uint32, time.Duration, error) {
 		DataIn:      sys.SlicePointer(opts.Data),
 		DataOut:     sys.SlicePointer(opts.DataOut),
 		Repeat:      uint32(opts.Repeat),
-		CtxSizeIn:   uint32(len(ctxBytes)),
+		CtxSizeIn:   uint32(len(ctxIn)),
 		CtxSizeOut:  uint32(len(ctxOut)),
-		CtxIn:       sys.SlicePointer(ctxBytes),
+		CtxIn:       sys.SlicePointer(ctxIn),
 		CtxOut:      sys.SlicePointer(ctxOut),
 		Flags:       opts.Flags,
 		Cpu:         opts.CPU,
diff --git a/syscalls.go b/syscalls.go
index f03bed0..f0f42b7 100644
--- a/syscalls.go
+++ b/syscalls.go
@@ -100,7 +100,7 @@ var haveNestedMaps = internal.NewFeatureTest("nested maps", func() error {
 		return nil
 	}
 	return err
-}, "4.12", "windows:0.20.0")
+}, "4.12", "windows:0.21.0")
 
 var haveMapMutabilityModifiers = internal.NewFeatureTest("read- and write-only maps", func() error {
 	// This checks BPF_F_RDONLY_PROG and BPF_F_WRONLY_PROG. Since
@@ -214,7 +214,7 @@ var haveObjName = internal.NewFeatureTest("object names", func() error {
 
 	_ = fd.Close()
 	return nil
-}, "4.15", "windows:0.20.0")
+}, "4.15", "windows:0.21.0")
 
 var objNameAllowsDot = internal.NewFeatureTest("dot in object names", func() error {
 	if platform.IsWindows {
@@ -241,7 +241,7 @@ var objNameAllowsDot = internal.NewFeatureTest("dot in object names", func() err
 
 	_ = fd.Close()
 	return nil
-}, "5.2", "windows:0.20.0")
+}, "5.2", "windows:0.21.0")
 
 var haveBatchAPI = internal.NewFeatureTest("map batch api", func() error {
 	var maxEntries uint32 = 2


Test patch:
diff --git a/btf/fuzz_test.go b/btf/fuzz_test.go
index 64ebe10..df93cf5 100644
--- a/btf/fuzz_test.go
+++ b/btf/fuzz_test.go
@@ -7,20 +7,14 @@ import (
 	"io"
 	"testing"
 
+	"github.com/go-quicktest/qt"
+
 	"github.com/cilium/ebpf/internal"
 )
 
 func FuzzSpec(f *testing.F) {
-	var buf bytes.Buffer
-	err := binary.Write(&buf, internal.NativeEndian, &btfHeader{
-		Magic:   btfMagic,
-		Version: 1,
-		HdrLen:  uint32(binary.Size(btfHeader{})),
-	})
-	if err != nil {
-		f.Fatal(err)
-	}
-	f.Add(buf.Bytes())
+	f.Add(mustBTFHeader(f))
+
 	f.Fuzz(func(t *testing.T, data []byte) {
 		if len(data) < binary.Size(btfHeader{}) {
 			t.Skip("data is too short")
@@ -46,16 +40,7 @@ func FuzzSpec(f *testing.F) {
 }
 
 func FuzzExtInfo(f *testing.F) {
-	var buf bytes.Buffer
-	err := binary.Write(&buf, internal.NativeEndian, &btfExtHeader{
-		Magic:   btfMagic,
-		Version: 1,
-		HdrLen:  uint32(binary.Size(btfExtHeader{})),
-	})
-	if err != nil {
-		f.Fatal(err)
-	}
-	f.Add(buf.Bytes(), []byte("\x00foo\x00barfoo\x00"))
+	f.Add(mustBTFHeader(f), []byte("\x00foo\x00barfoo\x00"))
 
 	f.Fuzz(func(t *testing.T, data, strings []byte) {
 		if len(data) < binary.Size(btfExtHeader{}) {
@@ -80,3 +65,13 @@ func FuzzExtInfo(f *testing.F) {
 		}
 	})
 }
+
+func mustBTFHeader(f *testing.F) []byte {
+	buf, err := binary.Append(nil, internal.NativeEndian, &btfHeader{
+		Magic:   btfMagic,
+		Version: 1,
+		HdrLen:  uint32(binary.Size(btfHeader{})),
+	})
+	qt.Assert(f, qt.IsNil(err))
+	return buf
+}
diff --git a/btf/types_test.go b/btf/types_test.go
index 9382568..54695f4 100644
--- a/btf/types_test.go
+++ b/btf/types_test.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"encoding/binary"
 	"fmt"
-	"io"
 	"reflect"
 	"testing"
 
@@ -388,54 +387,49 @@ func TestInflateLegacyBitfield(t *testing.T) {
 	const offset = 3
 	const size = 5
 
-	var rawInt rawType
-	rawInt.SetKind(kindInt)
-	rawInt.SetSize(4)
-	var data btfInt
-	data.SetOffset(offset)
-	data.SetBits(size)
-	rawInt.data = &data
-
-	var (
-		before bytes.Buffer
-		after  bytes.Buffer
-	)
-
-	var beforeInt rawType
-	beforeInt.SetKind(kindStruct)
-	beforeInt.SetVlen(1)
-	beforeInt.data = []btfMember{{Type: 2}}
-
-	if err := beforeInt.Marshal(&before, binary.LittleEndian); err != nil {
-		t.Fatal(err)
+	var placeholder struct {
+		btfType
+		btfInt
 	}
-	if err := rawInt.Marshal(&before, binary.LittleEndian); err != nil {
-		t.Fatal(err)
+	placeholder.SetKind(kindInt)
+	placeholder.SetSize(4)
+	placeholder.SetOffset(offset)
+	placeholder.SetBits(size)
+
+	var structFirst struct {
+		btfType
+		Members [1]btfMember
 	}
+	structFirst.SetKind(kindStruct)
+	structFirst.SetVlen(1)
+	structFirst.Members = [...]btfMember{{Type: 2}}
 
-	afterInt := beforeInt
-	afterInt.data = []btfMember{{Type: 1}}
+	before, err := binary.Append(nil, binary.LittleEndian, &structFirst)
+	qt.Assert(t, qt.IsNil(err))
+	before, err = binary.Append(before, binary.LittleEndian, &placeholder)
+	qt.Assert(t, qt.IsNil(err))
 
-	if err := rawInt.Marshal(&after, binary.LittleEndian); err != nil {
-		t.Fatal(err)
-	}
-	if err := afterInt.Marshal(&after, binary.LittleEndian); err != nil {
-		t.Fatal(err)
-	}
+	structSecond := structFirst
+	structSecond.Members = [...]btfMember{{Type: 1}}
+
+	after, err := binary.Append(nil, binary.LittleEndian, &placeholder)
+	qt.Assert(t, qt.IsNil(err))
+	after, err = binary.Append(after, binary.LittleEndian, &structSecond)
+	qt.Assert(t, qt.IsNil(err))
 
 	emptyStrings := newStringTable("")
 
 	for _, test := range []struct {
-		name   string
-		reader io.Reader
+		name string
+		buf  []byte
 	}{
-		{"struct before int", &before},
-		{"struct after int", &after},
+		{"struct before int", before},
+		{"struct after int", after},
 	} {
 		t.Run(test.name, func(t *testing.T) {
-			types, err := readAndInflateTypes(test.reader, binary.LittleEndian, 2, emptyStrings, nil)
+			types, err := readAndInflateTypes(bytes.NewReader(test.buf), binary.LittleEndian, 2, emptyStrings, nil)
 			if err != nil {
-				t.Log(before.Bytes())
+				t.Log(test.buf)
 				t.Fatal(err)
 			}
 
diff --git a/cmd/bpf2go/gen/target_test.go b/cmd/bpf2go/gen/target_test.go
index c0fe2ca..18085bd 100644
--- a/cmd/bpf2go/gen/target_test.go
+++ b/cmd/bpf2go/gen/target_test.go
@@ -43,32 +43,32 @@ func TestCollectTargets(t *testing.T) {
 	}{
 		{
 			"bpf",
-			Target{"bpf", ""},
+			Target{"bpf", "", ""},
 			nil,
 		},
 		{
 			"bpfel",
-			Target{"bpfel", ""},
+			Target{"bpfel", "", ""},
 			clangArches["bpfel"],
 		},
 		{
 			"bpfeb",
-			Target{"bpfeb", ""},
+			Target{"bpfeb", "", ""},
 			clangArches["bpfeb"],
 		},
 		{
 			"amd64",
-			Target{"bpfel", "x86"},
+			Target{"bpfel", "x86", ""},
 			linuxArchesLE["x86"],
 		},
 		{
 			"386",
-			Target{"bpfel", "x86"},
+			Target{"bpfel", "x86", ""},
 			linuxArchesLE["x86"],
 		},
 		{
 			"ppc64",
-			Target{"bpfeb", "powerpc"},
+			Target{"bpfeb", "powerpc", ""},
 			linuxArchesBE["powerpc"],
 		},
 		{
@@ -111,10 +111,14 @@ func TestCollectTargetsErrors(t *testing.T) {
 func TestGoarches(t *testing.T) {
 	exe := goBin(t)
 
-	for GoArch := range targetsByGoArch {
+	for GoArch, tgt := range targetsByGoArch {
 		t.Run(string(GoArch), func(t *testing.T) {
+			goOS := "linux"
+			if tgt.goos != "" {
+				goOS = tgt.goos
+			}
 			goEnv := exec.Command(exe, "env")
-			goEnv.Env = []string{"GOROOT=/", "GOOS=linux", "GOARCH=" + string(GoArch)}
+			goEnv.Env = []string{"GOROOT=/", "GOOS=" + string(goOS), "GOARCH=" + string(GoArch)}
 			output, err := goEnv.CombinedOutput()
 			qt.Assert(t, qt.IsNil(err), qt.Commentf("go output is:\n%s", string(output)))
 		})
diff --git a/cmd/bpf2go/test/test_bpfel.go b/cmd/bpf2go/test/test_bpfel.go
index a990bcc..169b88d 100644
--- a/cmd/bpf2go/test/test_bpfel.go
+++ b/cmd/bpf2go/test/test_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm) && linux
 
 package test
 
diff --git a/internal/kconfig/kconfig_test.go b/internal/kconfig/kconfig_test.go
index d9cd719..25c6002 100644
--- a/internal/kconfig/kconfig_test.go
+++ b/internal/kconfig/kconfig_test.go
@@ -1,7 +1,6 @@
 package kconfig
 
 import (
-	"bytes"
 	"encoding/binary"
 	"os"
 	"testing"
@@ -401,13 +400,9 @@ func TestPutValue(t *testing.T) {
 			continue
 		}
 
-		var buf bytes.Buffer
-		err := binary.Write(&buf, internal.NativeEndian, c.expected)
-		if err != nil {
-			t.Fatal(err)
-		}
+		expected, err := binary.Append(nil, internal.NativeEndian, c.expected)
+		qt.Assert(t, qt.IsNil(err))
 
-		expected := buf.Bytes()
 		data := make([]byte, len(expected))
 		err = PutValue(data, c.typ, c.value)
 
diff --git a/internal/sysenc/marshal_test.go b/internal/sysenc/marshal_test.go
index 97de49a..d6ce22a 100644
--- a/internal/sysenc/marshal_test.go
+++ b/internal/sysenc/marshal_test.go
@@ -60,18 +60,16 @@ func TestMarshal(t *testing.T) {
 	for _, test := range testcases() {
 		value := test.new()
 		t.Run(fmt.Sprintf("%T", value), func(t *testing.T) {
-			var want bytes.Buffer
-			if err := binary.Write(&want, internal.NativeEndian, value); err != nil {
-				t.Fatal(err)
-			}
+			want, err := binary.Append(nil, internal.NativeEndian, value)
+			qt.Assert(t, qt.IsNil(err))
 
-			have := make([]byte, want.Len())
+			have := make([]byte, len(want))
 			buf, err := Marshal(value, binary.Size(value))
 			if err != nil {
 				t.Fatal(err)
 			}
-			qt.Assert(t, qt.Equals(buf.CopyTo(have), want.Len()))
-			qt.Assert(t, qt.CmpEquals(have, want.Bytes(), cmpopts.EquateEmpty()))
+			qt.Assert(t, qt.Equals(buf.CopyTo(have), len(want)))
+			qt.Assert(t, qt.CmpEquals(have, want, cmpopts.EquateEmpty()))
 		})
 	}
 }
@@ -143,9 +141,9 @@ func TestUnsafeBackingMemory(t *testing.T) {
 	marshalNative := func(t *testing.T, data any) []byte {
 		t.Helper()
 
-		var buf bytes.Buffer
-		qt.Assert(t, qt.IsNil(binary.Write(&buf, internal.NativeEndian, data)))
-		return buf.Bytes()
+		buf, err := binary.Append(nil, internal.NativeEndian, data)
+		qt.Assert(t, qt.IsNil(err))
+		return buf
 	}
 
 	for _, test := range []struct {