Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 32 additions & 23 deletions agent/reseed_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import (
"fmt"
"io/fs"
"os"
"os/exec"
"unsafe"

"github.com/projecteru2/core/log"
"golang.org/x/sys/unix"
)

Expand All @@ -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))
}
Expand Down Expand Up @@ -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)
Expand All @@ -106,33 +108,32 @@ 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) {
return nil
}
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.
// 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").Warnf(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 {
Expand All @@ -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
}
22 changes: 22 additions & 0 deletions agent/reseed_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
3 changes: 2 additions & 1 deletion client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down