-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsecurebuf.go
More file actions
595 lines (552 loc) · 23 KB
/
Copy pathsecurebuf.go
File metadata and controls
595 lines (552 loc) · 23 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
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
// securebuf.go implements SecureBuffer, the hardened off-heap memory type.
//
// # Architecture layering
//
// Layer 2 — off-heap : mmap(MAP_ANON|MAP_PRIVATE) — all platforms
// Layer 3 — swap-proof: mlock / VirtualLock — all platforms
// Layer 4 — kernel-isolated (Linux 5.14+): memfd_secret (via allocSecretMem)
//
// # Critical invariants
//
// - region is IMMUTABLE after construction: it holds the exact secRegion
// returned by allocSecretMem/allocMapAnon (guarded outer reservation +
// inner secret area). Truncate MUST NOT modify region.
//
// - mu.rLock is held by ALL access methods for the duration of the operation.
// mu.lock is held ONLY by Destroy. This prevents TOCTOU races between
// Destroy (Munmap) and in-flight access callbacks.
//
// - The lock is a sync.Cond-based reader-writer lock (not sync.RWMutex) so
// that all blocking states are durably blocked under testing/synctest.
//
// - cleanup.Stop() is called before the wipe in Destroy to prevent double-free.
// runtime.KeepAlive(s) is called at the end of Destroy to keep the GC from
// running the cleanup between Stop() and the actual wipe.
package secmem
import (
"errors"
"fmt"
"log/slog"
"runtime"
"sync/atomic"
)
// SecureBuffer holds sensitive data in a page-aligned, mlock'd, off-heap memory
// region. The region is allocated via mmap(MAP_ANON|MAP_PRIVATE) (or higher-
// privilege equivalents on supported platforms) and is invisible to the Go GC.
//
// Callers MUST call [SecureBuffer.Destroy] explicitly. The AddCleanup fallback
// is a safety net only — it runs non-deterministically at GC time.
//
// WARNING: Never retain a reference from [SecureBuffer.WithBytes]
// beyond the buffer's lifetime. After Destroy,
// the backing memory is unmapped; any retained slice becomes a dangling pointer.
type SecureBuffer struct {
// data is the usable portion, region.inner[:size:size]. Access is
// controlled via methods — never exported directly to prevent heap
// copies. The capacity is clamped to the requested size so no re-slice
// can reach the canary slack behind it.
data []byte
// region is the guarded allocation: inner (wipe/lock/protect target,
// canary slack included) bracketed by PROT_NONE guard pages inside outer
// (the unmap target). See secRegion for the field contract.
// Invariant: ®ion.inner[0] == &data[0]; region is IMMUTABLE.
region secRegion
// mu: rLock=access methods, lock=Destroy. Prevents Munmap racing a callback.
// Uses a sync.Cond-based RWLock (not sync.RWMutex) so that all blocking
// states are durably blocked under testing/synctest.
mu *bufferRWLock
// cleanup is the handle returned by runtime.AddCleanup, used to Stop (cancel)
// the cleanup when Destroy is called explicitly — prevents double-free.
cleanup runtime.Cleanup
// janitorKey identifies this buffer's raw mapping in emergencyJanitor.
janitorKey uintptr
// sealed is true when the buffer's mmap region has been set to PROT_NONE.
// All access methods return ErrSealed while sealed is true.
// Protected by mu (same lock used for all state changes).
sealed bool
// readOnly is true when [SecureBuffer.ReadOnly] has set the region to
// PROT_READ. The mutating methods (CopyIn, SetByteAt, Truncate, ReadFrom)
// return ErrReadOnly while it is set — the API-boundary guard that turns a
// would-be PROT_READ write fault into a clean error. It also lets Seal lift
// and Unseal restore the protection so the physical page protection always
// matches the flag across a seal cycle. Protected by mu.
readOnly bool
// sealCipher is true while the contents are seal-cipher ciphertext
// (Windows: CryptProtectMemory under Seal). Atomic because it is read on
// the janitor's wipe paths on another goroutine. Shared with janitorRegion.
sealCipher *atomic.Bool
// wiped is set by WipeAllSecrets when this buffer's region was wiped in
// place and deliberately left mapped. Shared with janitorRegion, which is
// how the emergency path reaches a buffer it holds no pointer to. Reads
// still work and return the zeros; every mutator refuses with ErrWiped, so
// a process that keeps running after an emergency wipe cannot write a fresh
// secret into a region the wipe already reported as handled.
wiped *atomic.Bool
// backing records which protections this allocation actually received.
// Immutable after construction; read by Capabilities without the lock.
backing allocInfo
}
// ---------------------------------------------------------------------------
// Constructors
// ---------------------------------------------------------------------------
// NewBuffer allocates a hardened memory region and copies raw into it.
//
// The backing allocation is page-rounded (typically 4096 bytes), mlock'd,
// and invisible to the Go GC. raw is zeroed after the copy (defense-in-depth).
//
// WARNING: raw is zeroed after copying. The caller must not reuse raw after
// this call. If the same secret must be used multiple times, copy it first.
//
// raw is zeroed whether this call SUCCEEDS OR FAILS. Wiping only on success
// would leave the caller's plaintext sitting in an ordinary heap slice that the
// warning above has just told them is gone — the worst of both, since they will
// not wipe it themselves. The only exception is an empty raw, where there is
// nothing to wipe.
//
// That means a retry after [ErrNoSecureMemory] has nothing left to copy. It does
// not need one: that error depends only on the platform and on
// [WithInsecureFallback], both knowable before the call — see [Probe].
//
// Common errors: EPERM / ENOMEM from mlock (RLIMIT_MEMLOCK exceeded — check
// `ulimit -l` or systemd LimitMEMLOCK=). On platforms with no lockable
// off-heap memory the error is [ErrNoSecureMemory] unless
// [WithInsecureFallback] is passed.
func NewBuffer(raw []byte, opts ...Option) (*SecureBuffer, error) {
if len(raw) == 0 {
return nil, errors.New("secmem.NewBuffer: empty input")
}
// Deferred, not placed after the copy: every early return below is an error
// path that used to hand the caller back their plaintext intact. A defer
// also means a future error path cannot forget it.
defer secureWipeSlice(raw)
if err := gateInsecure(platformHasSecureMemory, applyOptions(opts)); err != nil {
return nil, fmt.Errorf("secmem.NewBuffer: %w", err)
}
region, data, info, err := allocSecretMem(len(raw))
if err != nil {
return nil, fmt.Errorf("secmem.NewBuffer: %w", err)
}
if err := fillCanary(region.inner[len(data):]); err != nil {
_ = freeSecretMem(region) // nothing secret written yet
return nil, fmt.Errorf("secmem.NewBuffer: %w", err)
}
copy(data, raw)
return newSecureBuffer(region, data, info), nil
}
// NewEmptyBuffer allocates an mlock'd zero-filled region of exactly size bytes.
// Equivalent to NewBuffer(make([]byte, size)) without the intermediate heap copy.
func NewEmptyBuffer(size int, opts ...Option) (*SecureBuffer, error) {
if size <= 0 {
return nil, fmt.Errorf("secmem.NewEmptyBuffer: invalid size %d", size)
}
if err := gateInsecure(platformHasSecureMemory, applyOptions(opts)); err != nil {
return nil, fmt.Errorf("secmem.NewEmptyBuffer: %w", err)
}
region, data, info, err := allocSecretMem(size)
if err != nil {
return nil, fmt.Errorf("secmem.NewEmptyBuffer: %w", err)
}
if err := fillCanary(region.inner[len(data):]); err != nil {
_ = freeSecretMem(region)
return nil, fmt.Errorf("secmem.NewEmptyBuffer: %w", err)
}
return newSecureBuffer(region, data, info), nil
}
// NewSyscallSafeBuffer allocates via MAP_ANON only (no memfd_secret attempt).
// Use this for ingestion paths where syscall arguments are read directly into
// the buffer — memfd_secret's extra isolation is not needed because the data
// arrives from a kernel-controlled channel.
func NewSyscallSafeBuffer(raw []byte, opts ...Option) (*SecureBuffer, error) {
if len(raw) == 0 {
return nil, errors.New("secmem.NewSyscallSafeBuffer: empty input")
}
defer secureWipeSlice(raw) // on failure too — see NewBuffer
if err := gateInsecure(platformHasSecureMemory, applyOptions(opts)); err != nil {
return nil, fmt.Errorf("secmem.NewSyscallSafeBuffer: %w", err)
}
region, data, info, err := allocMapAnon(len(raw))
if err != nil {
return nil, fmt.Errorf("secmem.NewSyscallSafeBuffer: %w", err)
}
if err := fillCanary(region.inner[len(data):]); err != nil {
_ = freeSecretMem(region)
return nil, fmt.Errorf("secmem.NewSyscallSafeBuffer: %w", err)
}
copy(data, raw)
return newSecureBuffer(region, data, info), nil
}
// newSecureBuffer wires up a SecureBuffer from a pre-allocated (region, data)
// pair plus its allocation facts, and registers the AddCleanup finalization
// fallback. The canary slack must already be filled by the caller.
//
// The janitor key is passed to AddCleanup by value; cleanup resolution happens
// through emergencyJanitor's raw-mapping registry.
func newSecureBuffer(region secRegion, data []byte, backing allocInfo) *SecureBuffer {
sb := &SecureBuffer{
data: data,
region: region,
mu: newBufferRWLock(),
backing: backing,
sealCipher: new(atomic.Bool),
wiped: new(atomic.Bool),
}
// The canary zone is the slack between the caller's size and the page
// boundary. cap(data) is clamped to the original size and survives
// Truncate re-slices, so the zone stays correct for the buffer's lifetime.
canary := bufferCanary(cap(data), len(region.inner))
// Register with the emergency janitor first. The janitor stores raw mapping
// metadata (not *SecureBuffer), so this does not keep sb reachable for GC.
sb.janitorKey = emergencyJanitor.register(region, canary, sb.mu, sb.sealCipher, sb.wiped)
// Safety-net cleanup: if the caller forgets Destroy(), this wipes and frees
// the mmap'd region when the *SecureBuffer is GC'd.
//
// runtime.AddCleanup callbacks MUST NOT reference sb directly (that would
// keep sb alive and prevent the cleanup from running). The raw slice is
// passed as the argument, capturing only the off-heap mapping metadata.
//
// IMPORTANT: The cleanup fires when sb becomes unreachable — NOT when all
// references to data are gone. Any retained []byte from WithBytes
// becomes a dangling pointer after the cleanup runs.
sb.cleanup = runtime.AddCleanup(sb, func(key uintptr) {
slog.Warn("secmem: SecureBuffer finalized without explicit Destroy()",
slog.Int("size", len(region.inner)),
slog.String("advice", "call Destroy() explicitly for deterministic wipe"),
)
if err := emergencyJanitor.release(key, false); err != nil {
slog.Error("secmem: SecureBuffer cleanup release failed",
slog.Any("error", err),
)
}
}, sb.janitorKey)
return sb
}
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
// Destroy performs an architectural wipe of the entire mapped region:
//
// 1. Stop the AddCleanup fallback (prevents double-free).
// 2. Mprotect(RW) — ensure the page is writable before wiping.
// 3. secureWipeSlice — zero + CLFLUSH/CLFLUSHOPT + SFENCE/LFENCE.
// 4. Madvise(DONTNEED) — release physical frames immediately.
// 5. freeSecretMem — Munlock + Munmap / VirtualUnlock + VirtualFree.
// 6. Nil out data and raw — makes Destroy idempotent.
// 7. runtime.KeepAlive(s) — ensures the GC does not run the cleanup
// concurrently between Stop() and the wipe.
//
// Destroy is idempotent and goroutine-safe. After Destroy, IsDestroyed()
// returns true and all subsequent method calls return ErrDestroyed.
func (s *SecureBuffer) Destroy() error {
if s == nil {
return nil
}
s.mu.lock()
defer s.mu.unlock()
if s.region.inner == nil {
return nil // already destroyed — idempotent
}
// Stop the safety-net cleanup first. If Destroy was called explicitly
// (the expected path), the cleanup is no longer needed. Stop() is a no-op
// if the cleanup has already fired.
s.cleanup.Stop()
// A sealed-encrypted buffer is decrypted before release so the janitor's
// canary verification sees the real slack, not ciphertext. On any failure
// the flag stays set and wipeAndFree skips the canary check instead —
// the wipe and unmap are never skipped.
if s.sealCipher.Load() {
if err := mprotectSecretMem(s.region, 3 /*PROT_READ|PROT_WRITE*/); err == nil {
if derr := sealDecrypt(s.region); derr == nil {
s.sealCipher.Store(false)
}
}
}
// Take exclusive ownership from janitor registry and wipe/free exactly once.
// If the entry is already gone (cleanup or emergency-wipe path won), treat as
// successfully destroyed and skip touching raw to avoid use-after-free.
err := emergencyJanitor.release(s.janitorKey, true)
// Step 5 — nil references. Makes IsDestroyed() true and Destroy idempotent.
s.data = nil
s.region = secRegion{}
// Step 6 — ensure the GC does not run the cleanup concurrently between
// Stop() and here. KeepAlive pins s in the liveness analysis until this
// point, preventing the finalizer goroutine from scheduling the already-
// Stopped cleanup during the wipe window.
runtime.KeepAlive(s)
if err != nil {
return fmt.Errorf("secmem.SecureBuffer.Destroy: %w", err)
}
return nil
}
// IsDestroyed reports whether the buffer has been destroyed.
func (s *SecureBuffer) IsDestroyed() bool {
if s == nil {
return true
}
s.mu.rLock()
defer s.mu.rUnlock()
return s.region.inner == nil
}
// ---------------------------------------------------------------------------
// Size & mprotect
// ---------------------------------------------------------------------------
// Len returns the usable size of the buffer (the size requested by the caller).
// May be smaller than [MappedLen] due to page-rounding.
func (s *SecureBuffer) Len() int {
if s == nil {
return 0
}
s.mu.rLock()
defer s.mu.rUnlock()
return len(s.data)
}
// MappedLen returns the size of the locked secret area (the page-rounded
// region holding the data and its canary slack). Always a multiple of the OS
// page size (≥ Len). The PROT_NONE guard pages bracketing the area are NOT
// counted — they are reserved address space, not lockable memory.
func (s *SecureBuffer) MappedLen() int {
if s == nil {
return 0
}
s.mu.rLock()
defer s.mu.rUnlock()
return len(s.region.inner)
}
// ReadOnly sets the buffer's memory protection to read-only.
// This prevents accidental overwrites once a secret is fully loaded.
// Call [ReadWrite] before [Destroy] to restore write access.
//
// The exclusive lock is held to drain all in-flight Write/SetByteAt calls
// before the mprotect, preventing a SIGSEGV from a concurrent write hitting a
// PROT_READ page.
//
// NOTE: Operates on the full page-rounded region; sub-page protection
// is not possible on any supported OS.
func (s *SecureBuffer) ReadOnly() error {
if s == nil {
return errors.New("secmem.SecureBuffer.ReadOnly: nil receiver")
}
s.mu.lock()
defer s.mu.unlock()
if s.region.inner == nil {
return fmt.Errorf("secmem.SecureBuffer.ReadOnly: %w", ErrDestroyed)
}
if s.wiped.Load() {
return fmt.Errorf("secmem.SecureBuffer.ReadOnly: %w", ErrWiped)
}
if s.sealed {
return fmt.Errorf("secmem.SecureBuffer.ReadOnly: %w", ErrSealed)
}
if err := mprotectSecretMem(s.region, 1 /*PROT_READ*/); err != nil {
return fmt.Errorf("secmem.SecureBuffer.ReadOnly: %w", err)
}
s.readOnly = true
return nil
}
// ReadWrite restores read-write access to the buffer.
// Must be called before [Destroy] if [ReadOnly] was previously applied.
//
// The exclusive lock is held to drain all in-flight access before the
// mprotect.
func (s *SecureBuffer) ReadWrite() error {
if s == nil {
return errors.New("secmem.SecureBuffer.ReadWrite: nil receiver")
}
s.mu.lock()
defer s.mu.unlock()
if s.region.inner == nil {
return fmt.Errorf("secmem.SecureBuffer.ReadWrite: %w", ErrDestroyed)
}
if s.wiped.Load() {
return fmt.Errorf("secmem.SecureBuffer.ReadWrite: %w", ErrWiped)
}
if s.sealed {
return fmt.Errorf("secmem.SecureBuffer.ReadWrite: %w", ErrSealed)
}
if err := mprotectSecretMem(s.region, 3 /*PROT_READ|PROT_WRITE*/); err != nil {
return fmt.Errorf("secmem.SecureBuffer.ReadWrite: %w", err)
}
s.readOnly = false
return nil
}
// Seal sets the buffer's memory protection to PROT_NONE, making any access
// (including speculative reads) cause a hardware fault. This is the hardened
// dormant state for long-lived secrets that are not actively being used.
//
// On Windows, Seal additionally encrypts the contents in place with a
// KERNEL-HELD per-boot key (CryptProtectMemory): a full process memory dump
// taken while the buffer is sealed — procdump, Task Manager, a WER full dump,
// the hibernation file — contains ciphertext, and the key is not in the dump.
// This protects the sealed window only, and is not a defense against code
// executing inside the process (which can call CryptUnprotectMemory itself)
// nor against cold-boot RAM capture (the kernel's key is in RAM too). On
// other platforms Seal is page protection only; on Linux the allocation-time
// protections (memfd_secret, MADV_DONTDUMP) are the dump defenses.
//
// While sealed, all access methods (WithBytes, WithBytesErr, CopyOut, CopyIn,
// etc.) return [ErrSealed]. Call [SecureBuffer.Unseal] before accessing the
// buffer.
//
// [SecureBuffer.Destroy] works correctly on sealed buffers — it lifts the
// PROT_NONE restriction (and decrypts) internally before wiping.
//
// Note: [ReadOnly] and [ReadWrite] return [ErrSealed] while sealed. To
// transition from Sealed to ReadOnly, call Unseal then ReadOnly. A buffer that
// was read-only before Seal stays read-only after [SecureBuffer.Unseal] — the
// protection is preserved across the seal cycle.
//
// Seal is idempotent: calling it on an already-sealed buffer is a no-op.
func (s *SecureBuffer) Seal() error {
if s == nil {
return errors.New("secmem.SecureBuffer.Seal: nil receiver")
}
s.mu.lock()
defer s.mu.unlock()
if s.region.inner == nil {
return fmt.Errorf("secmem.SecureBuffer.Seal: %w", ErrDestroyed)
}
if s.wiped.Load() {
return fmt.Errorf("secmem.SecureBuffer.Seal: %w", ErrWiped)
}
if s.sealed {
return nil // idempotent
}
// The seal cipher encrypts in place (Windows: CryptProtectMemory), so the
// region must be writable during Seal. If the caller had set it read-only,
// lift the PROT_READ protection just for the encrypt: the region ends at
// PROT_NONE regardless, s.readOnly stays set, and Unseal re-applies
// PROT_READ. On non-Windows the cipher is a no-op, but the lift keeps the
// path uniform; the rollback paths below restore PROT_READ.
if s.readOnly {
if err := mprotectSecretMem(s.region, 3 /*PROT_READ|PROT_WRITE*/); err != nil {
return fmt.Errorf("secmem.SecureBuffer.Seal: %w", err)
}
}
// Encrypt BEFORE dropping write access. The flag is set immediately so
// the janitor's emergency-wipe path never canary-checks ciphertext.
applied, err := sealEncrypt(s.region)
if err != nil {
s.reapplyReadOnly()
return fmt.Errorf("secmem.SecureBuffer.Seal: %w", err)
}
if applied {
s.sealCipher.Store(true)
}
if err := mprotectSecretMem(s.region, 0 /*PROT_NONE*/); err != nil {
// Roll the cipher back so the buffer stays usable plaintext.
if applied {
if derr := sealDecrypt(s.region); derr == nil {
s.sealCipher.Store(false)
}
}
s.reapplyReadOnly()
return fmt.Errorf("secmem.SecureBuffer.Seal: %w", err)
}
s.sealed = true
return nil
}
// reapplyReadOnly re-applies PROT_READ when the buffer is flagged read-only.
// Seal lifts that protection to run the in-place seal cipher; on a Seal
// rollback path this restores it so the physical page protection stays in sync
// with s.readOnly. Best-effort — the caller is already returning an error.
// Caller holds s.mu and the region is live.
func (s *SecureBuffer) reapplyReadOnly() {
if s.readOnly {
_ = mprotectSecretMem(s.region, 1 /*PROT_READ*/)
}
}
// Unseal lifts the PROT_NONE protection applied by [SecureBuffer.Seal],
// restoring PROT_READ|PROT_WRITE access to the buffer.
//
// After Unseal, all access methods work normally. To re-protect after use,
// call Seal again.
//
// Unseal is idempotent: calling it on an already-unsealed buffer is a no-op.
func (s *SecureBuffer) Unseal() error {
if s == nil {
return errors.New("secmem.SecureBuffer.Unseal: nil receiver")
}
s.mu.lock()
defer s.mu.unlock()
if s.region.inner == nil {
return fmt.Errorf("secmem.SecureBuffer.Unseal: %w", ErrDestroyed)
}
if s.wiped.Load() {
return fmt.Errorf("secmem.SecureBuffer.Unseal: %w", ErrWiped)
}
if !s.sealed {
return nil // idempotent
}
if err := mprotectSecretMem(s.region, 3 /*PROT_READ|PROT_WRITE*/); err != nil {
return fmt.Errorf("secmem.SecureBuffer.Unseal: %w", err)
}
if s.sealCipher.Load() {
if err := sealDecrypt(s.region); err != nil {
// Contents are still ciphertext: re-protect and stay sealed so
// no access method can hand out garbage as the secret.
_ = mprotectSecretMem(s.region, 0 /*PROT_NONE*/)
return fmt.Errorf("secmem.SecureBuffer.Unseal: %w", err)
}
s.sealCipher.Store(false)
}
// Unseal lifted the region to PROT_READ|PROT_WRITE to decrypt in place. If
// the caller had set the buffer read-only before sealing, re-apply
// PROT_READ so the physical protection matches s.readOnly — a post-Unseal
// mutator then refuses with ErrReadOnly instead of faulting. On failure the
// buffer stays sealed (fail closed) rather than exposing a writable region.
if s.readOnly {
if err := mprotectSecretMem(s.region, 1 /*PROT_READ*/); err != nil {
return fmt.Errorf("secmem.SecureBuffer.Unseal: restoring read-only: %w", err)
}
}
s.sealed = false
return nil
}
// IsSealed reports whether the buffer is currently in the sealed (PROT_NONE) state.
func (s *SecureBuffer) IsSealed() bool {
if s == nil {
return false
}
s.mu.rLock()
defer s.mu.rUnlock()
return s.sealed
}
// Truncate re-slices data to n bytes and wipes the freed tail [n:].
//
// Invariant: raw is NEVER modified. Only data is re-sliced.
// This ensures the AddCleanup finalization closure always holds the correct
// full-page allocation regardless of Truncate calls.
func (s *SecureBuffer) Truncate(n int) error {
if s == nil {
return errors.New("secmem.SecureBuffer.Truncate: nil receiver")
}
s.mu.lock()
defer s.mu.unlock()
if s.region.inner == nil {
return fmt.Errorf("secmem.SecureBuffer.Truncate: %w", ErrDestroyed)
}
if s.wiped.Load() {
return fmt.Errorf("secmem.SecureBuffer.Truncate: %w", ErrWiped)
}
if s.sealed {
// The region is PROT_NONE while sealed; wiping the freed tail would
// fault. Match the other mutating methods (CopyIn/SetByteAt/ReadFrom).
return fmt.Errorf("secmem.SecureBuffer.Truncate: %w", ErrSealed)
}
if s.readOnly {
// The region is PROT_READ; wiping the freed tail would fault. Refuse
// at the API boundary instead of crashing.
return fmt.Errorf("secmem.SecureBuffer.Truncate: %w", ErrReadOnly)
}
if n < 0 || n > len(s.data) {
return fmt.Errorf("secmem.SecureBuffer.Truncate: n=%d out of range [0, %d]", n, len(s.data))
}
tail := s.data[n:]
if len(tail) > 0 {
secureWipeSlice(tail)
}
s.data = s.data[:n]
return nil
}
// ---------------------------------------------------------------------------
// Internals
// ---------------------------------------------------------------------------