Skip to content

Commit

Permalink
sys: add windows support
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
lmb committed Feb 14, 2025
1 parent 6368b81 commit 1c307eb
Show file tree
Hide file tree
Showing 18 changed files with 425 additions and 146 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ jobs:
./internal/kallsyms
./internal/kconfig
./internal/linux
./internal/sys
./internal/sysenc
./internal/testutils
./internal/testutils/testmain
Expand Down
121 changes: 12 additions & 109 deletions internal/sys/fd.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
package sys

import (
"fmt"
"math"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"

"github.com/cilium/ebpf/internal/testutils/testmain"
"github.com/cilium/ebpf/internal/unix"
)

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)
Expand All @@ -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
}

Expand All @@ -39,49 +38,21 @@ 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
}
return uint32(fd.raw)
}

func (fd *FD) Close() error {
if fd.raw < 0 {
return nil
}

return unix.Close(fd.Disown())
func (fd *FD) String() string {
return strconv.FormatInt(int64(fd.raw), 10)
}

// Disown destroys the FD and returns its raw file descriptor without closing
Expand All @@ -90,76 +61,8 @@ func (fd *FD) Close() error {
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())
}
2 changes: 1 addition & 1 deletion internal/sys/fd_test.go → internal/sys/fd_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
70 changes: 70 additions & 0 deletions internal/sys/fd_other.go
Original file line number Diff line number Diff line change
@@ -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 nil if the FD is not valid.
func (fd *FD) File(name string) *os.File {
if fd.raw == invalidFd {
return nil
}

return os.NewFile(uintptr(fd.Disown()), name)
}
58 changes: 58 additions & 0 deletions internal/sys/fd_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package sys

import (
"fmt"
"os"

"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 bpf() syscall API can't deal with zero fds but we can't dup because
// the handle is managed by efW.
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 panics on Windows.
func (fd *FD) File(name string) *os.File {
panic("FD.File is not implementable on Windows")
}
2 changes: 2 additions & 0 deletions internal/sys/pinning.go → internal/sys/pinning_other.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !windows

package sys

import (
Expand Down
45 changes: 45 additions & 0 deletions internal/sys/pinning_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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(),
})
}

// TODO(windows): This should not allow replacing an existing object.
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
}
4 changes: 2 additions & 2 deletions internal/sys/ptr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions internal/sys/signals.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !windows

package sys

import (
Expand Down
Loading

0 comments on commit 1c307eb

Please sign in to comment.