-
Notifications
You must be signed in to change notification settings - Fork 699
/
memory.go
145 lines (122 loc) · 4.29 KB
/
memory.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
package ebpf
import (
"errors"
"fmt"
"io"
"runtime"
"github.com/cilium/ebpf/internal/unix"
)
// Memory is the building block for accessing the memory of specific bpf map
// types (Array and Arena at the time of writing) without going through the bpf
// syscall interface.
//
// Given the fd of a bpf map created with the BPF_F_MMAPABLE flag, a shared
// 'file'-based memory-mapped region can be allocated in the process' address
// space, exposing the bpf map's memory by simply accessing a memory location.
var ErrReadOnly = errors.New("resource is read-only")
// Memory implements accessing a Map's memory without making any syscalls.
// Pay attention to the difference between Go and C struct alignment rules. Use
// [structs.HostLayout] on supported Go versions to help with alignment.
//
// Note on memory coherence: avoid using packed structs in memory shared between
// user space and eBPF C programs. This drops a struct's memory alignment to 1,
// forcing the compiler to use single-byte loads and stores for field accesses.
// This may lead to partially-written data to be observed from user space.
//
// On most architectures, the memmove implementation used by Go's copy() will
// access data in word-sized chunks. If paired with a matching access pattern on
// the eBPF C side (and if using default memory alignment), accessing shared
// memory without atomics or other synchronization primitives should be sound
// for individual values. For accesses beyond a single value, the usual
// concurrent programming rules apply.
type Memory struct {
b []byte
ro bool
}
func newMemory(fd, size int) (*Memory, error) {
// Typically, maps created with BPF_F_RDONLY_PROG remain writable from user
// space until frozen. As a security precaution, the kernel doesn't allow
// mapping bpf map memory as read-write into user space if the bpf map was
// frozen, or if it was created using the RDONLY_PROG flag.
//
// The user would be able to write to the map after freezing (since the kernel
// can't change the protection mode of an already-mapped page), while the
// verifier assumes the contents to be immutable.
b, err := unix.Mmap(fd, 0, size, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
// If the map is frozen when an rw mapping is requested, expect EPERM. If the
// map was created with BPF_F_RDONLY_PROG, expect EACCES.
var ro bool
if errors.Is(err, unix.EPERM) || errors.Is(err, unix.EACCES) {
ro = true
b, err = unix.Mmap(fd, 0, size, unix.PROT_READ, unix.MAP_SHARED)
}
if err != nil {
return nil, fmt.Errorf("setting up memory-mapped region: %w", err)
}
mm := &Memory{
b,
ro,
}
runtime.SetFinalizer(mm, (*Memory).close)
return mm, nil
}
func (mm *Memory) close() {
if err := unix.Munmap(mm.b); err != nil {
panic(fmt.Errorf("unmapping memory: %w", err))
}
mm.b = nil
}
// Size returns the size of the memory-mapped region in bytes.
func (mm *Memory) Size() int {
return len(mm.b)
}
// ReadOnly returns true if the memory-mapped region is read-only.
func (mm *Memory) ReadOnly() bool {
return mm.ro
}
// bounds returns true if an access at off of the given size is within bounds.
func (mm *Memory) bounds(off uint64, size uint64) bool {
return off+size < uint64(len(mm.b))
}
// ReadAt implements [io.ReaderAt]. Useful for creating a new [io.OffsetWriter].
//
// See [Memory] for details around memory coherence.
func (mm *Memory) ReadAt(p []byte, off int64) (int, error) {
if mm.b == nil {
return 0, fmt.Errorf("memory-mapped region closed")
}
if p == nil {
return 0, fmt.Errorf("input buffer p is nil")
}
if off < 0 || off >= int64(len(mm.b)) {
return 0, fmt.Errorf("read offset out of range")
}
n := copy(p, mm.b[off:])
if n < len(p) {
return n, io.EOF
}
return n, nil
}
// WriteAt implements [io.WriterAt]. Useful for creating a new
// [io.SectionReader].
//
// See [Memory] for details around memory coherence.
func (mm *Memory) WriteAt(p []byte, off int64) (int, error) {
if mm.b == nil {
return 0, fmt.Errorf("memory-mapped region closed")
}
if mm.ro {
return 0, fmt.Errorf("memory-mapped region not writable: %w", ErrReadOnly)
}
if p == nil {
return 0, fmt.Errorf("output buffer p is nil")
}
if off < 0 || off >= int64(len(mm.b)) {
return 0, fmt.Errorf("write offset out of range")
}
n := copy(mm.b[off:], p)
if n < len(p) {
return n, io.EOF
}
return n, nil
}