Repository: cilium/ebpf
Pull request #1685 (2025-02-14 10:37:39)

sys: add windows support
More Windows pieces. Note that package ebpf, btf, link need follow up work.

efw: add wrappers for eBPF for Windows runtime

    Add basic infrastructure to call into the eBPF for Windows runtime. This
    uses runtime dynamic linking to call into ebpfapi.dll instead of relying on
    CGo. Ultimately this boils down to using syscall.SyscallN to call into
    specific functions in the DLL and looks very similar to what we do on Linux.

    The most important function is a wrapper for the bpf() function in 
    ebpfapi.dll. This is an ABI compatible re-implementation of the Linux bpf()
    syscall. Of course, not all of the syscall is implemented yet.

    In addition to that there are a variety of helper functions that do not have
    a bpf() equivalent and therefore need to be called directly.

    Signed-off-by: Lorenz Bauer <lmb@isovalent.com>

sys: add windows support

    The sys package is at the core of the library, so make it work on Windows to
    minimise changes to the rest of the code base.

    The first important piece is a custom FD abstraction. On Windows, operating
    system resources are identified by handles. The differences are large enough
    that the efW runtime decided to wrap them in a file descriptor abstraction,
    most of which is provided by the Universal C Runtime on Windows. Change our
    FD type to call into the efW runtime.

    Second, Windows does not have the equivalent of bpffs. Instead objects are
    stored in a global table, keyed by a string. This means that we can't use
    file system APIs to manipulate pinning.

    Third, the bpf() syscall is emulated by calling into ebpfapi.dll. This is
    the key piece which allows most code to work unchanged.

    Signed-off-by: Lorenz Bauer <lmb@isovalent.com>



Patch:
diff --git a/.gitattributes b/.gitattributes
index 113f97b..ea7c9a8 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,1 +1,4 @@
+# Force line ending normalisation
+*	text=auto
+# Show types.go in the PR diff view by default
 internal/sys/types.go linguist-generated=false
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fb56fb1..101dc2b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -264,8 +264,9 @@ jobs:
               owner: 'microsoft',
               repo: 'ebpf-for-windows',
               workflow_id: 'cicd.yml',
-              event: 'merge_group',
-              status: 'success',
+              event: 'push',
+              branch: 'main',
+              status: 'completed',
               per_page: 1
             });
 
@@ -329,6 +330,10 @@ jobs:
             exit 1
           }
 
+      - name: Add eBPF for Windows to PATH
+        shell: pwsh
+        run: echo "C:\Program Files\ebpf-for-windows\" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
+
       - uses: actions/checkout@v4
 
       - name: Set up Go
@@ -344,9 +349,11 @@ jobs:
           go test -short -count 1 -json
           ./asm
           ./internal
+          ./internal/efw
           ./internal/kallsyms
           ./internal/kconfig
           ./internal/linux
+          ./internal/sys
           ./internal/sysenc
           ./internal/testutils
           ./internal/testutils/testmain
diff --git a/internal/efw/enums.go b/internal/efw/enums.go
new file mode 100644
index 0000000..71320b6
--- /dev/null
+++ b/internal/efw/enums.go
@@ -0,0 +1,65 @@
+//go:build windows
+
+package efw
+
+import (
+	"syscall"
+	"unsafe"
+
+	"golang.org/x/sys/windows"
+)
+
+/*
+Converts an attach type enum into a GUID.
+
+	ebpf_result_t ebpf_get_ebpf_attach_type(
+		bpf_attach_type_t bpf_attach_type,
+		_Out_ ebpf_attach_type_t* ebpf_attach_type_t *ebpf_attach_type)
+*/
+var ebpfGetEbpfAttachTypeProc = newProc("ebpf_get_ebpf_attach_type")
+
+func EbpfGetEbpfAttachType(attachType uint32) (windows.GUID, error) {
+	addr, err := ebpfGetEbpfAttachTypeProc.Find()
+	if err != nil {
+		return windows.GUID{}, err
+	}
+
+	var attachTypeGUID windows.GUID
+	err = errorResult(syscall.SyscallN(addr,
+		uintptr(attachType),
+		uintptr(unsafe.Pointer(&attachTypeGUID)),
+	))
+	return attachTypeGUID, err
+}
+
+/*
+Retrieve a program type given a GUID.
+
+	bpf_prog_type_t ebpf_get_bpf_program_type(_In_ const ebpf_program_type_t* program_type)
+*/
+var ebpfGetBpfProgramTypeProc = newProc("ebpf_get_bpf_program_type")
+
+func EbpfGetBpfProgramType(programType windows.GUID) (uint32, error) {
+	addr, err := ebpfGetBpfProgramTypeProc.Find()
+	if err != nil {
+		return 0, err
+	}
+
+	return uint32Result(syscall.SyscallN(addr, uintptr(unsafe.Pointer(&programType)))), nil
+}
+
+/*
+Retrieve an attach type given a GUID.
+
+	bpf_attach_type_t ebpf_get_bpf_attach_type(_In_ const ebpf_attach_type_t* ebpf_attach_type)
+*/
+var ebpfGetBpfAttachTypeProc = newProc("ebpf_get_bpf_attach_type")
+
+func EbpfGetBpfAttachType(attachType windows.GUID) (uint32, error) {
+	addr, err := ebpfGetBpfAttachTypeProc.Find()
+	if err != nil {
+		return 0, err
+	}
+
+	return uint32Result(syscall.SyscallN(addr, uintptr(unsafe.Pointer(&attachType)))), nil
+}
diff --git a/internal/efw/fd.go b/internal/efw/fd.go
new file mode 100644
index 0000000..b0d0bcd
--- /dev/null
+++ b/internal/efw/fd.go
@@ -0,0 +1,34 @@
+//go:build windows
+
+package efw
+
+import (
+	"syscall"
+	"unsafe"
+)
+
+// ebpf_result_t ebpf_close_fd(fd_t fd)
+var ebpfCloseFdProc = newProc("ebpf_close_fd")
+
+func EbpfCloseFd(fd int) error {
+	addr, err := ebpfCloseFdProc.Find()
+	if err != nil {
+		return err
+	}
+
+	return errorResult(syscall.SyscallN(addr, uintptr(fd)))
+}
+
+// ebpf_result_t ebpf_duplicate_fd(fd_t fd, _Out_ fd_t* dup)
+var ebpfDuplicateFdProc = newProc("ebpf_duplicate_fd")
+
+func EbpfDuplicateFd(fd int) (int, error) {
+	addr, err := ebpfDuplicateFdProc.Find()
+	if err != nil {
+		return -1, err
+	}
+
+	var dup FD
+	err = errorResult(syscall.SyscallN(addr, uintptr(fd), uintptr(unsafe.Pointer(&dup))))
+	return int(dup), err
+}
diff --git a/internal/efw/module.go b/internal/efw/module.go
new file mode 100644
index 0000000..606d839
--- /dev/null
+++ b/internal/efw/module.go
@@ -0,0 +1,36 @@
+//go:build windows
+
+// Package efw contains support code for eBPF for Windows.
+package efw
+
+import (
+	"golang.org/x/sys/windows"
+)
+
+// module is the global handle for the eBPF for Windows user-space API.
+var module = windows.NewLazyDLL("ebpfapi.dll")
+
+// FD is the equivalent of fd_t.
+//
+// See https://github.com/microsoft/ebpf-for-windows/blob/54632eb360c560ebef2f173be1a4a4625d540744/include/ebpf_api.h#L24
+type FD int32
+
+// Size is the equivalent of size_t.
+//
+// This is correct on amd64 and arm64 according to tests on godbolt.org.
+type Size uint64
+
+// Int is the equivalent of int on MSVC (am64, arm64) and MinGW (gcc, clang).
+type Int int32
+
+// ObjectType is the equivalent of ebpf_object_type_t.
+//
+// See https://github.com/microsoft/ebpf-for-windows/blob/44f5de09ec0f3f7ad176c00a290c1cb7106cdd5e/include/ebpf_core_structs.h#L41
+type ObjectType uint32
+
+const (
+	EBPF_OBJECT_UNKNOWN ObjectType = iota
+	EBPF_OBJECT_MAP
+	EBPF_OBJECT_LINK
+	EBPF_OBJECT_PROGRAM
+)
diff --git a/internal/efw/object.go b/internal/efw/object.go
new file mode 100644
index 0000000..0695fe7
--- /dev/null
+++ b/internal/efw/object.go
@@ -0,0 +1,54 @@
+//go:build windows
+
+package efw
+
+import (
+	"syscall"
+	"unsafe"
+
+	"golang.org/x/sys/windows"
+)
+
+/*
+Retrieve object info and type from a fd.
+
+	ebpf_result_t ebpf_object_get_info_by_fd(
+		fd_t bpf_fd,
+		_Inout_updates_bytes_to_opt_(*info_size, *info_size) void* info,
+		_Inout_opt_ uint32_t* info_size,
+		_Out_opt_ ebpf_object_type_t* type)
+*/
+var ebpfObjectGetInfoByFdProc = newProc("ebpf_object_get_info_by_fd")
+
+func EbpfObjectGetInfoByFd(fd int, info unsafe.Pointer, info_size *uint32) (ObjectType, error) {
+	addr, err := ebpfObjectGetInfoByFdProc.Find()
+	if err != nil {
+		return 0, err
+	}
+
+	var objectType ObjectType
+	err = errorResult(syscall.SyscallN(addr,
+		uintptr(fd),
+		uintptr(info),
+		uintptr(unsafe.Pointer(info_size)),
+		uintptr(unsafe.Pointer(&objectType)),
+	))
+	return objectType, err
+}
+
+// ebpf_result_t ebpf_object_unpin(_In_z_ const char* path)
+var ebpfObjectUnpinProc = newProc("ebpf_object_unpin")
+
+func EbpfObjectUnpin(path string) error {
+	addr, err := ebpfObjectUnpinProc.Find()
+	if err != nil {
+		return err
+	}
+
+	pathBytes, err := windows.ByteSliceFromString(path)
+	if err != nil {
+		return err
+	}
+
+	return errorResult(syscall.SyscallN(addr, uintptr(unsafe.Pointer(&pathBytes[0]))))
+}
diff --git a/internal/efw/proc.go b/internal/efw/proc.go
new file mode 100644
index 0000000..8132990
--- /dev/null
+++ b/internal/efw/proc.go
@@ -0,0 +1,50 @@
+//go:build windows
+
+package efw
+
+import (
+	"errors"
+	"fmt"
+	"syscall"
+
+	"golang.org/x/sys/windows"
+)
+
+/*
+The BPF syscall wrapper which is ABI compatible with Linux.
+
+	int bpf(int cmd, union bpf_attr* attr, unsigned int size)
+*/
+var BPF = newProc("bpf")
+
+type proc struct {
+	proc *windows.LazyProc
+}
+
+func newProc(name string) proc {
+	return proc{module.NewProc(name)}
+}
+
+func (p proc) Find() (uintptr, error) {
+	if err := p.proc.Find(); err != nil {
+		if errors.Is(err, windows.ERROR_MOD_NOT_FOUND) {
+			return 0, fmt.Errorf("load %s: not found", module.Name)
+		}
+		return 0, err
+	}
+	return p.proc.Addr(), nil
+}
+
+// uint32Result wraps a function which returns a uint32_t.
+func uint32Result(r1, _ uintptr, _ syscall.Errno) uint32 {
+	return uint32(r1)
+}
+
+// errorResult wraps a function which returns ebpf_result_t.
+func errorResult(r1, _ uintptr, errNo syscall.Errno) error {
+	err := resultToError(Result(r1))
+	if err != nil && errNo != 0 {
+		return fmt.Errorf("%w (errno: %v)", err, errNo)
+	}
+	return err
+}
diff --git a/internal/efw/result.go b/internal/efw/result.go
new file mode 100644
index 0000000..3275941
--- /dev/null
+++ b/internal/efw/result.go
@@ -0,0 +1,57 @@
+//go:build windows
+
+package efw
+
+// See https://github.com/microsoft/ebpf-for-windows/blob/main/include/ebpf_result.h
+type Result int32
+
+//go:generate go run golang.org/x/tools/cmd/stringer@latest -tags windows -output result_string_windows.go -type=Result
+
+const (
+	EBPF_SUCCESS Result = iota
+	EBPF_VERIFICATION_FAILED
+	EBPF_JIT_COMPILATION_FAILED
+	EBPF_PROGRAM_LOAD_FAILED
+	EBPF_INVALID_FD
+	EBPF_INVALID_OBJECT
+	EBPF_INVALID_ARGUMENT
+	EBPF_OBJECT_NOT_FOUND
+	EBPF_OBJECT_ALREADY_EXISTS
+	EBPF_FILE_NOT_FOUND
+	EBPF_ALREADY_PINNED
+	EBPF_NOT_PINNED
+	EBPF_NO_MEMORY
+	EBPF_PROGRAM_TOO_LARGE
+	EBPF_RPC_EXCEPTION
+	EBPF_ALREADY_INITIALIZED
+	EBPF_ELF_PARSING_FAILED
+	EBPF_FAILED
+	EBPF_OPERATION_NOT_SUPPORTED
+	EBPF_KEY_NOT_FOUND
+	EBPF_ACCESS_DENIED
+	EBPF_BLOCKED_BY_POLICY
+	EBPF_ARITHMETIC_OVERFLOW
+	EBPF_EXTENSION_FAILED_TO_LOAD
+	EBPF_INSUFFICIENT_BUFFER
+	EBPF_NO_MORE_KEYS
+	EBPF_KEY_ALREADY_EXISTS
+	EBPF_NO_MORE_TAIL_CALLS
+	EBPF_PENDING
+	EBPF_OUT_OF_SPACE
+	EBPF_CANCELED
+	EBPF_INVALID_POINTER
+	EBPF_TIMEOUT
+	EBPF_STALE_ID
+	EBPF_INVALID_STATE
+)
+
+func (r Result) Error() string {
+	return r.String()
+}
+
+func resultToError(res Result) error {
+	if res == EBPF_SUCCESS {
+		return nil
+	}
+	return res
+}
diff --git a/internal/efw/result_string_windows.go b/internal/efw/result_string_windows.go
new file mode 100644
index 0000000..1e55b51
--- /dev/null
+++ b/internal/efw/result_string_windows.go
@@ -0,0 +1,57 @@
+// Code generated by "stringer -tags windows -output result_string_windows.go -type=Result"; DO NOT EDIT.
+
+package efw
+
+import "strconv"
+
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[EBPF_SUCCESS-0]
+	_ = x[EBPF_VERIFICATION_FAILED-1]
+	_ = x[EBPF_JIT_COMPILATION_FAILED-2]
+	_ = x[EBPF_PROGRAM_LOAD_FAILED-3]
+	_ = x[EBPF_INVALID_FD-4]
+	_ = x[EBPF_INVALID_OBJECT-5]
+	_ = x[EBPF_INVALID_ARGUMENT-6]
+	_ = x[EBPF_OBJECT_NOT_FOUND-7]
+	_ = x[EBPF_OBJECT_ALREADY_EXISTS-8]
+	_ = x[EBPF_FILE_NOT_FOUND-9]
+	_ = x[EBPF_ALREADY_PINNED-10]
+	_ = x[EBPF_NOT_PINNED-11]
+	_ = x[EBPF_NO_MEMORY-12]
+	_ = x[EBPF_PROGRAM_TOO_LARGE-13]
+	_ = x[EBPF_RPC_EXCEPTION-14]
+	_ = x[EBPF_ALREADY_INITIALIZED-15]
+	_ = x[EBPF_ELF_PARSING_FAILED-16]
+	_ = x[EBPF_FAILED-17]
+	_ = x[EBPF_OPERATION_NOT_SUPPORTED-18]
+	_ = x[EBPF_KEY_NOT_FOUND-19]
+	_ = x[EBPF_ACCESS_DENIED-20]
+	_ = x[EBPF_BLOCKED_BY_POLICY-21]
+	_ = x[EBPF_ARITHMETIC_OVERFLOW-22]
+	_ = x[EBPF_EXTENSION_FAILED_TO_LOAD-23]
+	_ = x[EBPF_INSUFFICIENT_BUFFER-24]
+	_ = x[EBPF_NO_MORE_KEYS-25]
+	_ = x[EBPF_KEY_ALREADY_EXISTS-26]
+	_ = x[EBPF_NO_MORE_TAIL_CALLS-27]
+	_ = x[EBPF_PENDING-28]
+	_ = x[EBPF_OUT_OF_SPACE-29]
+	_ = x[EBPF_CANCELED-30]
+	_ = x[EBPF_INVALID_POINTER-31]
+	_ = x[EBPF_TIMEOUT-32]
+	_ = x[EBPF_STALE_ID-33]
+	_ = x[EBPF_INVALID_STATE-34]
+}
+
+const _Result_name = "EBPF_SUCCESSEBPF_VERIFICATION_FAILEDEBPF_JIT_COMPILATION_FAILEDEBPF_PROGRAM_LOAD_FAILEDEBPF_INVALID_FDEBPF_INVALID_OBJECTEBPF_INVALID_ARGUMENTEBPF_OBJECT_NOT_FOUNDEBPF_OBJECT_ALREADY_EXISTSEBPF_FILE_NOT_FOUNDEBPF_ALREADY_PINNEDEBPF_NOT_PINNEDEBPF_NO_MEMORYEBPF_PROGRAM_TOO_LARGEEBPF_RPC_EXCEPTIONEBPF_ALREADY_INITIALIZEDEBPF_ELF_PARSING_FAILEDEBPF_FAILEDEBPF_OPERATION_NOT_SUPPORTEDEBPF_KEY_NOT_FOUNDEBPF_ACCESS_DENIEDEBPF_BLOCKED_BY_POLICYEBPF_ARITHMETIC_OVERFLOWEBPF_EXTENSION_FAILED_TO_LOADEBPF_INSUFFICIENT_BUFFEREBPF_NO_MORE_KEYSEBPF_KEY_ALREADY_EXISTSEBPF_NO_MORE_TAIL_CALLSEBPF_PENDINGEBPF_OUT_OF_SPACEEBPF_CANCELEDEBPF_INVALID_POINTEREBPF_TIMEOUTEBPF_STALE_IDEBPF_INVALID_STATE"
+
+var _Result_index = [...]uint16{0, 12, 36, 63, 87, 102, 121, 142, 163, 189, 208, 227, 242, 256, 278, 296, 320, 343, 354, 382, 400, 418, 440, 464, 493, 517, 534, 557, 580, 592, 609, 622, 642, 654, 667, 685}
+
+func (i Result) String() string {
+	if i < 0 || i >= Result(len(_Result_index)-1) {
+		return "Result(" + strconv.FormatInt(int64(i), 10) + ")"
+	}
+	return _Result_name[_Result_index[i]:_Result_index[i+1]]
+}
diff --git a/internal/efw/structs.go b/internal/efw/structs.go
new file mode 100644
index 0000000..558dbb8
--- /dev/null
+++ b/internal/efw/structs.go
@@ -0,0 +1,36 @@
+//go:build windows
+
+package efw
+
+import "golang.org/x/sys/windows"
+
+// https://github.com/microsoft/ebpf-for-windows/blob/95267a53b26c68a94145d1731e2a4c8b546034c3/include/ebpf_structs.h#L366
+const _BPF_OBJ_NAME_LEN = 64
+
+// See https://github.com/microsoft/ebpf-for-windows/blob/95267a53b26c68a94145d1731e2a4c8b546034c3/include/ebpf_structs.h#L372-L386
+type BpfMapInfo struct {
+	_    uint32                  ///< Map ID.
+	_    uint32                  ///< Type of map.
+	_    uint32                  ///< Size in bytes of a map key.
+	_    uint32                  ///< Size in bytes of a map value.
+	_    uint32                  ///< Maximum number of entries allowed in the map.
+	Name [_BPF_OBJ_NAME_LEN]byte ///< Null-terminated map name.
+	_    uint32                  ///< Map flags.
+
+	_ uint32 ///< ID of inner map template.
+	_ uint32 ///< Number of pinned paths.
+}
+
+// See https://github.com/microsoft/ebpf-for-windows/blob/95267a53b26c68a94145d1731e2a4c8b546034c3/include/ebpf_structs.h#L396-L410
+type BpfProgInfo struct {
+	_    uint32                  ///< Program ID.
+	_    uint32                  ///< Program type, if a cross-platform type.
+	_    uint32                  ///< Number of maps associated with this program.
+	_    uintptr                 ///< Pointer to caller-allocated array to fill map IDs into.
+	Name [_BPF_OBJ_NAME_LEN]byte ///< Null-terminated map name.
+
+	_ windows.GUID ///< Program type UUID.
+	_ windows.GUID ///< Attach type UUID.
+	_ uint32       ///< Number of pinned paths.
+	_ uint32       ///< Number of attached links.
+}
diff --git a/internal/sys/fd.go b/internal/sys/fd.go
index 7432813..7e769fb 100644
--- a/internal/sys/fd.go
+++ b/internal/sys/fd.go
@@ -1,13 +1,9 @@
 package sys
 
 import (
-	"fmt"
 	"math"
-	"os"
-	"path/filepath"
 	"runtime"
 	"strconv"
-	"strings"
 
 	"github.com/cilium/ebpf/internal/testutils/testmain"
 	"github.com/cilium/ebpf/internal/unix"
@@ -15,9 +11,12 @@ import (
 
 var ErrClosedFd = unix.EBADF
 
-type FD struct {
-	raw int
-}
+// A value for an invalid fd.
+//
+// Luckily this is consistent across Linux and Windows.
+//
+// See https://github.com/microsoft/ebpf-for-windows/blob/54632eb360c560ebef2f173be1a4a4625d540744/include/ebpf_api.h#L25
+const invalidFd = -1
 
 func newFD(value int) *FD {
 	testmain.TraceFD(value, 1)
@@ -30,7 +29,7 @@ func newFD(value int) *FD {
 // finalize is set as the FD's runtime finalizer and
 // sends a leak trace before calling FD.Close().
 func (fd *FD) finalize() {
-	if fd.raw < 0 {
+	if fd.raw == invalidFd {
 		return
 	}
 
@@ -39,36 +38,16 @@ func (fd *FD) finalize() {
 	_ = fd.Close()
 }
 
-// NewFD wraps a raw fd with a finalizer.
-//
-// You must not use the raw fd after calling this function, since the underlying
-// file descriptor number may change. This is because the BPF UAPI assumes that
-// zero is not a valid fd value.
-func NewFD(value int) (*FD, error) {
-	if value < 0 {
-		return nil, fmt.Errorf("invalid fd %d", value)
-	}
-
-	fd := newFD(value)
-	if value != 0 {
-		return fd, nil
-	}
-
-	dup, err := fd.Dup()
-	_ = fd.Close()
-	return dup, err
-}
-
 func (fd *FD) String() string {
 	return strconv.FormatInt(int64(fd.raw), 10)
 }
 
 func (fd *FD) Int() int {
-	return fd.raw
+	return int(fd.raw)
 }
 
 func (fd *FD) Uint() uint32 {
-	if fd.raw < 0 || int64(fd.raw) > math.MaxUint32 {
+	if fd.raw == invalidFd {
 		// Best effort: this is the number most likely to be an invalid file
 		// descriptor. It is equal to -1 (on two's complement arches).
 		return math.MaxUint32
@@ -76,90 +55,14 @@ func (fd *FD) Uint() uint32 {
 	return uint32(fd.raw)
 }
 
-func (fd *FD) Close() error {
-	if fd.raw < 0 {
-		return nil
-	}
-
-	return unix.Close(fd.Disown())
-}
-
 // Disown destroys the FD and returns its raw file descriptor without closing
 // it. After this call, the underlying fd is no longer tied to the FD's
 // lifecycle.
 func (fd *FD) Disown() int {
 	value := fd.raw
 	testmain.ForgetFD(value)
-	fd.raw = -1
+	fd.raw = invalidFd
 
 	runtime.SetFinalizer(fd, nil)
 	return value
 }
-
-func (fd *FD) Dup() (*FD, error) {
-	if fd.raw < 0 {
-		return nil, ErrClosedFd
-	}
-
-	// Always require the fd to be larger than zero: the BPF API treats the value
-	// as "no argument provided".
-	dup, err := unix.FcntlInt(uintptr(fd.raw), unix.F_DUPFD_CLOEXEC, 1)
-	if err != nil {
-		return nil, fmt.Errorf("can't dup fd: %v", err)
-	}
-
-	return newFD(dup), nil
-}
-
-// File takes ownership of FD and turns it into an [*os.File].
-//
-// You must not use the FD after the call returns.
-//
-// Returns nil if the FD is not valid.
-func (fd *FD) File(name string) *os.File {
-	if fd.raw < 0 {
-		return nil
-	}
-
-	return os.NewFile(uintptr(fd.Disown()), name)
-}
-
-// ObjGetTyped wraps [ObjGet] with a readlink call to extract the type of the
-// underlying bpf object.
-func ObjGetTyped(attr *ObjGetAttr) (*FD, ObjType, error) {
-	fd, err := ObjGet(attr)
-	if err != nil {
-		return nil, 0, err
-	}
-
-	typ, err := readType(fd)
-	if err != nil {
-		_ = fd.Close()
-		return nil, 0, fmt.Errorf("reading fd type: %w", err)
-	}
-
-	return fd, typ, nil
-}
-
-// readType returns the bpf object type of the file descriptor by calling
-// readlink(3). Returns an error if the file descriptor does not represent a bpf
-// object.
-func readType(fd *FD) (ObjType, error) {
-	s, err := os.Readlink(filepath.Join("/proc/self/fd/", fd.String()))
-	if err != nil {
-		return 0, fmt.Errorf("readlink fd %d: %w", fd.Int(), err)
-	}
-
-	s = strings.TrimPrefix(s, "anon_inode:")
-
-	switch s {
-	case "bpf-map":
-		return BPF_TYPE_MAP, nil
-	case "bpf-prog":
-		return BPF_TYPE_PROG, nil
-	case "bpf-link":
-		return BPF_TYPE_LINK, nil
-	}
-
-	return 0, fmt.Errorf("unknown type %s of fd %d", s, fd.Int())
-}
diff --git a/internal/sys/fd_other.go b/internal/sys/fd_other.go
new file mode 100644
index 0000000..4705739
--- /dev/null
+++ b/internal/sys/fd_other.go
@@ -0,0 +1,70 @@
+//go:build !windows
+
+package sys
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/cilium/ebpf/internal/unix"
+)
+
+type FD struct {
+	raw int
+}
+
+// NewFD wraps a raw fd with a finalizer.
+//
+// You must not use the raw fd after calling this function, since the underlying
+// file descriptor number may change. This is because the BPF UAPI assumes that
+// zero is not a valid fd value.
+func NewFD(value int) (*FD, error) {
+	if value < 0 {
+		return nil, fmt.Errorf("invalid fd %d", value)
+	}
+
+	fd := newFD(value)
+	if value != 0 {
+		return fd, nil
+	}
+
+	dup, err := fd.Dup()
+	_ = fd.Close()
+	return dup, err
+}
+
+func (fd *FD) Close() error {
+	if fd.raw < 0 {
+		return nil
+	}
+
+	return unix.Close(fd.Disown())
+}
+
+func (fd *FD) Dup() (*FD, error) {
+	if fd.raw < 0 {
+		return nil, ErrClosedFd
+	}
+
+	// Always require the fd to be larger than zero: the BPF API treats the value
+	// as "no argument provided".
+	dup, err := unix.FcntlInt(uintptr(fd.raw), unix.F_DUPFD_CLOEXEC, 1)
+	if err != nil {
+		return nil, fmt.Errorf("can't dup fd: %v", err)
+	}
+
+	return newFD(dup), nil
+}
+
+// File takes ownership of FD and turns it into an [*os.File].
+//
+// You must not use the FD after the call returns.
+//
+// Returns [ErrClosedFd] if the fd is not valid.
+func (fd *FD) File(name string) (*os.File, error) {
+	if fd.raw == invalidFd {
+		return nil, ErrClosedFd
+	}
+
+	return os.NewFile(uintptr(fd.Disown()), name), nil
+}
diff --git a/internal/sys/fd_windows.go b/internal/sys/fd_windows.go
new file mode 100644
index 0000000..f392763
--- /dev/null
+++ b/internal/sys/fd_windows.go
@@ -0,0 +1,58 @@
+package sys
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/cilium/ebpf/internal"
+	"github.com/cilium/ebpf/internal/efw"
+)
+
+// FD wraps a handle which is managed by the eBPF for Windows runtime.
+//
+// It is not equivalent to a real file descriptor or handle.
+type FD struct {
+	raw int
+}
+
+// NewFD wraps a raw fd with a finalizer.
+//
+// You must not use the raw fd after calling this function.
+func NewFD(value int) (*FD, error) {
+	if value == invalidFd {
+		return nil, fmt.Errorf("invalid fd %d", value)
+	}
+
+	if value == 0 {
+		// The efW runtime never uses zero fd it seems. No need to dup it.
+		return nil, fmt.Errorf("invalid zero fd")
+	}
+
+	return newFD(value), nil
+}
+
+func (fd *FD) Close() error {
+	if fd.raw == invalidFd {
+		return nil
+	}
+
+	return efw.EbpfCloseFd(fd.Disown())
+}
+
+func (fd *FD) Dup() (*FD, error) {
+	if fd.raw == invalidFd {
+		return nil, ErrClosedFd
+	}
+
+	dup, err := efw.EbpfDuplicateFd(fd.raw)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewFD(int(dup))
+}
+
+// File is not implemented.
+func (fd *FD) File(name string) (*os.File, error) {
+	return nil, fmt.Errorf("file from fd: %w", internal.ErrNotSupportedOnOS)
+}
diff --git a/internal/sys/pinning.go b/internal/sys/pinning_other.go
similarity index 98%
rename from internal/sys/pinning.go
rename to internal/sys/pinning_other.go
index 9a4c6c7..96ad43a 100644
--- a/internal/sys/pinning.go
+++ b/internal/sys/pinning_other.go
@@ -1,3 +1,5 @@
+//go:build !windows
+
 package sys
 
 import (
diff --git a/internal/sys/pinning_windows.go b/internal/sys/pinning_windows.go
new file mode 100644
index 0000000..c8ab685
--- /dev/null
+++ b/internal/sys/pinning_windows.go
@@ -0,0 +1,44 @@
+package sys
+
+import (
+	"errors"
+	"runtime"
+
+	"github.com/cilium/ebpf/internal/efw"
+)
+
+func Pin(currentPath, newPath string, fd *FD) error {
+	defer runtime.KeepAlive(fd)
+
+	if newPath == "" {
+		return errors.New("given pinning path cannot be empty")
+	}
+	if currentPath == newPath {
+		return nil
+	}
+
+	if currentPath == "" {
+		return ObjPin(&ObjPinAttr{
+			Pathname: NewStringPointer(newPath),
+			BpfFd:    fd.Uint(),
+		})
+	}
+
+	return ObjPin(&ObjPinAttr{
+		Pathname: NewStringPointer(newPath),
+		BpfFd:    fd.Uint(),
+	})
+}
+
+func Unpin(pinnedPath string) error {
+	if pinnedPath == "" {
+		return nil
+	}
+
+	err := efw.EbpfObjectUnpin(pinnedPath)
+	if err != nil && !errors.Is(err, efw.EBPF_KEY_NOT_FOUND) {
+		return err
+	}
+
+	return nil
+}
diff --git a/internal/sys/ptr.go b/internal/sys/ptr.go
index af0c014..5443dd1 100644
--- a/internal/sys/ptr.go
+++ b/internal/sys/ptr.go
@@ -29,12 +29,12 @@ func NewSlicePointerLen(buf []byte) (Pointer, uint32) {
 
 // NewStringPointer creates a 64-bit pointer from a string.
 func NewStringPointer(str string) Pointer {
-	p, err := unix.BytePtrFromString(str)
+	slice, err := unix.ByteSliceFromString(str)
 	if err != nil {
 		return Pointer{}
 	}
 
-	return Pointer{ptr: unsafe.Pointer(p)}
+	return Pointer{ptr: unsafe.Pointer(&slice[0])}
 }
 
 // NewStringSlicePointer allocates an array of Pointers to each string in the
diff --git a/internal/sys/signals.go b/internal/sys/signals.go
index e533719..e75e960 100644
--- a/internal/sys/signals.go
+++ b/internal/sys/signals.go
@@ -1,3 +1,5 @@
+//go:build !windows
+
 package sys
 
 import (
diff --git a/internal/sys/syscall.go b/internal/sys/syscall.go
index 4fdb74c..1093abf 100644
--- a/internal/sys/syscall.go
+++ b/internal/sys/syscall.go
@@ -12,36 +12,6 @@ import (
 // It is not the same as ENOTSUP or EOPNOTSUPP.
 const ENOTSUPP = unix.Errno(524)
 
-// BPF wraps SYS_BPF.
-//
-// Any pointers contained in attr must use the Pointer type from this package.
-func BPF(cmd Cmd, attr unsafe.Pointer, size uintptr) (uintptr, error) {
-	// Prevent the Go profiler from repeatedly interrupting the verifier,
-	// which could otherwise lead to a livelock due to receiving EAGAIN.
-	if cmd == BPF_PROG_LOAD || cmd == BPF_PROG_RUN {
-		maskProfilerSignal()
-		defer unmaskProfilerSignal()
-	}
-
-	for {
-		r1, _, errNo := unix.Syscall(unix.SYS_BPF, uintptr(cmd), uintptr(attr), size)
-		runtime.KeepAlive(attr)
-
-		// As of ~4.20 the verifier can be interrupted by a signal,
-		// and returns EAGAIN in that case.
-		if errNo == unix.EAGAIN && cmd == BPF_PROG_LOAD {
-			continue
-		}
-
-		var err error
-		if errNo != 0 {
-			err = wrappedErrno{errNo}
-		}
-
-		return r1, err
-	}
-}
-
 // Info is implemented by all structs that can be passed to the ObjInfo syscall.
 //
 //	MapInfo
diff --git a/internal/sys/syscall_other.go b/internal/sys/syscall_other.go
new file mode 100644
index 0000000..b99e6e4
--- /dev/null
+++ b/internal/sys/syscall_other.go
@@ -0,0 +1,84 @@
+//go:build !windows
+
+package sys
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"unsafe"
+
+	"github.com/cilium/ebpf/internal/unix"
+)
+
+// BPF wraps SYS_BPF.
+//
+// Any pointers contained in attr must use the Pointer type from this package.
+func BPF(cmd Cmd, attr unsafe.Pointer, size uintptr) (uintptr, error) {
+	// Prevent the Go profiler from repeatedly interrupting the verifier,
+	// which could otherwise lead to a livelock due to receiving EAGAIN.
+	if cmd == BPF_PROG_LOAD || cmd == BPF_PROG_RUN {
+		maskProfilerSignal()
+		defer unmaskProfilerSignal()
+	}
+
+	for {
+		r1, _, errNo := unix.Syscall(unix.SYS_BPF, uintptr(cmd), uintptr(attr), size)
+		runtime.KeepAlive(attr)
+
+		// As of ~4.20 the verifier can be interrupted by a signal,
+		// and returns EAGAIN in that case.
+		if errNo == unix.EAGAIN && cmd == BPF_PROG_LOAD {
+			continue
+		}
+
+		var err error
+		if errNo != 0 {
+			err = wrappedErrno{errNo}
+		}
+
+		return r1, err
+	}
+}
+
+// ObjGetTyped wraps [ObjGet] with a readlink call to extract the type of the
+// underlying bpf object.
+func ObjGetTyped(attr *ObjGetAttr) (*FD, ObjType, error) {
+	fd, err := ObjGet(attr)
+	if err != nil {
+		return nil, 0, err
+	}
+
+	typ, err := readType(fd)
+	if err != nil {
+		_ = fd.Close()
+		return nil, 0, fmt.Errorf("reading fd type: %w", err)
+	}
+
+	return fd, typ, nil
+}
+
+// readType returns the bpf object type of the file descriptor by calling
+// readlink(3). Returns an error if the file descriptor does not represent a bpf
+// object.
+func readType(fd *FD) (ObjType, error) {
+	s, err := os.Readlink(filepath.Join("/proc/self/fd/", fd.String()))
+	if err != nil {
+		return 0, fmt.Errorf("readlink fd %d: %w", fd.Int(), err)
+	}
+
+	s = strings.TrimPrefix(s, "anon_inode:")
+
+	switch s {
+	case "bpf-map":
+		return BPF_TYPE_MAP, nil
+	case "bpf-prog":
+		return BPF_TYPE_PROG, nil
+	case "bpf-link":
+		return BPF_TYPE_LINK, nil
+	}
+
+	return 0, fmt.Errorf("unknown type %s of fd %d", s, fd.Int())
+}
diff --git a/internal/sys/syscall_windows.go b/internal/sys/syscall_windows.go
new file mode 100644
index 0000000..08f7380
--- /dev/null
+++ b/internal/sys/syscall_windows.go
@@ -0,0 +1,69 @@
+package sys
+
+import (
+	"fmt"
+	"syscall"
+	"unsafe"
+
+	"golang.org/x/sys/windows"
+
+	"github.com/cilium/ebpf/internal"
+	"github.com/cilium/ebpf/internal/efw"
+	"github.com/cilium/ebpf/internal/unix"
+)
+
+// BPF calls the BPF syscall wrapper in ebpfapi.dll.
+//
+// Any pointers contained in attr must use the Pointer type from this package.
+//
+// The implementation lives in https://github.com/microsoft/ebpf-for-windows/blob/main/libs/api/bpf_syscall.cpp
+func BPF(cmd Cmd, attr unsafe.Pointer, size uintptr) (uintptr, error) {
+	// On Linux we need to guard against preemption by the profiler here. On
+	// Windows it seems like a cgocall may not be preempted:
+	// https://github.com/golang/go/blob/8b51146c698bcfcc2c2b73fa9390db5230f2ce0a/src/runtime/os_windows.go#L1240-L1246
+
+	addr, err := efw.BPF.Find()
+	if err != nil {
+		return 0, err
+	}
+
+	// Using [LazyProc.Call] forces attr to escape, which isn't the case when using syscall.Syscall directly.
+	r1, _, lastError := syscall.SyscallN(addr, uintptr(cmd), uintptr(attr), size)
+
+	if ret := int(efw.Int(r1)); ret < 0 {
+		errNo := unix.Errno(-ret)
+		if errNo == unix.EINVAL && lastError == windows.ERROR_CALL_NOT_IMPLEMENTED {
+			return 0, internal.ErrNotSupportedOnOS
+		}
+		return 0, wrappedErrno{errNo}
+	}
+
+	return r1, nil
+}
+
+// ObjGetTyped retrieves an pinned object and its type.
+func ObjGetTyped(attr *ObjGetAttr) (*FD, ObjType, error) {
+	fd, err := ObjGet(attr)
+	if err != nil {
+		return nil, 0, err
+	}
+
+	efwType, err := efw.EbpfObjectGetInfoByFd(fd.Int(), nil, nil)
+	if err != nil {
+		_ = fd.Close()
+		return nil, 0, err
+	}
+
+	switch efwType {
+	case efw.EBPF_OBJECT_UNKNOWN:
+		return fd, BPF_TYPE_UNSPEC, nil
+	case efw.EBPF_OBJECT_MAP:
+		return fd, BPF_TYPE_MAP, nil
+	case efw.EBPF_OBJECT_LINK:
+		return fd, BPF_TYPE_LINK, nil
+	case efw.EBPF_OBJECT_PROGRAM:
+		return fd, BPF_TYPE_PROG, nil
+	default:
+		return nil, 0, fmt.Errorf("unrecognized object type %v", efwType)
+	}
+}
diff --git a/internal/unix/strings_other.go b/internal/unix/strings_other.go
index 49e1fe2..76f367a 100644
--- a/internal/unix/strings_other.go
+++ b/internal/unix/strings_other.go
@@ -9,3 +9,7 @@ func BytePtrFromString(s string) (*byte, error) {
 func ByteSliceToString(s []byte) string {
 	return ""
 }
+
+func ByteSliceFromString(s string) ([]byte, error) {
+	return nil, errNonLinux()
+}
diff --git a/internal/unix/strings_windows.go b/internal/unix/strings_windows.go
index 7e3d4da..00af5a9 100644
--- a/internal/unix/strings_windows.go
+++ b/internal/unix/strings_windows.go
@@ -17,3 +17,7 @@ func BytePtrFromString(s string) (*byte, error) {
 func ByteSliceToString(s []byte) string {
 	return windows.ByteSliceToString(s)
 }
+
+func ByteSliceFromString(s string) ([]byte, error) {
+	return windows.ByteSliceFromString(s)
+}
diff --git a/internal/unix/types_linux.go b/internal/unix/types_linux.go
index 385adf0..7225220 100644
--- a/internal/unix/types_linux.go
+++ b/internal/unix/types_linux.go
@@ -168,6 +168,10 @@ func ByteSliceToString(s []byte) string {
 	return linux.ByteSliceToString(s)
 }
 
+func ByteSliceFromString(s string) ([]byte, error) {
+	return linux.ByteSliceFromString(s)
+}
+
 func Renameat2(olddirfd int, oldpath string, newdirfd int, newpath string, flags uint) error {
 	return linux.Renameat2(olddirfd, oldpath, newdirfd, newpath, flags)
 }
diff --git a/link/iter.go b/link/iter.go
index 0a39fae..2ba601b 100644
--- a/link/iter.go
+++ b/link/iter.go
@@ -75,7 +75,7 @@ func (it *Iter) Open() (io.ReadCloser, error) {
 		return nil, fmt.Errorf("can't create iterator: %w", err)
 	}
 
-	return fd.File("bpf_iter"), nil
+	return fd.File("bpf_iter")
 }
 
 // union bpf_iter_link_info.map
diff --git a/link/perf_event.go b/link/perf_event.go
index 7440e8b..42c1bc5 100644
--- a/link/perf_event.go
+++ b/link/perf_event.go
@@ -132,7 +132,7 @@ func (pl *perfEventLink) PerfEvent() (*os.File, error) {
 		return nil, err
 	}
 
-	return fd.File("perf-event"), nil
+	return fd.File("perf-event")
 }
 
 func (pl *perfEventLink) Info() (*Info, error) {
@@ -209,7 +209,7 @@ func (pi *perfEventIoctl) PerfEvent() (*os.File, error) {
 		return nil, err
 	}
 
-	return fd.File("perf-event"), nil
+	return fd.File("perf-event")
 }
 
 // attach the given eBPF prog to the perf event stored in pe.


Test patch:
diff --git a/internal/efw/proc_test.go b/internal/efw/proc_test.go
new file mode 100644
index 0000000..ff9b868
--- /dev/null
+++ b/internal/efw/proc_test.go
@@ -0,0 +1,23 @@
+//go:build windows
+
+package efw
+
+import (
+	"testing"
+
+	"github.com/go-quicktest/qt"
+)
+
+func TestNewProc(t *testing.T) {
+	_, err := newProc("a_function_which_doesnt_exist").Find()
+	qt.Assert(t, qt.ErrorMatches(err, ".* a_function_which_doesnt_exist .*"))
+}
+
+func TestCall(t *testing.T) {
+	var err error
+	allocs := testing.AllocsPerRun(10, func() {
+		_, err = EbpfGetEbpfAttachType(1)
+	})
+	qt.Assert(t, qt.IsNil(err))
+	qt.Assert(t, qt.Equals(allocs, 0))
+}
diff --git a/internal/efw/result_test.go b/internal/efw/result_test.go
new file mode 100644
index 0000000..81c9318
--- /dev/null
+++ b/internal/efw/result_test.go
@@ -0,0 +1,28 @@
+//go:build windows
+
+package efw
+
+import (
+	"testing"
+
+	"github.com/go-quicktest/qt"
+)
+
+func TestResultToError(t *testing.T) {
+	qt.Assert(t, qt.IsNil(resultToError(EBPF_SUCCESS)))
+	qt.Assert(t, qt.IsNotNil(resultToError(EBPF_ACCESS_DENIED)))
+
+	// Ensure that common results do not allocate.
+	for _, result := range []Result{
+		EBPF_SUCCESS,
+		EBPF_NO_MORE_KEYS,
+		EBPF_KEY_NOT_FOUND,
+	} {
+		t.Run(result.String(), func(t *testing.T) {
+			allocs := testing.AllocsPerRun(1, func() {
+				_ = resultToError(result)
+			})
+			qt.Assert(t, qt.Equals(allocs, 0.0))
+		})
+	}
+}
diff --git a/internal/sys/fd_test.go b/internal/sys/fd_linux_test.go
similarity index 84%
rename from internal/sys/fd_test.go
rename to internal/sys/fd_linux_test.go
index 2600db0..7e9c7ff 100644
--- a/internal/sys/fd_test.go
+++ b/internal/sys/fd_linux_test.go
@@ -37,7 +37,7 @@ func reserveFdZero() {
 
 func TestFD(t *testing.T) {
 	_, err := NewFD(-1)
-	qt.Assert(t, qt.IsNotNil(err), qt.Commentf("negative fd should be rejected"))
+	qt.Assert(t, qt.IsNotNil(err), qt.Commentf("invalid fd should be rejected"))
 
 	fd, err := NewFD(0)
 	qt.Assert(t, qt.IsNil(err))
@@ -53,12 +53,16 @@ func TestFD(t *testing.T) {
 
 func TestFDFile(t *testing.T) {
 	fd := newFD(openFd(t))
-	file := fd.File("test")
+	file, err := fd.File("test")
+	qt.Assert(t, qt.IsNil(err))
 	qt.Assert(t, qt.IsNotNil(file))
+
 	qt.Assert(t, qt.IsNil(file.Close()))
-	qt.Assert(t, qt.IsNil(fd.File("closed")))
 
-	_, err := fd.Dup()
+	_, err = fd.File("closed")
+	qt.Assert(t, qt.ErrorIs(err, ErrClosedFd))
+
+	_, err = fd.Dup()
 	qt.Assert(t, qt.ErrorIs(err, ErrClosedFd))
 }
 
diff --git a/internal/sys/signals_test.go b/internal/sys/signals_test.go
index 0e8dcc7..3e038a0 100644
--- a/internal/sys/signals_test.go
+++ b/internal/sys/signals_test.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package sys
 
 import (
@@ -5,9 +7,9 @@ import (
 	"testing"
 	"unsafe"
 
-	"github.com/cilium/ebpf/internal/unix"
-
 	"github.com/go-quicktest/qt"
+
+	"github.com/cilium/ebpf/internal/unix"
 )
 
 func TestSigset(t *testing.T) {
diff --git a/internal/sys/syscall_test.go b/internal/sys/syscall_test.go
index 9429c2c..652ec40 100644
--- a/internal/sys/syscall_test.go
+++ b/internal/sys/syscall_test.go
@@ -2,7 +2,9 @@ package sys
 
 import (
 	"errors"
+	"math"
 	"testing"
+	"unsafe"
 
 	"github.com/cilium/ebpf/internal/testutils/testmain"
 	"github.com/cilium/ebpf/internal/unix"
@@ -10,6 +12,28 @@ import (
 	"github.com/go-quicktest/qt"
 )
 
+func TestBPF(t *testing.T) {
+	fd, err := MapCreate(&MapCreateAttr{
+		MapType:    BPF_MAP_TYPE_HASH,
+		KeySize:    4,
+		ValueSize:  4,
+		MaxEntries: 1,
+	})
+	qt.Assert(t, qt.IsNil(err))
+	qt.Assert(t, qt.IsNil(fd.Close()))
+}
+
+func TestBPFAllocations(t *testing.T) {
+	n := testing.AllocsPerRun(10, func() {
+		var attr struct {
+			Foo uint64
+		}
+
+		BPF(math.MaxUint32, unsafe.Pointer(&attr), 0)
+	})
+	qt.Assert(t, qt.Equals(n, 0))
+}
+
 func TestObjName(t *testing.T) {
 	name := NewObjName("more_than_16_characters_long")
 	if name[len(name)-1] != 0 {
diff --git a/map_test.go b/map_test.go
index a437a26..56e65b9 100644
--- a/map_test.go
+++ b/map_test.go
@@ -11,14 +11,14 @@ import (
 	"testing"
 	"unsafe"
 
+	"github.com/go-quicktest/qt"
+
 	"github.com/cilium/ebpf/asm"
 	"github.com/cilium/ebpf/btf"
 	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/sys"
 	"github.com/cilium/ebpf/internal/testutils"
 	"github.com/cilium/ebpf/internal/unix"
-
-	"github.com/go-quicktest/qt"
 )
 
 var (