Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ab71d89
test(uffd): add cross-process scaffolding for gated and async ops
ValentaTomas Apr 21, 2026
b14a18c
test(uffd): restore early uffd close cleanup, keep unregister late
ValentaTomas Apr 21, 2026
620b111
test(uffd): make gated cleanup idempotent to avoid pause-then-exit hang
ValentaTomas Apr 21, 2026
bbdc8f9
test(uffd): drop REMOVE-specific bits, keep only gated/async scaffolding
ValentaTomas Apr 21, 2026
c39d391
refactor(uffd-tests): replace SIGUSR/env-var/pipe IPC with net/rpc/js…
ValentaTomas Apr 29, 2026
efb09f4
Merge branch 'main' into refactor/uffd-test-harness
ValentaTomas May 1, 2026
803ce4b
refactor(uffd): bundle test hooks into single atomic.Pointer[testHooks]
ValentaTomas May 1, 2026
89b4747
refactor(uffd-tests): collapse cross-process harness side-channels in…
ValentaTomas May 1, 2026
0b4c590
refactor(uffd-tests): pass rpc socket via socketpair fd, drop env path
ValentaTomas May 1, 2026
a55bc31
test(uffd): rename pageStatesOnce → pageStates
ValentaTomas May 1, 2026
aa1d10c
refactor(uffd-tests): extract rpc wire types/client/barrier registry …
ValentaTomas May 1, 2026
1d0e876
refactor(uffd): collapse test-only hooks into single phase-dispatched fn
ValentaTomas May 2, 2026
729917b
chore(uffd-tests): tighten harness diff — trim comments, drop dead co…
ValentaTomas May 2, 2026
687f5c3
refactor(uffd): revert NewUserfaultfdFromFd → NewFromFd rename
ValentaTomas May 2, 2026
ee0186b
fix(uffd-tests): propagate startServe errors and reap child on bootst…
ValentaTomas May 2, 2026
61ab59a
fix(uffd-tests): hold settleRequests.Lock for entire pageStateEntries…
ValentaTomas May 2, 2026
96df539
refactor(uffd-tests): move internal/rpcharness → testutils/testharness
ValentaTomas May 2, 2026
eb294e2
fix(uffd-tests): reap child on FileConn failure; require Barriers opt-in
ValentaTomas May 2, 2026
93e2727
fix(uffd-tests): cleanup at acquisition, shutdown-aware WaitHeld, def…
ValentaTomas May 2, 2026
4681ab2
test(uffd): restore TestParallelMissing/Write parallelOperations to 1…
ValentaTomas May 2, 2026
d264863
test(uffd): restore TestSerial{Missing,MissingWrite} and TestParallel…
ValentaTomas May 2, 2026
7683f88
fix(uffd-tests): release harnessState.mu before blocking on Serve drain
ValentaTomas May 2, 2026
fa7d2ed
chore(uffd-tests): drop non-load-bearing comments and unreferenced he…
ValentaTomas May 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package testharness

import (
"context"
"fmt"
"sync"
)

// Point identifies which worker hook to park on. Values must match the
// parent package's faultPhase iota so the hook can cast across.
type Point uint8

const (
// BeforeRLock parks before settleRequests.RLock.
BeforeRLock Point = iota
// BeforeFaultPage parks after settleRequests.RLock, before UFFDIO_COPY.
BeforeFaultPage
)

// Registry is the child-side barrier store consulted by the per-fault hook.
type Registry struct {
mu sync.Mutex
next uint64
tokens map[uint64]*slot
byKey map[key]uint64
}

type key struct {
addr uintptr
point Point
}

type slot struct {
addr uintptr
point Point
arrived chan struct{}
release chan struct{}
arrivedOnce sync.Once
}

func NewRegistry() *Registry {
return &Registry{
tokens: make(map[uint64]*slot),
byKey: make(map[key]uint64),
}
}

func (r *Registry) Install(addr uintptr, point Point) uint64 {
r.mu.Lock()
defer r.mu.Unlock()

r.next++
token := r.next
s := &slot{
addr: addr,
point: point,
arrived: make(chan struct{}),
release: make(chan struct{}),
}
r.tokens[token] = s
r.byKey[key{addr, point}] = token

return token
}

func (r *Registry) lookupByAddr(addr uintptr, point Point) *slot {
r.mu.Lock()
defer r.mu.Unlock()

token, ok := r.byKey[key{addr, point}]
if !ok {
return nil
}

return r.tokens[token]
}

func (r *Registry) WaitArrived(ctx context.Context, token uint64) error {
r.mu.Lock()
s, ok := r.tokens[token]
r.mu.Unlock()
if !ok {
return fmt.Errorf("unknown barrier token %d", token)
}

select {
case <-s.arrived:
return nil
case <-ctx.Done():
return ctx.Err()
}
}

// Release frees the barrier; unknown token is a no-op.
func (r *Registry) Release(token uint64) {
r.mu.Lock()
s, ok := r.tokens[token]
delete(r.tokens, token)
if ok {
// A later Install at this key overwrites byKey; only delete if
// it still maps to this token.
k := key{s.addr, s.point}
if r.byKey[k] == token {
delete(r.byKey, k)
}
}
r.mu.Unlock()

if !ok {
return
}

select {
case <-s.release:
default:
close(s.release)
}
}

// ReleaseAll releases every still-installed barrier.
func (r *Registry) ReleaseAll() {
r.mu.Lock()
tokens := make([]uint64, 0, len(r.tokens))
for t := range r.tokens {
tokens = append(tokens, t)
}
r.mu.Unlock()

for _, t := range tokens {
r.Release(t)
}
}

// Hook returns the per-fault hook to install on *Userfaultfd; faults
// without an installed slot are no-ops.
func (r *Registry) Hook() func(addr uintptr, point Point) {
return func(addr uintptr, point Point) {
s := r.lookupByAddr(addr, point)
if s == nil {
return
}

s.arrivedOnce.Do(func() {
close(s.arrived)
})

<-s.release
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package testharness

import (
"io"
"net/rpc"
"net/rpc/jsonrpc"
)

// Client is the typed parent-side wrapper around the JSON-RPC channel
// to the child helper process.
type Client struct {
rpc *rpc.Client
conn io.Closer
}

// NewClient wraps an already-connected duplex stream. Closing the
// returned Client closes the underlying conn.
func NewClient(conn io.ReadWriteCloser) *Client {
return &Client{
rpc: jsonrpc.NewClient(conn),
conn: conn,
}
}

func (c *Client) Bootstrap(args BootstrapArgs) error {
return c.rpc.Call("Lifecycle.Bootstrap", &args, &BootstrapReply{})
}

func (c *Client) WaitReady() error {
return c.rpc.Call("Lifecycle.WaitReady", &Empty{}, &Empty{})
}

func (c *Client) Shutdown() error {
return c.rpc.Call("Lifecycle.Shutdown", &Empty{}, &Empty{})
}

func (c *Client) Pause() error {
return c.rpc.Call("Paging.Pause", &Empty{}, &Empty{})
}

func (c *Client) Resume() error {
return c.rpc.Call("Paging.Resume", &Empty{}, &Empty{})
}

func (c *Client) PageStates() ([]PageStateEntry, error) {
var reply PageStatesReply
if err := c.rpc.Call("Paging.States", &Empty{}, &reply); err != nil {
return nil, err
}

return reply.Entries, nil
}

func (c *Client) InstallBarrier(addr uintptr, point Point) (uint64, error) {
var reply FaultBarrierReply
if err := c.rpc.Call("Barriers.Install", &FaultBarrierArgs{Addr: uint64(addr), Point: uint8(point)}, &reply); err != nil {
return 0, err
}

return reply.Token, nil
}

func (c *Client) WaitFaultHeld(token uint64) error {
return c.rpc.Call("Barriers.WaitHeld", &TokenArgs{Token: token}, &Empty{})
}

func (c *Client) ReleaseFault(token uint64) error {
return c.rpc.Call("Barriers.Release", &TokenArgs{Token: token}, &Empty{})
}

func (c *Client) Close() error {
return c.conn.Close()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Package testharness provides the wire types, typed RPC client, and
// barrier registry shared between the parent and child halves of the
// userfaultfd test harness.
package testharness

// Empty is the placeholder for net/rpc methods that take or return
// nothing; net/rpc still requires both args and reply pointers.
type Empty struct{}

type BootstrapArgs struct {
MmapStart uint64
Pagesize int64
TotalSize int64
AlwaysWP bool
// Barriers gates the test-only worker hooks (off by default).
Barriers bool
Content []byte
}

type BootstrapReply struct{}

// PageStateEntry is the wire form of the parent package's pageState enum.
type PageStateEntry struct {
State uint8
Offset uint64
}

type PageStatesReply struct {
Entries []PageStateEntry
}

type FaultBarrierArgs struct {
Addr uint64
Point uint8
}

type FaultBarrierReply struct {
Token uint64
}

type TokenArgs struct {
Token uint64
}
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ func TestAsyncWriteProtection(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

h, err := configureCrossProcessTest(t, testConfig{
h, err := configureCrossProcessTest(t.Context(), t, testConfig{
pagesize: tt.pagesize,
numberOfPages: tt.numberOfPages,
alwaysWP: tt.alwaysWP,
Expand Down
Loading
Loading