Help us shape the future of Go! Take the 2026 Go Developer Survey

uffd

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 13, 2026 License: Apache-2.0 Imports: 16 Imported by: 1

README

linux-uffd-go

linux-uffd-go is a Go package for working with Linux userfaultfd(2) without cgo at runtime.

  • Module path: github.com/niallnsec/linux-uffd-go
  • Package name: uffd
  • Target OS: Linux

The package is intended for applications that need to lazily populate memory from files, network-backed data sources, or other custom paging logic while staying in pure Go.

Features

  • Pure-Go runtime implementation
  • Linux kernel struct definitions generated with go tool cgo -godefs
  • Simple convenience API for common io.ReaderAt paging
  • Generic handler API for custom fault-resolution strategies
  • Unix socket helpers for passing userfaultfd file descriptors between processes
  • Default-safe UFFDIO_COPY population for caller-provided buffers
  • Explicit UFFDIO_MOVE opt-in for sacrificial mmap-backed buffers

Installation

go get github.com/niallnsec/linux-uffd-go@latest

Kernel support

  • Minimum supported kernel: Linux 4.3
  • CheckSupport() verifies that the running kernel supports the minimum feature set required by this package
  • The convenience helpers use UFFDIO_MOVE for their internal mmap scratch pages when the kernel advertises UFFD_FEATURE_MOVE
  • Caller-provided buffers use UFFDIO_COPY by default
  • PopulateMove allows callers to opt into UFFDIO_MOVE for buffers they know are safe to move

Package overview

The package exposes two levels of API.

1. Convenience helpers

For the common case where you want to register a memory region and populate it from an io.ReaderAt, use:

  • Register
  • Registration
  • Handle
  • HandleContext

Register returns a *Registration; call Close when you are done with the mapping and its userfaultfd.

2. Generic custom handler API

For custom paging strategies, use:

  • NewHandler
  • Handler.Serve
  • Fault
  • Resolver
  • Populate
  • PopulateMove
  • AllocateMoveBuffer

When you pass a userfaultfd between processes with the Unix socket helpers, the client and manager processes must have the same bit-ness. Mixed 32-bit and 64-bit setups are not supported.

This is the intended extension point when you want to:

  • populate pages from multiple sources
  • add tracing or metrics
  • prefetch or batch data
  • inject custom retry or cancellation behaviour

Examples

Runnable example programs live in examples/. That directory has its own go.mod, so example-only dependencies such as the AWS SDK for Go v2 do not affect the main module. See examples/README.md for file-backed, custom-handler, transferred-userfaultfd, S3-backed, and benchmark examples.

Basic example

package main

import (
	"bytes"
	"context"
	"log"

	uffd "github.com/niallnsec/linux-uffd-go"
)

func main() {
	if err := uffd.CheckSupport(); err != nil {
		log.Fatal(err)
	}

	payload := bytes.Repeat([]byte("hello"), 1024)
	reg, err := uffd.Register(len(payload))
	if err != nil {
		log.Fatal(err)
	}
	defer reg.Close()

	buf := reg.Bytes()

	go func() {
		if err := uffd.HandleContext(context.Background(), reg.UFFD(), reg.Start(), bytes.NewReader(payload)); err != nil {
			log.Fatal(err)
		}
	}()

	_ = buf
}

Custom handler example

The generic handler avoids exposing unsafe, raw fault messages, or ioctl details to ordinary callers:

handler := uffd.NewHandler(fd, nil)

err := handler.Serve(ctx, func(fault uffd.Fault, resolver uffd.Resolver) error {
	page := make([]byte, os.Getpagesize())
	pageAddress := fault.Address &^ uintptr(os.Getpagesize()-1)
	_, err := src.ReadAt(page, int64(pageAddress)-int64(start))
	if err != nil && !errors.Is(err, io.EOF) {
		return err
	}
	_, err = resolver.Populate(pageAddress, page)
	return err
})

If your handler wants to opt into UFFDIO_MOVE, allocate its scratch pages with AllocateMoveBuffer; it returns a private anonymous mmap that is safe to sacrifice to MOVE. Then call PopulateMove explicitly:

page, err := uffd.AllocateMoveBuffer(os.Getpagesize())
if err != nil {
	return err
}
defer page.Close()

err = handler.Serve(ctx, func(fault uffd.Fault, resolver uffd.Resolver) error {
	pageAddress := fault.Address &^ uintptr(os.Getpagesize()-1)
	clear(page.Bytes())
	_, err := src.ReadAt(page.Bytes(), int64(pageAddress)-int64(start))
	if err != nil && !errors.Is(err, io.EOF) {
		return err
	}
	_, err = resolver.PopulateMove(pageAddress, page.Bytes())
	return err
})

Why MOVE can be more efficient than COPY

UFFDIO_COPY resolves a fault by copying bytes from your source buffer into the faulting range. That is the safest default because the source buffer keeps its original mapping and contents.

UFFDIO_COPY

userspace scratch buffer           registered faulting address
+------------------------+         +------------------------+
| page bytes             | --copy->| new page with bytes    |
+------------------------+         +------------------------+

source buffer still points at the same backing page

UFFDIO_MOVE is different. Instead of copying page contents, the kernel can move the page itself by rewriting page-table entries (PTEs): the destination PTE is updated to point at the page that used to back the source buffer, and the source PTE is removed. Avoiding that extra page-sized memcpy can reduce memory bandwidth, cache churn, and latency on the fault path.

UFFDIO_MOVE (conceptually)

before:
  source VA -> source PTE -> physical page P
  dest   VA -> missing PTE

after:
  source VA -> no longer backed by page P
  dest   VA -> dest   PTE -> physical page P

That PTE remapping is exactly why the source must be a sacrificial anonymous mapping. After a successful MOVE, the old source virtual address should no longer be treated as owning the original bytes. Because that address still sits inside the same anonymous mapping, touching it again faults in zero-filled anonymous memory, which is what makes a single scratch buffer reusable.

This package therefore requires an anonymous private mmap for MOVE buffers. AllocateMoveBuffer creates a MAP_PRIVATE|MAP_ANONYMOUS region so the kernel can treat each page as an exclusive anonymous page, detach it from the temporary scratch address, and reattach it to the faulting address.

safe source for UFFDIO_MOVE in this package

temporary scratch VA -> private anonymous page
                                      |
                                      | PTE ownership is transferred
                                      v
registered fault VA  -> receives that same page

A normal Go slice is not a safe MOVE source:

  • it usually points into Go-managed heap pages that are not disposable scratch pages
  • MOVE changes virtual-to-physical page ownership rather than copying bytes
  • later reads from the original source address may observe a different page
  • file-backed or shared mappings do not fit the private-anonymous-page model

That is why:

  • the convenience helpers stage data in an internal anonymous mmap scratch page
  • Populate remains the default for caller-provided buffers
  • PopulateMove is the opt-in API for callers that already have a disposable anonymous mapping
  • when MOVE is unavailable or rejected for the requested span, the implementation falls back to UFFDIO_COPY

Open flags

To avoid requiring callers to import golang.org/x/sys/unix just to open a userfaultfd, the package exposes flag aliases for Open:

  • UFFD_O_CLOEXEC
  • UFFD_O_NONBLOCK
  • UFFD_OPEN_DEFAULT
  • UFFD_USER_MODE_ONLY

UFFD_OPEN_DEFAULT enables the package's normal close-on-exec and non-blocking behavior. Add UFFD_USER_MODE_ONLY explicitly when you need a descriptor that only handles user-mode faults.

Examples:

fd, err := uffd.Open(uffd.UFFD_OPEN_DEFAULT)

fd, err := uffd.Open(uffd.UFFD_OPEN_DEFAULT | uffd.UFFD_USER_MODE_ONLY)

reg, err := uffd.RegisterWithFlags(
	len(payload),
	uffd.UFFD_OPEN_DEFAULT|uffd.UFFD_USER_MODE_ONLY,
)

Generated Linux types

The Linux structs used by this package are generated using the same godefs-style workflow used by golang.org/x/sys/unix.

To regenerate them on Linux:

go generate ./...

Testing

Unit tests:

go test ./...

Linux integration tests:

LINUX_UFFD_GO_INTEGRATION=1 go test ./... -count=1

On non-Linux systems, use the included Docker harness:

make docker-test
make docker-test-integration

The Docker targets default to linux/arm64 on arm64/aarch64 hosts and to linux/amd64 otherwise. You can still override the platform explicitly:

make docker-test DOCKER_PLATFORM=linux/amd64

The integration target runs the container with:

  • --cap-add SYS_PTRACE
  • --security-opt seccomp=unconfined
Custom seccomp profile alternative

Instead of running the container with seccomp=unconfined, you can start from Docker's default seccomp profile and add an allow rule for the userfaultfd syscall.

The additional rule looks like this:

{
  "names": ["userfaultfd"],
  "action": "SCMP_ACT_ALLOW",
  "args": [],
  "comment": "Allow userfaultfd for linux-uffd-go",
  "includes": {},
  "excludes": {}
}

You would then run the container with that custom profile instead of seccomp=unconfined.

vm.unprivileged_userfaultfd

Linux exposes a sysctl that controls whether unprivileged processes may create userfaultfd file descriptors that can handle kernel page faults:

  • /proc/sys/vm/unprivileged_userfaultfd
  • sysctl vm.unprivileged_userfaultfd

Typical values:

  • 1: unprivileged processes may create userfaultfd file descriptors that can handle kernel page faults
  • 0: unprivileged processes must either:
    • have CAP_SYS_PTRACE, or
    • use UFFD_USER_MODE_ONLY to request a userspace-fault-only file descriptor

This matters in containers too. The default configuration for most linux hosts, vm.unprivileged_userfaultfd=0, is the reason why CAP_SYS_PTRACE is required for the integration tests. The convenience helpers in this package use UFFD_OPEN_DEFAULT by default, and if the userfaultfd(2) syscall returns EPERM they also try /dev/userfaultfd with USERFAULTFD_IOC_NEW. If your environment only permits userspace faults, opt into UFFD_USER_MODE_ONLY explicitly with Open, RegisterWithFlags, and CheckSupportWithFlags.

If you need a temporary override on a Linux host, the usual form is:

sudo sysctl -w vm.unprivileged_userfaultfd=1

To inspect the current value:

sysctl vm.unprivileged_userfaultfd

Attribution

This project was inspired by and partially based on:

The Linux type-generation workflow also follows the approach used by the Go authors in golang.org/x/sys/unix.

See NOTICE for detailed attribution.

License

Apache License, Version 2.0. See LICENSE.

Documentation

Overview

Package uffd provides Linux userfaultfd helpers without cgo at runtime.

Index

Constants

View Source
const (
	// NR_userfaultfd is the Linux syscall number for creating a userfaultfd
	// descriptor directly.
	NR_userfaultfd = uintptr(0)

	// UFFD_O_CLOEXEC requests that Open mark the returned descriptor
	// close-on-exec.
	UFFD_O_CLOEXEC = uintptr(0x80000)

	// UFFD_O_NONBLOCK requests that Open return a non-blocking descriptor.
	UFFD_O_NONBLOCK = uintptr(0x800)

	// UFFD_OPEN_DEFAULT keeps the returned fd close-on-exec and non-blocking.
	//
	// Callers that specifically want a userspace-fault-only descriptor can add
	// UFFD_USER_MODE_ONLY explicitly.
	UFFD_OPEN_DEFAULT = uffdSharedOpenFlags

	// UFFD_API is the userfaultfd API version passed to UFFDIO_API during
	// descriptor initialisation.
	UFFD_API = uint64(0xAA)

	// UFFD_EVENT_PAGEFAULT identifies a missing-page event in UffdMsg.Event.
	UFFD_EVENT_PAGEFAULT = uint8(0x12)

	// UFFDIO_REGISTER_MODE_MISSING requests missing-page tracking when
	// registering a virtual address range.
	UFFDIO_REGISTER_MODE_MISSING = uint64(1 << 0)

	// UFFDIO_API is the ioctl number used to negotiate the userfaultfd API
	// version and feature bits.
	UFFDIO_API = uintptr(0)

	// UFFDIO_REGISTER is the ioctl number used to register a virtual address
	// range for userfaultfd tracking.
	UFFDIO_REGISTER = uintptr(0)

	// UFFDIO_COPY is the ioctl number used to resolve a fault by copying bytes
	// into the registered range.
	UFFDIO_COPY = uintptr(0)

	// UFFDIO_MOVE is the ioctl number used to resolve a fault by moving page
	// contents into the registered range.
	UFFDIO_MOVE = uintptr(0)
)
View Source
const (
	// UFFD_FEATURE_MOVE is the feature bit negotiated through UFFDIO_API that
	// enables UFFDIO_MOVE support for a descriptor.
	UFFD_FEATURE_MOVE = uint64(1 << 16)

	// UFFD_USER_MODE_ONLY requests a descriptor that handles faults originating
	// in user mode and skips kernel-mode faults.
	UFFD_USER_MODE_ONLY = uintptr(1)
)

Variables

View Source
var (
	// ErrUnexpectedEventType indicates that a userfaultfd event other than a
	// missing-page fault was received by Handle.
	ErrUnexpectedEventType = errors.New("unexpected event type")

	// ErrInvalidFDsLength indicates that a Unix rights message contained an
	// unexpected number of file descriptors.
	ErrInvalidFDsLength = errors.New("invalid file descriptors length")

	// ErrUnsupportedPlatform indicates that the current GOOS/GOARCH does not
	// support the low-level userfaultfd implementation in this package.
	ErrUnsupportedPlatform = errors.New("userfaultfd is unsupported on this platform")

	// ErrInvalidPopulateRange indicates that Populate was asked to operate on a
	// destination or buffer length that is not page aligned.
	ErrInvalidPopulateRange = errors.New("populate range must be page aligned")
)

Functions

func CheckSupport

func CheckSupport() error

CheckSupport always returns ErrUnsupportedPlatform on non-Linux builds.

func CheckSupportWithFlags

func CheckSupportWithFlags(flags uintptr) error

CheckSupportWithFlags always returns ErrUnsupportedPlatform on non-Linux builds.

func GetCopyBytes

func GetCopyBytes(copy *UffdioCopy) int64

GetCopyBytes returns the completion count from a placeholder UFFDIO_COPY request.

func GetMoveBytes

func GetMoveBytes(move *UffdioMove) int64

GetMoveBytes returns the completion count from a placeholder UFFDIO_MOVE request.

func GetMsgArg

func GetMsgArg(msg *UffdMsg) [24]byte

GetMsgArg returns the raw 24-byte argument payload from a placeholder uffd_msg.

func GetMsgEvent

func GetMsgEvent(msg *UffdMsg) uint8

GetMsgEvent returns the event discriminator from a placeholder uffd_msg.

func GetPagefaultAddress

func GetPagefaultAddress(pagefault *UffdPagefault) uint64

GetPagefaultAddress returns the fault address from a placeholder pagefault payload.

func Handle

func Handle(uffd UFFD, start uintptr, src io.ReaderAt) error

Handle always returns ErrUnsupportedPlatform on non-Linux builds.

func HandleContext

func HandleContext(ctx context.Context, uffd UFFD, start uintptr, src io.ReaderAt) error

HandleContext always returns ErrUnsupportedPlatform on non-Linux builds.

func ReceiveFDs

func ReceiveFDs(conn *net.UnixConn, num int) ([]uintptr, error)

ReceiveFDs always returns ErrUnsupportedPlatform on non-Linux builds.

func SendFDs

func SendFDs(conn *net.UnixConn, fds []uintptr) error

SendFDs always returns ErrUnsupportedPlatform on non-Linux builds.

func SendUFFD

func SendUFFD(conn *net.UnixConn, uffd UFFD, start uintptr) error

SendUFFD always returns ErrUnsupportedPlatform on non-Linux builds.

Types

type Fault

type Fault struct {
	Address uintptr
}

Fault describes a missing-page event delivered by userfaultfd on Linux.

On non-Linux builds it exists only so the public API remains type-compatible.

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler serves missing-page events and delegates page population to a callback.

On non-Linux builds it exists for API compatibility and always reports ErrUnsupportedPlatform when used.

func NewHandler

func NewHandler(uffd UFFD, opts *HandlerOptions) *Handler

NewHandler creates a placeholder handler that reports ErrUnsupportedPlatform when used on non-Linux builds.

func (*Handler) Serve

func (h *Handler) Serve(ctx context.Context, onFault func(Fault, Resolver) error) error

Serve always returns ErrUnsupportedPlatform on non-Linux builds.

type HandlerOptions

type HandlerOptions struct {
	PollTimeout         time.Duration
	DisableLockOSThread bool
}

HandlerOptions configures a generic fault handler.

On non-Linux builds it is retained for API compatibility only.

type MoveBuffer

type MoveBuffer []byte

MoveBuffer is a placeholder mmap-backed buffer type on non-Linux builds.

func AllocateMoveBuffer

func AllocateMoveBuffer(length int) (MoveBuffer, error)

AllocateMoveBuffer always returns ErrUnsupportedPlatform on non-Linux builds.

func (MoveBuffer) Bytes

func (b MoveBuffer) Bytes() []byte

Bytes always returns nil on non-Linux builds.

func (MoveBuffer) Close

func (b MoveBuffer) Close() error

Close is a no-op on non-Linux builds.

type Registration

type Registration struct {
	// contains filtered or unexported fields
}

Registration owns an mmap-backed region registered with a userfaultfd and the descriptor that tracks it. Call Close when the registration is no longer needed.

func Register

func Register(length int) (*Registration, error)

Register always returns ErrUnsupportedPlatform on non-Linux builds.

func RegisterWithFlags

func RegisterWithFlags(length int, flags uintptr) (*Registration, error)

RegisterWithFlags always returns ErrUnsupportedPlatform on non-Linux builds.

func (*Registration) Bytes

func (r *Registration) Bytes() []byte

Bytes returns the registered region clipped to the originally requested length.

func (*Registration) Close

func (r *Registration) Close() error

Close is a no-op on non-Linux builds.

func (*Registration) Start

func (r *Registration) Start() uintptr

Start returns the base address used to translate fault offsets back into reader offsets.

func (*Registration) UFFD

func (r *Registration) UFFD() UFFD

UFFD returns the userfaultfd associated with the registration.

type Resolver

type Resolver struct {
	// contains filtered or unexported fields
}

Resolver resolves faults by populating pages in the registered region.

On non-Linux builds its methods always return ErrUnsupportedPlatform.

func (Resolver) Populate

func (r Resolver) Populate(address uintptr, data []byte) (int64, error)

Populate always returns ErrUnsupportedPlatform on non-Linux builds.

func (Resolver) PopulateMove

func (r Resolver) PopulateMove(address uintptr, data []byte) (int64, error)

PopulateMove always returns ErrUnsupportedPlatform on non-Linux builds.

type UFFD

type UFFD uintptr

UFFD is a placeholder userfaultfd descriptor type on non-Linux builds.

func Open

func Open(flags uintptr) (UFFD, error)

Open always returns ErrUnsupportedPlatform on non-Linux builds.

func ReceiveUFFD

func ReceiveUFFD(conn *net.UnixConn) (UFFD, uintptr, error)

ReceiveUFFD always returns ErrUnsupportedPlatform on non-Linux builds.

func (UFFD) Close

func (u UFFD) Close() error

Close is a no-op on non-Linux builds.

func (UFFD) Init

func (u UFFD) Init(features uint64) error

Init always returns ErrUnsupportedPlatform on non-Linux builds.

func (UFFD) Register

func (u UFFD) Register(start, length uintptr, mode uint64) error

Register always returns ErrUnsupportedPlatform on non-Linux builds.

type UffdMsg

type UffdMsg struct {
	Event     uint8
	Reserved1 uint8
	Reserved2 uint16
	Reserved3 uint32
	Arg       [24]byte
}

UffdMsg mirrors Linux struct uffd_msg and carries events read from a userfaultfd descriptor.

type UffdPagefault

type UffdPagefault struct {
	Flags     uint64
	Address   uint64
	Feat      [4]byte
	Pad_cgo_0 [4]byte
}

UffdPagefault mirrors the pagefault payload stored in UffdMsg.Arg for UFFD_EVENT_PAGEFAULT events.

type UffdioAPI

type UffdioAPI struct {
	Api      uint64
	Features uint64
	Ioctls   uint64
}

UffdioAPI mirrors Linux struct uffdio_api, used with UFFDIO_API to negotiate feature bits and discover supported ioctls.

func NewUffdioAPI

func NewUffdioAPI(api, features uint64) UffdioAPI

NewUffdioAPI builds a placeholder UFFDIO_API request struct on non-Linux builds so tests and callers can still construct values.

type UffdioCopy

type UffdioCopy struct {
	Dst  uint64
	Src  uint64
	Len  uint64
	Mode uint64
	Copy int64
}

UffdioCopy mirrors Linux struct uffdio_copy, used with UFFDIO_COPY to resolve missing pages from a source buffer.

func NewUffdioCopy

func NewUffdioCopy(b []byte, address uint64, length uint64, mode uint64, copied int64) UffdioCopy

NewUffdioCopy builds a placeholder UFFDIO_COPY request struct on non-Linux builds.

type UffdioMove

type UffdioMove struct {
	Dst  uint64
	Src  uint64
	Len  uint64
	Mode uint64
	Move int64
}

UffdioMove mirrors Linux struct uffdio_move, used with UFFDIO_MOVE to move page contents into a registered range.

func NewUffdioMove

func NewUffdioMove(b []byte, address uint64, length uint64, mode uint64, moved int64) UffdioMove

NewUffdioMove builds a placeholder UFFDIO_MOVE request struct on non-Linux builds.

type UffdioRange

type UffdioRange struct {
	Start uint64
	Len   uint64
}

UffdioRange mirrors Linux struct uffdio_range and identifies a virtual address range in a process.

type UffdioRegister

type UffdioRegister struct {
	Range  UffdioRange
	Mode   uint64
	Ioctls uint64
}

UffdioRegister mirrors Linux struct uffdio_register, used with UFFDIO_REGISTER to register ranges for fault tracking.

func NewUffdioRegister

func NewUffdioRegister(start, length, mode uint64) UffdioRegister

NewUffdioRegister builds a placeholder UFFDIO_REGISTER request struct on non-Linux builds.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL