-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcapabilities.go
More file actions
306 lines (280 loc) · 12.2 KB
/
Copy pathcapabilities.go
File metadata and controls
306 lines (280 loc) · 12.2 KB
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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
// capabilities.go implements honest capability reporting: the Capabilities
// struct, the Probe() startup preflight, and the per-allocation plumbing that
// feeds SecureBuffer.Capabilities() and SecureArena.Capabilities().
//
// Probe() reports what THIS platform can do; the per-object methods report
// what a specific allocation ACTUALLY got. The two can differ: memfd_secret
// can fail per-call (ENOSYS, EPERM under kernel lockdown, RLIMIT) and fall
// through to mmap+mlock, so the per-buffer value is the honest one.
package secmem
import (
"runtime"
"strings"
)
// Capabilities describes which memory protections are in force, either for
// the platform as a whole ([Probe]) or for one specific allocation
// ([SecureBuffer.Capabilities], [SecureArena.Capabilities]).
//
// Every field is stated per the platform guarantee matrix in the package
// documentation. A false field is not an error — it is the truth about what
// this build, kernel, and allocation provide. Use [Capabilities.Warnings] to
// enumerate the protections NOT in force.
type Capabilities struct {
// GOOS and GOARCH identify the build this report describes.
GOOS, GOARCH string
// OffHeap reports that the memory lives outside the Go heap (mmap or
// VirtualAlloc, not make([]byte)) — the GC never scans it or recycles it
// unzeroed, and its pages can be locked, guarded and protected.
OffHeap bool
// Mlocked reports that the pages are locked against paging: mlock, or
// memfd_secret's kernel-enforced equivalent, keeps them off the swap
// device. On Windows the mechanism is VirtualLock, which is weaker than
// the name suggests: it pins the pages into the process WORKING SET, not
// into physical memory. They stay resident and exempt from working-set
// trimming while the process runs, but when the memory manager outswaps
// an idle process's working set as a whole, locked pages go to the
// pagefile with it, and no user-mode setting prevents that. Warnings
// reports this on Windows.
Mlocked bool
// MemfdSecret reports kernel isolation: the pages are invisible to
// /proc/<pid>/mem, ptrace, and other readers of process memory.
// Linux amd64/arm64 with kernel 5.14+ and CONFIG_SECRETMEM only, and can
// fail per-allocation.
MemfdSecret bool
// NoDump reports the allocation is excluded from crash dumps: on Linux,
// MADV_DONTDUMP took effect or the memory is memfd_secret-backed; on
// Windows, WerRegisterExcludedMemoryBlock succeeded — which covers
// WER-generated dumps only (a debugger-driven dump still captures the
// pages; Seal's cipher covers the dormant window there). Process-wide
// exclusion ([HardenProcess], [DisableCoreDumps]) is NOT reflected here.
NoDump bool
// NoFork reports MADV_DONTFORK took effect: forked children do not
// inherit the mapping.
NoFork bool
// FlushedWipe reports the destroy-time wipe is architecture assembly
// with a cache-line flush (amd64 CLFLUSH/CLFLUSHOPT, arm64 DC CIVAC).
// When false the wipe is a constant-time store loop only — the zeros are
// written, but lines may linger in cache.
FlushedWipe bool
// RegisterScrub reports runtime/secret erasure is active
// (GOEXPERIMENT=runtimesecret on a supported platform): [Scrub] erases
// the registers, stack, and heap of its callback's entire call tree.
// When false, Scrub is a best-effort stack-frame wipe.
RegisterScrub bool
// FrameScrub reports that [Scrub]'s stack-frame burn is real assembly on
// this architecture (amd64, arm64) rather than the no-op stub. When false,
// Scrub reserves no headroom and erases no stack residue; only the
// runtime/secret path (see RegisterScrub) covers the stack at all.
//
// When RegisterScrub is also true the frame burn is not what runs —
// runtime/secret supersedes it and covers the stack more thoroughly — so
// read this as architecture support for the fallback path, not as a claim
// about which routine executed.
FrameScrub bool
// AsyncPreemptSuppressed reports that [Scrub] blocks Go's preemption signal
// for the duration of its window, so runtime.asyncPreempt cannot spill the
// full register file onto the stack at an arbitrary instruction. Linux only:
// Windows preemption rewrites the thread context instead of signalling, and
// x/sys/unix exposes no PthreadSigmask on Darwin.
//
// It does NOT claim immunity from every stack copy — cooperative preemption
// at a call boundary is unaffected. See [Scrub].
AsyncPreemptSuppressed bool
// VectorRegisterClear reports that [Scrub] zeroes the vector register file
// (X0–X15 at full YMM/ZMM width plus Z16–Z31 under AVX-512 on amd64; V0–V31
// on arm64) on the thread that ran its callback, as the first thing after
// the callback returns. Vectorised crypto keeps its working state there and
// nothing else in the process ever clears it. Real assembly on amd64 and
// arm64, proven by a register-dump test; a no-op elsewhere. When false and
// RegisterScrub is also false, residue in the vector file outlives the
// window.
//
// General-purpose registers are reported separately, by GPRegisterClear.
VectorRegisterClear bool
// GPRegisterClear reports that [Scrub] zeroes every general-purpose
// register a callee may clobber under the Go ABI (all but the stack
// pointer, frame pointer, g, and on arm64 the platform and link registers)
// on the thread that ran its callback, right after the vector clear.
// Scalar code and short copies leave secrets there, and the next
// asynchronous preemption on the thread saves them onto a goroutine stack
// outside the window. Real assembly on amd64 and arm64, proven by a
// register-dump test; a no-op elsewhere. When false and RegisterScrub is
// also false, that residue outlives the window.
GPRegisterClear bool
// GuardPages reports PROT_NONE guard pages bracket the mapping so a
// linear over/under-flow traps (SIGSEGV / access violation) instead of
// silently touching adjacent memory. A memory-safety bug-catcher, not a
// secrecy mechanism. False only on the insecure heap fallback.
GuardPages bool
// Insecure reports the memory is plain heap — NO protection is in force.
// True only on platforms with no lockable off-heap memory.
Insecure bool
}
// allocInfo records which protections one specific allocation actually
// received. Platform allocSecretMem/allocMapAnon implementations fill it at
// allocation time; constructors store it for Capabilities().
type allocInfo struct {
offHeap bool
mlocked bool
memfdSecret bool
noDump bool
noFork bool
guardPages bool
insecure bool
}
// capsFromAlloc composes a Capabilities report from a specific allocation's
// facts plus the process-wide facts (build identity, wipe class, scrub layer).
func capsFromAlloc(info allocInfo) Capabilities {
return Capabilities{
GOOS: runtime.GOOS,
GOARCH: runtime.GOARCH,
OffHeap: info.offHeap,
Mlocked: info.mlocked,
MemfdSecret: info.memfdSecret,
NoDump: info.noDump,
NoFork: info.noFork,
FlushedWipe: archWipeFlushed,
RegisterScrub: RuntimeSecretActive(),
FrameScrub: archFrameScrub,
AsyncPreemptSuppressed: asyncPreemptSuppressionSupported,
VectorRegisterClear: archVectorClear,
GPRegisterClear: archGPClear,
GuardPages: info.guardPages,
Insecure: info.insecure,
}
}
// Probe reports what this platform can do, by performing one real one-byte
// allocation through the same path the constructors use and reporting what it
// received. Call it once at startup to log or gate on the protections in
// force; per-allocation truth is [SecureBuffer.Capabilities].
//
// Probe is not cached: it reflects the kernel and limits at the moment of the
// call. If even the probe allocation fails, the report is fully degraded
// (every protection false) — treat that as this platform providing nothing.
func Probe() Capabilities {
region, _, info, err := allocSecretMem(1)
if err != nil {
return capsFromAlloc(allocInfo{})
}
secureWipeSlice(region.inner)
_ = freeSecretMem(region)
return capsFromAlloc(info)
}
// Warnings returns a human-readable line for every protection NOT in force,
// most severe first. An empty slice means every protection this library can
// provide is active. Intended for startup logging:
//
// for _, w := range secmem.Probe().Warnings() {
// slog.Warn("secmem", "degradation", w)
// }
func (c Capabilities) Warnings() []string {
var w []string
if c.Insecure {
w = append(w, "INSECURE: plain heap fallback — no memory protection is in force at all")
}
if !c.OffHeap && !c.Insecure {
w = append(w, "memory is on the Go heap — the GC may copy secrets during collection")
}
if !c.Mlocked {
w = append(w, "pages are not locked — secrets may be written to the swap device")
} else if c.GOOS == "windows" {
w = append(w, "VirtualLock pins pages into the working set only — when an idle process is outswapped, locked pages go to the pagefile with it")
}
if !c.MemfdSecret {
w = append(w, "no kernel isolation (memfd_secret) — a sufficiently privileged process or debugger can read the memory")
}
if !c.NoDump {
w = append(w, "not excluded from core dumps")
}
if !c.NoFork {
w = append(w, "mapping is inherited by forked children (no MADV_DONTFORK)")
}
if !c.FlushedWipe {
w = append(w, "wipe is a constant-time store only — no cache-line flush on this architecture")
}
if !c.RegisterScrub {
w = append(w, "runtime/secret erasure inactive — Scrub is a best-effort stack-frame wipe")
}
if !c.FrameScrub && !c.RegisterScrub {
w = append(w, "no stack scrub on this architecture — Scrub does not erase stack residue at all")
}
if !c.AsyncPreemptSuppressed {
w = append(w, "async preemption not suppressed — the runtime may spill the register file to the stack inside a Scrub window")
}
if !c.VectorRegisterClear && !c.RegisterScrub {
w = append(w, "vector registers not cleared after a Scrub window — residue from vectorised crypto survives on this architecture")
}
if !c.GPRegisterClear && !c.RegisterScrub {
w = append(w, "general-purpose registers not cleared after a Scrub window — residue from scalar code and short copies survives on this architecture")
}
if !c.GuardPages {
w = append(w, "no guard pages — buffer overflows are not trapped")
}
return w
}
// String returns a compact one-line summary: the build identity, the
// protections in force, and the protections missing. It never prints secret
// material — Capabilities holds none.
func (c Capabilities) String() string {
var have, miss []string
flag := func(name string, on bool) {
if on {
have = append(have, name)
} else {
miss = append(miss, name)
}
}
flag("off-heap", c.OffHeap)
flag("mlock", c.Mlocked)
flag("memfd_secret", c.MemfdSecret)
flag("no-dump", c.NoDump)
flag("no-fork", c.NoFork)
flag("wipe+flush", c.FlushedWipe)
flag("register-scrub", c.RegisterScrub)
flag("frame-scrub", c.FrameScrub)
flag("preempt-suppress", c.AsyncPreemptSuppressed)
flag("vector-clear", c.VectorRegisterClear)
flag("gp-clear", c.GPRegisterClear)
flag("guard-pages", c.GuardPages)
var b strings.Builder
b.WriteString(c.GOOS)
b.WriteByte('/')
b.WriteString(c.GOARCH)
if c.Insecure {
b.WriteString(" INSECURE(heap)")
}
if len(have) > 0 {
b.WriteString(" [")
b.WriteString(strings.Join(have, " "))
b.WriteByte(']')
}
if len(miss) > 0 {
b.WriteString(" missing:[")
b.WriteString(strings.Join(miss, " "))
b.WriteByte(']')
}
return b.String()
}
// Capabilities reports how THIS buffer's allocation is actually backed —
// the honest per-allocation counterpart to [Probe] (memfd_secret can fail
// per-call and fall through to mmap+mlock; this reflects what happened).
//
// The report is fixed at construction and remains valid after Destroy.
// A nil receiver reports a fully degraded posture.
func (s *SecureBuffer) Capabilities() Capabilities {
if s == nil {
return capsFromAlloc(allocInfo{})
}
return capsFromAlloc(s.backing)
}
// Capabilities reports how this arena's slab is actually backed. Every slot
// shares the slab's single allocation, so one report covers them all.
//
// The report is fixed at construction and remains valid after Destroy.
// A nil receiver reports a fully degraded posture.
func (a *SecureArena) Capabilities() Capabilities {
if a == nil {
return capsFromAlloc(allocInfo{})
}
return capsFromAlloc(a.backing)
}