Files
egress/memory_test.go
zeekay 5423ac5213 a provider key cannot be swapped to disk
Egress writes no file, refuses core dumps, mounts no writable path and hides
/proc — and none of that governs the kernel, which pages anonymous memory to
swap whenever it likes. A key held only in RAM is still a key on a disk once it
has been swapped, and it stays there after the process is gone. That was the
last way a credential reached one.

It now calls mlockall(MCL_CURRENT|MCL_FUTURE) BEFORE opening the store, because
the credential does not exist yet at that point and locking after a secret is in
memory locks it too late. MCL_FUTURE is the half that matters for the same
reason. The unit grants LimitMEMLOCK=infinity; without it the process refuses to
start rather than serving without the guarantee, since "best effort" means it
holds on some hosts and not others with nothing to say which.

Memory encryption is reported rather than assumed: pinning keeps a key off the
disk and does nothing about the DRAM, so the boot line names sev-snp, tdx or
none and a deployment that needs one can be held to it. Off Linux these pages
cannot be pinned, so egress refuses there rather than serving quietly without
the property it is built on.

The deploy doc states the other two guarantees that were true and unwritten:
nothing is kept, so replicas are interchangeable and a suspect one is deleted
rather than investigated; and --with-key is an operator's choice between host
and tpm2 that egress never learns, with a cloud KMS or PKCS#11 HSM named as the
integration it would take rather than a flag that exists.
2026-08-22 12:14:18 -07:00

48 lines
1.8 KiB
Go

// Copyright 2026 Hanzo AI, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
package egress
import (
"runtime"
"strings"
"testing"
)
// The claim this service makes is that a provider key never reaches a disk, and
// swap is the last way it could. So the lock either holds or the process does
// not serve — and when it cannot hold, the refusal has to name the grant that
// fixes it, because "cannot allocate memory" from mlockall reads like a machine
// out of RAM rather than a missing capability.
func TestMemoryIsLockedOrTheRefusalSaysHow(t *testing.T) {
err := LockMemory()
if err == nil {
if runtime.GOOS != "linux" {
t.Fatalf("LockMemory succeeded on %s, where these pages cannot be pinned", runtime.GOOS)
}
return // locked: the guarantee holds on this host
}
if runtime.GOOS != "linux" {
return // refusing off Linux is the documented answer
}
// It failed on Linux, which is allowed — an unprivileged test process has a
// small RLIMIT_MEMLOCK. What is not allowed is a refusal nobody can act on.
for _, want := range []string{"LimitMEMLOCK", "CAP_IPC_LOCK", "swap"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("the refusal does not name %q, so a reader cannot act on it: %v", want, err)
}
}
}
// Memory encryption is reported, never assumed. Locking keeps a key off the
// disk and says nothing about the DRAM, so a deployment that needs SEV-SNP or
// TDX has to be able to read which is in force rather than trust that it is.
func TestMemoryEncryptionIsReported(t *testing.T) {
got := MemoryEncryption()
if strings.TrimSpace(got) == "" {
t.Fatal("MemoryEncryption() is empty — it must say 'none' rather than say nothing, " +
"or an absent guarantee reads as an unasked question")
}
t.Logf("memory encryption on this host: %s", got)
}