Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
138 commits
Select commit Hold shift + click to select a range
a929999
Wrap uffd fd in a struct with corresponding methods; Move the low lev…
ValentaTomas Aug 7, 2025
58a4d41
Unify receiver name
ValentaTomas Aug 7, 2025
e3d6716
[WIP] Add uffd tests
ValentaTomas Aug 7, 2025
56e43c8
Add disabled wp
ValentaTomas Aug 7, 2025
acc2d66
[WIP] WP
ValentaTomas Aug 8, 2025
1c4b8ea
[WIP] Check multicopy
ValentaTomas Aug 10, 2025
9d7f5e5
[WIP] Multicopy
ValentaTomas Aug 12, 2025
66a1bbe
Remove experiments
ValentaTomas Aug 19, 2025
e774a92
Add the page alignment check (only in method)
ValentaTomas Aug 19, 2025
f240542
Remove commented out part
ValentaTomas Aug 19, 2025
f31cb47
Remove unused return field
ValentaTomas Aug 19, 2025
bfc5935
Cleanup
ValentaTomas Aug 19, 2025
f732d5a
Remove WP parts
ValentaTomas Aug 19, 2025
855369b
Cleanup
ValentaTomas Aug 19, 2025
f981d76
Fix compile errors
ValentaTomas Oct 3, 2025
ccd8396
Cleanup
ValentaTomas Oct 4, 2025
fbf258a
Simplify offset check
ValentaTomas Oct 4, 2025
67b1bfb
Clarify comment
ValentaTomas Oct 4, 2025
28c48a7
Add test constant
ValentaTomas Oct 4, 2025
1ecd0a1
Fix incorrect log
ValentaTomas Oct 4, 2025
4fe40a0
Fix format
ValentaTomas Oct 4, 2025
bd8dc0b
Remove conversion
ValentaTomas Oct 4, 2025
f0d17ef
Make method public
ValentaTomas Oct 4, 2025
c4691b5
Add command context
ValentaTomas Oct 4, 2025
9581305
Fix error formatting
ValentaTomas Oct 4, 2025
fa72b74
Fix error formatting
ValentaTomas Oct 4, 2025
8230e4f
Improve error comparison
ValentaTomas Oct 4, 2025
1454794
Fix error formatting
ValentaTomas Oct 4, 2025
ac484d2
Change process exit
ValentaTomas Oct 4, 2025
f2d01de
Enable unpriviledged uffd mode in GH PR tests
ValentaTomas Oct 4, 2025
d27b2d4
Trigger build
ValentaTomas Oct 4, 2025
5e83359
Merge branch 'uffd-extract' of https://github.com/e2b-dev/infra into …
ValentaTomas Oct 4, 2025
05ae2e9
Fix uffd unpriviledged enable
ValentaTomas Oct 4, 2025
512fd04
Use 4k pages in uffd cross process test
ValentaTomas Oct 4, 2025
1ecc810
Add access checks to tests
ValentaTomas Oct 5, 2025
fd77d22
Cleanup
ValentaTomas Oct 5, 2025
b1a6d53
Remove unused constants for now
ValentaTomas Oct 5, 2025
2a329c8
Fix lint issue
ValentaTomas Oct 5, 2025
44984ea
Clarify naming
ValentaTomas Oct 5, 2025
c5e55e3
Clarify comment
ValentaTomas Oct 5, 2025
b04e552
Remove yet non-relevant diagram
ValentaTomas Oct 5, 2025
9607180
Remove comment
ValentaTomas Oct 5, 2025
3cf3bec
Remove write protection field
ValentaTomas Oct 5, 2025
ac9bfdf
Add explicit mmap cleanup
ValentaTomas Oct 6, 2025
3ab7848
Merge branch 'main' into uffd-extract
ValentaTomas Oct 6, 2025
69954c5
Improve test names
ValentaTomas Oct 6, 2025
4ce39ab
Fix test error message
ValentaTomas Oct 6, 2025
e8b9330
Cleanup tests
ValentaTomas Oct 6, 2025
e366753
Merge branch 'main' into uffd-extract
ValentaTomas Oct 6, 2025
5ef4373
Fix lint error
ValentaTomas Oct 6, 2025
bb951ba
Add uffd write protect constants
ValentaTomas Oct 5, 2025
8906575
Add UFFD write protection event diagram
ValentaTomas Oct 5, 2025
d89b81f
Add userfaultfd write protection methods
ValentaTomas Oct 5, 2025
f2a012a
Add tests todo
ValentaTomas Oct 5, 2025
e522612
[WIP] Add dirty tracking via WP
ValentaTomas Oct 6, 2025
64e2b13
Clarify WP flow
ValentaTomas Oct 7, 2025
bd8988e
[WIP] Add WP to the serve loop
ValentaTomas Oct 7, 2025
0dadb58
Wrap uffd fd in a struct with corresponding methods; Move the low lev…
ValentaTomas Aug 7, 2025
34ccfc5
Unify receiver name
ValentaTomas Aug 7, 2025
7312bca
[WIP] Add uffd tests
ValentaTomas Aug 7, 2025
3c75576
Add disabled wp
ValentaTomas Aug 7, 2025
b67f0f2
[WIP] WP
ValentaTomas Aug 8, 2025
9f1708c
[WIP] Check multicopy
ValentaTomas Aug 10, 2025
b07e13d
[WIP] Multicopy
ValentaTomas Aug 12, 2025
ff23f18
Remove experiments
ValentaTomas Aug 19, 2025
389862e
Add the page alignment check (only in method)
ValentaTomas Aug 19, 2025
a8ec1bf
Remove commented out part
ValentaTomas Aug 19, 2025
5632f63
Remove unused return field
ValentaTomas Aug 19, 2025
ef33099
Cleanup
ValentaTomas Aug 19, 2025
5384bec
Remove WP parts
ValentaTomas Aug 19, 2025
f26cc5c
Cleanup
ValentaTomas Aug 19, 2025
47e14b0
Fix compile errors
ValentaTomas Oct 3, 2025
7ba06e7
Cleanup
ValentaTomas Oct 4, 2025
e91945f
Simplify offset check
ValentaTomas Oct 4, 2025
d4c2ea0
Clarify comment
ValentaTomas Oct 4, 2025
091607e
Add test constant
ValentaTomas Oct 4, 2025
7ae5f70
Fix incorrect log
ValentaTomas Oct 4, 2025
f9f95c5
Fix format
ValentaTomas Oct 4, 2025
708d54b
Remove conversion
ValentaTomas Oct 4, 2025
2aa9449
Make method public
ValentaTomas Oct 4, 2025
dc7dc60
Add command context
ValentaTomas Oct 4, 2025
6331bd2
Fix error formatting
ValentaTomas Oct 4, 2025
97ee756
Fix error formatting
ValentaTomas Oct 4, 2025
03b0c46
Improve error comparison
ValentaTomas Oct 4, 2025
a331214
Fix error formatting
ValentaTomas Oct 4, 2025
6855c56
Change process exit
ValentaTomas Oct 4, 2025
359c17d
Trigger build
ValentaTomas Oct 4, 2025
835a32d
Enable unpriviledged uffd mode in GH PR tests
ValentaTomas Oct 4, 2025
76c3bf3
Fix uffd unpriviledged enable
ValentaTomas Oct 4, 2025
0ba2a14
Use 4k pages in uffd cross process test
ValentaTomas Oct 4, 2025
52d2fc7
Add access checks to tests
ValentaTomas Oct 5, 2025
f092687
Cleanup
ValentaTomas Oct 5, 2025
3f7216a
Remove unused constants for now
ValentaTomas Oct 5, 2025
7fb669e
Fix lint issue
ValentaTomas Oct 5, 2025
052b09f
Clarify naming
ValentaTomas Oct 5, 2025
1066947
Clarify comment
ValentaTomas Oct 5, 2025
d06985c
Remove yet non-relevant diagram
ValentaTomas Oct 5, 2025
179ba74
Remove comment
ValentaTomas Oct 5, 2025
bacedce
Remove write protection field
ValentaTomas Oct 5, 2025
5d4c576
Add explicit mmap cleanup
ValentaTomas Oct 6, 2025
fe1f562
Improve test names
ValentaTomas Oct 6, 2025
70015bd
Fix test error message
ValentaTomas Oct 6, 2025
6920876
Cleanup tests
ValentaTomas Oct 6, 2025
cb5cfdb
Fix lint error
ValentaTomas Oct 6, 2025
ac77fe5
Put back offset log on uffd panic
ValentaTomas Oct 7, 2025
c11e5ef
Fix lint errors
ValentaTomas Oct 8, 2025
dcea5f1
Merge branch 'main' into uffd-extract
djeebus Oct 10, 2025
43f7daa
Merge branch 'main' into uffd-extract
ValentaTomas Oct 11, 2025
6e6e073
Fix diagram flags
ValentaTomas Oct 13, 2025
1ec8b01
Add uffd write handling
ValentaTomas Oct 14, 2025
f0247a0
Fix test errors
ValentaTomas Oct 14, 2025
131b801
Merge branch 'uffd-extract' into use-uffd-wp-to-only-save-dirty-pages…
ValentaTomas Oct 14, 2025
c283d0c
Make offsetmap thread safe
ValentaTomas Oct 14, 2025
7e78473
Fix merge problems; [WIP] Add triggerable uffd copy
ValentaTomas Oct 14, 2025
bc50b15
Fix refactor bug
ValentaTomas Oct 16, 2025
96f546c
Fix compile error
ValentaTomas Oct 16, 2025
edb1c4c
[WIP] Refactor memory access
ValentaTomas Oct 17, 2025
ec4421f
Fix test after refactor
ValentaTomas Oct 17, 2025
f916358
Cleanup mapping
ValentaTomas Oct 17, 2025
6838b66
Improve naming
ValentaTomas Oct 17, 2025
0d4dc64
[WIP] Cleanup
ValentaTomas Oct 18, 2025
65cf118
Fix test bugs
ValentaTomas Oct 18, 2025
0505ab3
Add testing case
ValentaTomas Oct 18, 2025
a388c74
Cleanup
ValentaTomas Oct 18, 2025
4f89a59
[WIP] Add overlay layer
ValentaTomas Oct 20, 2025
2051bd6
Configure mise
ValentaTomas Oct 20, 2025
8fc2164
Exclude mise tools from git
ValentaTomas Oct 20, 2025
8995e6d
Extend tracker
ValentaTomas Oct 20, 2025
7ac927b
[WIP] Add masked overlay
ValentaTomas Oct 20, 2025
64be117
[WIP] Update masked overlay
ValentaTomas Oct 20, 2025
20c0766
Remove files that will be added later
ValentaTomas Oct 20, 2025
ae57cb4
Cleanup
ValentaTomas Oct 20, 2025
9783690
Remove files that will be added later
ValentaTomas Oct 20, 2025
aea1db4
Remove unnecessary change
ValentaTomas Oct 20, 2025
d9db61a
Cleanup
ValentaTomas Oct 20, 2025
12911de
Remove files that will be added later
ValentaTomas Oct 20, 2025
3b9249c
Add mapping tests
ValentaTomas Oct 20, 2025
06a82ba
Add invalid event log
ValentaTomas Oct 20, 2025
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
10 changes: 10 additions & 0 deletions .github/workflows/pr-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ jobs:
with:
job_key: unit-tests-${{ matrix.package }}

- name: Enable unprivileged uffd mode
run: |
echo 1 | sudo tee /proc/sys/vm/unprivileged_userfaultfd

- name: Enable hugepages
run: |
sudo mkdir -p /mnt/hugepages
sudo mount -t hugetlbfs none /mnt/hugepages
echo 128 | sudo tee /proc/sys/vm/nr_hugepages

- name: Run tests
working-directory: ${{ matrix.package }}
run: go test -v ${{ matrix.test_path }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ tests/periodic-test/build-template/e2b.toml
.air
go.work.sum
.infisical.json
.vscode/mise-tools/
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,11 @@
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"go.goroot": "${workspaceFolder}/.vscode/mise-tools/goRoot",
"go.alternateTools": {
"go": "${workspaceFolder}/.vscode/mise-tools/go",
"dlv": "${workspaceFolder}/.vscode/mise-tools/dlv",
"gopls": "${workspaceFolder}/.vscode/mise-tools/gopls"
}
}
82 changes: 43 additions & 39 deletions packages/orchestrator/internal/sandbox/block/tracker.go
Original file line number Diff line number Diff line change
@@ -1,66 +1,70 @@
package block

import (
"context"
"fmt"
"sync"
"sync/atomic"

"github.com/bits-and-blooms/bitset"

"github.com/e2b-dev/infra/packages/shared/pkg/storage/header"
)

type TrackedSliceDevice struct {
data ReadonlyDevice
blockSize int64
type Tracker struct {
b *bitset.BitSet
mu sync.RWMutex

nilTracking atomic.Bool
dirty *bitset.BitSet
dirtyMu sync.Mutex
empty []byte
blockSize int64
}

func NewTrackedSliceDevice(blockSize int64, device ReadonlyDevice) (*TrackedSliceDevice, error) {
return &TrackedSliceDevice{
data: device,
empty: make([]byte, blockSize),
func NewTracker(blockSize int64) *Tracker {
return &Tracker{
// The bitset resizes automatically based on the maximum set bit.
b: bitset.New(0),
blockSize: blockSize,
}, nil
}
}

func (t *TrackedSliceDevice) Disable() error {
size, err := t.data.Size()
if err != nil {
return fmt.Errorf("failed to get device size: %w", err)
}
func (t *Tracker) Has(off int64) bool {
t.mu.RLock()
defer t.mu.RUnlock()

t.dirty = bitset.New(uint(header.TotalBlocks(size, t.blockSize)))
// We are starting with all being dirty.
t.dirty.FlipRange(0, t.dirty.Len())
return t.b.Test(uint(header.BlockIdx(off, t.blockSize)))
}

func (t *Tracker) Add(off int64) bool {
t.mu.Lock()
defer t.mu.Unlock()

t.nilTracking.Store(true)
if t.b.Test(uint(header.BlockIdx(off, t.blockSize))) {
return false
}

return nil
t.b.Set(uint(header.BlockIdx(off, t.blockSize)))

return true
}

func (t *TrackedSliceDevice) Slice(ctx context.Context, off int64, length int64) ([]byte, error) {
if t.nilTracking.Load() {
t.dirtyMu.Lock()
t.dirty.Clear(uint(header.BlockIdx(off, t.blockSize)))
t.dirtyMu.Unlock()
func (t *Tracker) Reset() {
t.mu.Lock()
defer t.mu.Unlock()

return t.empty, nil
}
t.b.ClearAll()
}

return t.data.Slice(ctx, off, length)
// BitSet returns a clone of the bitset and the block size.
func (t *Tracker) BitSet() *bitset.BitSet {
t.mu.RLock()
defer t.mu.RUnlock()

return t.b.Clone()
}

// Return which bytes were not read since Disable.
// This effectively returns the bytes that have been requested after paused vm and are not dirty.
func (t *TrackedSliceDevice) Dirty() *bitset.BitSet {
t.dirtyMu.Lock()
defer t.dirtyMu.Unlock()
func (t *Tracker) BlockSize() int64 {
return t.blockSize
}

return t.dirty.Clone()
func (t *Tracker) Clone() *Tracker {
return &Tracker{
b: t.BitSet(),
blockSize: t.BlockSize(),
}
}
109 changes: 109 additions & 0 deletions packages/orchestrator/internal/sandbox/block/tracker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package block

import (
"testing"
)

func TestTracker_AddAndHas(t *testing.T) {
const pageSize = 4096
tr := NewTracker(pageSize)

offset := int64(pageSize * 4)

// Initially should not be marked
if tr.Has(offset) {
t.Errorf("Expected offset %d not to be marked initially", offset)
}

// After adding, should be marked
tr.Add(offset)
if !tr.Has(offset) {
t.Errorf("Expected offset %d to be marked after Add", offset)
}
}

func TestTracker_Reset(t *testing.T) {
const pageSize = 4096
tr := NewTracker(pageSize)

offset := int64(pageSize * 4)

// Add offset and verify it's marked
tr.Add(offset)
if !tr.Has(offset) {
t.Errorf("Expected offset %d to be marked after Add", offset)
}

// After reset, should not be marked
tr.Reset()
if tr.Has(offset) {
t.Errorf("Expected offset %d to be cleared after Reset", offset)
}
}

func TestTracker_MultipleOffsets(t *testing.T) {
const pageSize = 4096
tr := NewTracker(pageSize)

offsets := []int64{0, pageSize, 2 * pageSize, 10 * pageSize}

// Add multiple offsets
for _, o := range offsets {
tr.Add(o)
}

// Verify all offsets are marked
for _, o := range offsets {
if !tr.Has(o) {
t.Errorf("Expected offset %d to be marked", o)
}
}
}

func TestTracker_ResetClearsAll(t *testing.T) {
const pageSize = 4096
tr := NewTracker(pageSize)

offsets := []int64{0, pageSize, 2 * pageSize, 10 * pageSize}

// Add multiple offsets
for _, o := range offsets {
tr.Add(o)
}

// Reset should clear all
tr.Reset()

// Verify all offsets are cleared
for _, o := range offsets {
if tr.Has(o) {
t.Errorf("Expected offset %d to be cleared after Reset", o)
}
}
}

func TestTracker_MisalignedOffset(t *testing.T) {
const pageSize = 4096
tr := NewTracker(pageSize)

// Test with misaligned offset
misalignedOffset := int64(123)
tr.Add(misalignedOffset)

// Should be set for the block containing the offset—that is, block 0 (0..4095)
if !tr.Has(misalignedOffset) {
t.Errorf("Expected misaligned offset %d to be marked (should mark its containing block)", misalignedOffset)
}

// Now check that any offset in the same block is also considered marked
anotherOffsetInSameBlock := int64(1000)
if !tr.Has(anotherOffsetInSameBlock) {
t.Errorf("Expected offset %d to be marked as in same block as %d", anotherOffsetInSameBlock, misalignedOffset)
}

// But not for a different block
offsetInNextBlock := int64(pageSize) // convert to int64 to match Has signature
if tr.Has(offsetInNextBlock) {
t.Errorf("Did not expect offset %d to be marked", offsetInNextBlock)
}
}
14 changes: 11 additions & 3 deletions packages/orchestrator/internal/sandbox/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -671,10 +671,18 @@ func (s *Sandbox) Pause(
return nil, fmt.Errorf("failed to pause VM: %w", err)
}

if err := s.memory.Disable(); err != nil {
err = s.memory.Disable(ctx)
if err != nil {
return nil, fmt.Errorf("failed to disable uffd: %w", err)
}

dirty, err := s.memory.Dirty(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get dirty pages: %w", err)
}

dirtyPages := dirty.BitSet()

// Snapfile is not closed as it's returned and cached for later use (like resume)
snapfile := template.NewLocalFileLink(snapshotTemplateFiles.CacheSnapfilePath())
// Memfile is also closed on diff creation processing
Expand Down Expand Up @@ -718,7 +726,7 @@ func (s *Sandbox) Pause(
originalMemfile.Header(),
&MemoryDiffCreator{
memfile: memfile,
dirtyPages: s.memory.Dirty(),
dirtyPages: dirtyPages,
blockSize: originalMemfile.BlockSize(),
doneHook: func(context.Context) error {
return memfile.Close()
Expand Down Expand Up @@ -929,7 +937,7 @@ func serveMemory(
ctx, span := tracer.Start(ctx, "serve-memory")
defer span.End()

fcUffd, err := uffd.New(memfile, socketPath, memfile.BlockSize())
fcUffd, err := uffd.New(memfile, socketPath)
if err != nil {
return nil, fmt.Errorf("failed to create uffd: %w", err)
}
Expand Down

This file was deleted.

This file was deleted.

45 changes: 45 additions & 0 deletions packages/orchestrator/internal/sandbox/uffd/memory/mapping.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package memory

import (
"fmt"
)

type Mapping struct {
Regions []Region
}

func NewMapping(regions []Region) *Mapping {
return &Mapping{Regions: regions}
}

// GetOffset returns the relative offset and the page size of the mapped range for a given address.
func (m *Mapping) GetOffset(hostVirtAddr uintptr) (int64, uint64, error) {
for _, r := range m.Regions {
if hostVirtAddr >= r.BaseHostVirtAddr && hostVirtAddr < r.endHostVirtAddr() {
return r.shiftedOffset(hostVirtAddr), uint64(r.PageSize), nil
}
}

return 0, 0, fmt.Errorf("address %d not found in any mapping", hostVirtAddr)
}

// GetHostVirtAddr returns the host virtual address for a given offset.
func (m *Mapping) GetHostVirtAddr(off int64) (int64, uint64, error) {
r, err := m.getHostVirtRegion(off)
if err != nil {
return 0, 0, err
}

return int64(r.shiftedHostVirtAddr(off)), uint64(r.PageSize), nil
}

// getHostVirtRegion returns the region that contains the given offset.
func (m *Mapping) getHostVirtRegion(off int64) (*Region, error) {
for _, r := range m.Regions {
if off >= int64(r.Offset) && off < r.endOffset() {
return &r, nil
}
}

return nil, fmt.Errorf("offset %d not found in any mapping", off)
}
Loading
Loading