From f5cc259c50472b37c2d4f5da44b160d3992fbbc5 Mon Sep 17 00:00:00 2001 From: CMGS Date: Sat, 4 Jul 2026 03:03:34 +0800 Subject: [PATCH 1/2] fix(reseed): guarantee a unique machine-id per clone; zero host entropy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit systemd-machine-id-setup derives the machine-id from the SMBIOS product_uuid in a VM, which a snapshot clone inherits verbatim — so every clone regenerated the SAME id, silently defeating the reseed verb's whole purpose (cocoon assigns no per-clone SMBIOS UUID). Always write a fresh random id instead and drop the DMI-derived path. Writing the new id first also removes the empty-machine-id window a failing drop-dbus step used to leave. Zero the single-use host entropy after the mix, and note the agent's 512-byte cap on the client. --- agent/reseed_linux.go | 55 ++++++++++++++++++++++---------------- agent/reseed_linux_test.go | 22 +++++++++++++++ client/client.go | 3 ++- 3 files changed, 56 insertions(+), 24 deletions(-) diff --git a/agent/reseed_linux.go b/agent/reseed_linux.go index 8502121..3fc6f54 100644 --- a/agent/reseed_linux.go +++ b/agent/reseed_linux.go @@ -11,9 +11,9 @@ import ( "fmt" "io/fs" "os" - "os/exec" "unsafe" + "github.com/projecteru2/core/log" "golang.org/x/sys/unix" ) @@ -35,6 +35,7 @@ func runReseed(ctx context.Context, req Message, enc *Encoder) error { if err := reseedURandom(req.Data); err != nil { errs = append(errs, err) } + clear(req.Data) // host entropy is single-use; don't leave it on the heap if err := os.Remove(systemdRandomSeed); err != nil && !errors.Is(err, fs.ErrNotExist) { errs = append(errs, fmt.Errorf("remove systemd random seed: %w", err)) } @@ -89,6 +90,7 @@ func addEntropy(fd int, data []byte) error { bufSize := uint32(len(data)) //nolint:gosec // len(data) capped at maxReseedEntropyBytes above buf := make([]byte, 8+len(data)) + defer clear(buf) // the payload copy is single-use; zero it after the mix binary.NativeEndian.PutUint32(buf[0:4], entropyBits) binary.NativeEndian.PutUint32(buf[4:8], bufSize) copy(buf[8:], data) @@ -106,8 +108,11 @@ func reseedCRNG(fd int) error { return nil } -// regenMachineID truncates /etc/machine-id and regenerates it, skipping -// silently on systems that don't have the file (e.g. Android). +// regenMachineID overwrites /etc/machine-id with a fresh random id so clones of +// one snapshot don't share it. systemd-machine-id-setup is deliberately avoided: +// in a VM it derives the id from the SMBIOS product_uuid, which a snapshot clone +// inherits verbatim — every clone would regenerate the same id. Skips silently +// when the file is absent (e.g. Android). func regenMachineID(ctx context.Context) error { if _, err := os.Stat(machineIDPath); err != nil { if errors.Is(err, fs.ErrNotExist) { @@ -115,24 +120,20 @@ func regenMachineID(ctx context.Context) error { } return fmt.Errorf("stat %s: %w", machineIDPath, err) } - if err := os.Truncate(machineIDPath, 0); err != nil { - return fmt.Errorf("truncate %s: %w", machineIDPath, err) - } - if err := dropStaleDBusMachineID(dbusMachineIDPath); err != nil { + if err := writeRandomMachineID(); err != nil { return err } - if path, err := exec.LookPath("systemd-machine-id-setup"); err == nil { - if err := exec.CommandContext(ctx, path).Run(); err == nil { //nolint:gosec // path resolved by LookPath for a fixed binary name, not user input - return nil - } - // Fall through: a failed setup must not leave the truncated file empty. + // The D-Bus copy is secondary; /etc/machine-id is already fresh, so a drop + // failure can't strand the system id-less — best-effort. + if err := dropStaleDBusMachineID(dbusMachineIDPath); err != nil { + log.WithFunc("agent.regenMachineID").Debugf(ctx, "drop stale dbus machine id: %v", err) } - return writeRandomMachineID() + return nil } -// dropStaleDBusMachineID removes a baked regular-file D-Bus copy that -// systemd-machine-id-setup would re-adopt; a symlink or missing file is left -// alone, and any other lstat error is surfaced so regen fails loudly. +// dropStaleDBusMachineID removes a baked regular-file D-Bus copy that would +// otherwise pin the old id; a symlink or missing file already tracks +// /etc/machine-id and is left alone. func dropStaleDBusMachineID(path string) error { fi, err := os.Lstat(path) if err != nil { @@ -150,16 +151,24 @@ func dropStaleDBusMachineID(path string) error { return nil } -// writeRandomMachineID is the fallback when systemd-machine-id-setup is -// unavailable: a fresh random id, matching /etc/machine-id's own format. +// writeRandomMachineID overwrites /etc/machine-id with a fresh random id. func writeRandomMachineID() error { - buf := make([]byte, machineIDBytes) - if _, err := rand.Read(buf); err != nil { - return fmt.Errorf("generate machine id: %w", err) + id, err := randomMachineID() + if err != nil { + return err } - line := hex.EncodeToString(buf) + "\n" - if err := os.WriteFile(machineIDPath, []byte(line), 0o444); err != nil { //nolint:gosec // matches /etc/machine-id's own world-readable convention + if err := os.WriteFile(machineIDPath, []byte(id), 0o444); err != nil { //nolint:gosec // matches /etc/machine-id's own world-readable convention return fmt.Errorf("write %s: %w", machineIDPath, err) } return nil } + +// randomMachineID returns a fresh id in /etc/machine-id's canonical +// 32-hex-lowercase + newline format. +func randomMachineID() (string, error) { + buf := make([]byte, machineIDBytes) + if _, err := rand.Read(buf); err != nil { + return "", fmt.Errorf("generate machine id: %w", err) + } + return hex.EncodeToString(buf) + "\n", nil +} diff --git a/agent/reseed_linux_test.go b/agent/reseed_linux_test.go index a210878..ed09dcc 100644 --- a/agent/reseed_linux_test.go +++ b/agent/reseed_linux_test.go @@ -7,9 +7,31 @@ import ( "io/fs" "os" "path/filepath" + "regexp" "testing" ) +// machineIDRe matches /etc/machine-id's canonical 32-hex-lowercase + newline form. +var machineIDRe = regexp.MustCompile(`^[0-9a-f]{32}\n$`) + +func TestRandomMachineID(t *testing.T) { + a, err := randomMachineID() + if err != nil { + t.Fatalf("randomMachineID: %v", err) + } + if !machineIDRe.MatchString(a) { + t.Errorf("id %q not canonical 32-hex+newline", a) + } + // Uniqueness is the whole point: a snapshot clone must not reproduce it. + b, err := randomMachineID() + if err != nil { + t.Fatalf("randomMachineID: %v", err) + } + if a == b { + t.Error("two random machine ids are identical") + } +} + func TestDropStaleDBusMachineID(t *testing.T) { t.Run("regular file is removed", func(t *testing.T) { path := filepath.Join(t.TempDir(), "machine-id") diff --git a/client/client.go b/client/client.go index 143ddbe..9b8569b 100644 --- a/client/client.go +++ b/client/client.go @@ -112,7 +112,8 @@ readLoop: // Reseed sends host-fed entropy and a reseed order after a VM clone/restore, // so N clones sharing byte-identical snapshot memory don't share -// byte-identical CRNG state. nil iff the agent reports exit code 0. +// byte-identical CRNG state. Entropy beyond 512 bytes is capped by the agent. +// nil iff the agent reports exit code 0. func Reseed(ctx context.Context, conn io.ReadWriteCloser, entropy []byte, regenMachineID bool) error { _, dec, cancel, err := openSession(ctx, conn, agent.Message{Type: agent.MsgReseed, Data: entropy, RegenMachineID: regenMachineID}) if err != nil { From 6215dcefd1b2d856a81be8f0b3a9050a7e518a4b Mon Sep 17 00:00:00 2001 From: CMGS Date: Sat, 4 Jul 2026 09:53:24 +0800 Subject: [PATCH 2/2] fix(reseed): warn on stale dbus machine-id drop failure A surviving /var/lib/dbus/machine-id keeps serving the old id to dbus consumers after a clone; a drop failure is a real uniqueness gap, so surface it at Warn instead of Debug. --- agent/reseed_linux.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agent/reseed_linux.go b/agent/reseed_linux.go index 3fc6f54..44e07d0 100644 --- a/agent/reseed_linux.go +++ b/agent/reseed_linux.go @@ -123,10 +123,10 @@ func regenMachineID(ctx context.Context) error { if err := writeRandomMachineID(); err != nil { return err } - // The D-Bus copy is secondary; /etc/machine-id is already fresh, so a drop - // failure can't strand the system id-less — best-effort. + // Best-effort: /etc/machine-id is already fresh, but a surviving D-Bus copy + // keeps serving the old id to dbus consumers — worth a warning. if err := dropStaleDBusMachineID(dbusMachineIDPath); err != nil { - log.WithFunc("agent.regenMachineID").Debugf(ctx, "drop stale dbus machine id: %v", err) + log.WithFunc("agent.regenMachineID").Warnf(ctx, "drop stale dbus machine id: %v", err) } return nil }