From a2f8f132f7e596dbd2c7438f5cb475d2a95e8982 Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Sun, 31 May 2026 16:54:19 -0400 Subject: [PATCH] Experiment with VMClock resume network signal --- Makefile | 2 +- lib/hypervisor/firecracker/binaries.go | 4 +- lib/instances/guest_resume_network.go | 97 +++++++- .../resume_network_signal_perf_test.go | 184 +++++++++++++++ lib/system/guest_agent/resume_network.go | 216 +++++++++++++++++- lib/system/versions.go | 14 +- 6 files changed, 503 insertions(+), 14 deletions(-) create mode 100644 lib/instances/resume_network_signal_perf_test.go diff --git a/Makefile b/Makefile index 611bb11b..830b2eb6 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,7 @@ download-ch-binaries: @echo "Binaries downloaded successfully" # Firecracker version to embed -FIRECRACKER_VERSION := v1.14.2 +FIRECRACKER_VERSION := v1.15.1 # Download Firecracker binaries download-firecracker-binaries: diff --git a/lib/hypervisor/firecracker/binaries.go b/lib/hypervisor/firecracker/binaries.go index d9e2e104..0146010a 100644 --- a/lib/hypervisor/firecracker/binaries.go +++ b/lib/hypervisor/firecracker/binaries.go @@ -18,11 +18,13 @@ type Version string const ( V1_14_2 Version = "v1.14.2" + V1_15_1 Version = "v1.15.1" ) -const defaultVersion = V1_14_2 +const defaultVersion = V1_15_1 var supportedVersions = []Version{ + V1_15_1, V1_14_2, } diff --git a/lib/instances/guest_resume_network.go b/lib/instances/guest_resume_network.go index 354657b0..dd27d5e7 100644 --- a/lib/instances/guest_resume_network.go +++ b/lib/instances/guest_resume_network.go @@ -6,9 +6,11 @@ import ( "encoding/binary" "encoding/json" "fmt" + "io" stdnet "net" "os" "path/filepath" + "strconv" "strings" "sync" "time" @@ -16,11 +18,13 @@ import ( "github.com/kernel/hypeman/lib/hypervisor" "github.com/kernel/hypeman/lib/logger" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "golang.org/x/sys/unix" ) const guestResumeNetworkMailboxEnv = "HYPEMAN_RESUME_NETWORK_MAILBOX" const guestResumeNetworkMailboxTokenEnv = "HYPEMAN_RESUME_NETWORK_MAILBOX_TOKEN" +const guestResumeNetworkPrefetchBytesEnv = "HYPEMAN_RESUME_NETWORK_PREFETCH_BYTES" const firecrackerSnapshotMemoryFile = "memory" const guestResumeNetworkMailboxSeqOffset = 64 @@ -44,6 +48,12 @@ type guestResumeNetworkUDPAck struct { text string } +type guestResumeNetworkUDPWaitResult struct { + appliedElapsed time.Duration + appliedAck string + stageElapsed map[string]time.Duration +} + type guestResumeNetworkUDPWaiter struct { conn *stdnet.UDPConn ch chan guestResumeNetworkUDPAck @@ -117,27 +127,49 @@ func (w *guestResumeNetworkUDPWaiter) readLoop() { } } -func (w *guestResumeNetworkUDPWaiter) WaitApplied(ctx context.Context, mac, ip string) (time.Duration, string, error) { +func (w *guestResumeNetworkUDPWaiter) WaitApplied(ctx context.Context, mac, ip string) (guestResumeNetworkUDPWaitResult, error) { if w == nil { - return 0, "", fmt.Errorf("guest resume network UDP waiter is nil") + return guestResumeNetworkUDPWaitResult{}, fmt.Errorf("guest resume network UDP waiter is nil") } start := time.Now() wantMAC := "mac=" + strings.ToLower(mac) wantIP := "ip=" + ip + result := guestResumeNetworkUDPWaitResult{ + stageElapsed: make(map[string]time.Duration), + } for { select { case ack := <-w.ch: text := strings.ToLower(ack.text) - if strings.Contains(text, "stage=applied") && strings.Contains(text, wantMAC) && strings.Contains(text, wantIP) { - return ack.received.Sub(start), ack.text, nil + if !strings.Contains(text, wantMAC) || !strings.Contains(text, wantIP) { + continue + } + if stage, ok := guestResumeNetworkAckStage(text); ok { + if _, exists := result.stageElapsed[stage]; !exists { + result.stageElapsed[stage] = ack.received.Sub(start) + } + } + if strings.Contains(text, "stage=applied") { + result.appliedElapsed = ack.received.Sub(start) + result.appliedAck = ack.text + return result, nil } case <-ctx.Done(): - return 0, "", ctx.Err() + return guestResumeNetworkUDPWaitResult{}, ctx.Err() } } } +func guestResumeNetworkAckStage(text string) (string, bool) { + for _, field := range strings.Fields(text) { + if stage, ok := strings.CutPrefix(field, "stage="); ok && stage != "" { + return stage, true + } + } + return "", false +} + func (m *manager) waitForGuestResumeNetworkUDPAck(ctx context.Context, waiter *guestResumeNetworkUDPWaiter, stored *StoredMetadata, cfg *guestNetworkConfig) error { if waiter == nil || cfg == nil { return nil @@ -152,12 +184,16 @@ func (m *manager) waitForGuestResumeNetworkUDPAck(ctx context.Context, waiter *g waitCtx, cancel := context.WithTimeout(waitCtx, 2*time.Second) defer cancel() - elapsed, ack, err := waiter.WaitApplied(waitCtx, cfg.mac, cfg.ip) + result, err := waiter.WaitApplied(waitCtx, cfg.mac, cfg.ip) + span := trace.SpanFromContext(waitCtx) + for stage, elapsed := range result.stageElapsed { + span.SetAttributes(attribute.Int64("guest_resume_network_ack_"+stage+"_ms", elapsed.Milliseconds())) + } waitSpanEnd(err) if err != nil { return err } - log.InfoContext(ctx, "guest resume network UDP ack received", "instance_id", stored.Id, "elapsed", elapsed, "ack", ack) + log.InfoContext(ctx, "guest resume network UDP ack received", "instance_id", stored.Id, "elapsed", result.appliedElapsed, "ack", result.appliedAck, "stages", result.stageElapsed) return nil } @@ -218,6 +254,53 @@ func patchGuestResumeNetworkMailbox(snapshotDir, token string, payload *guestRes if _, err := file.WriteAt(u32[:], idx+int64(guestResumeNetworkMailboxSeqOffset)); err != nil { return fmt.Errorf("write resume network mailbox sequence: %w", err) } + if n := guestResumeNetworkPrefetchBytes(); n > 0 { + if err := prefetchSnapshotMemoryWindow(file, info.Size(), idx, n); err != nil { + return fmt.Errorf("prefetch resume network mailbox memory window: %w", err) + } + } + return nil +} + +func guestResumeNetworkPrefetchBytes() int64 { + raw := strings.TrimSpace(os.Getenv(guestResumeNetworkPrefetchBytesEnv)) + if raw == "" { + return 0 + } + n, err := strconv.ParseInt(raw, 10, 64) + if err != nil || n <= 0 { + return 0 + } + return n +} + +func prefetchSnapshotMemoryWindow(file *os.File, size int64, center int64, windowBytes int64) error { + if windowBytes <= 0 || size <= 0 { + return nil + } + start := center - windowBytes/2 + if start < 0 { + start = 0 + } + if start > size { + start = size + } + end := start + windowBytes + if end > size { + end = size + } + + buf := make([]byte, 1024*1024) + for off := start; off < end; { + n := int64(len(buf)) + if remaining := end - off; remaining < n { + n = remaining + } + if _, err := file.ReadAt(buf[:n], off); err != nil && err != io.EOF { + return err + } + off += n + } return nil } diff --git a/lib/instances/resume_network_signal_perf_test.go b/lib/instances/resume_network_signal_perf_test.go new file mode 100644 index 00000000..a2082917 --- /dev/null +++ b/lib/instances/resume_network_signal_perf_test.go @@ -0,0 +1,184 @@ +//go:build linux + +package instances + +import ( + "context" + "fmt" + "os" + "strconv" + "strings" + "testing" + "time" + + "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/images" + "github.com/kernel/hypeman/lib/paths" + snapshottest "github.com/kernel/hypeman/lib/snapshot/testsupport" + "github.com/kernel/hypeman/lib/system" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" +) + +const resumeNetworkSignalPerfEnv = "HYPEMAN_RUN_RESUME_NETWORK_SIGNAL_PERF" +const resumeNetworkSignalPerfItersEnv = "HYPEMAN_RESUME_NETWORK_SIGNAL_PERF_ITERS" +const resumeNetworkSignalPerfWaitEnv = "HYPEMAN_RESUME_NETWORK_SIGNAL_PERF_WAIT_FOR_NETWORK" +const resumeNetworkSignalGuestEnv = "HYPEMAN_RESUME_NETWORK_SIGNAL" +const resumeNetworkAckStagesGuestEnv = "HYPEMAN_RESUME_NETWORK_ACK_STAGES" + +func TestResumeNetworkSignalPerf(t *testing.T) { + if os.Getenv(resumeNetworkSignalPerfEnv) != "1" { + t.Skipf("set %s=1 to run resume network signal perf test", resumeNetworkSignalPerfEnv) + } + requireFirecrackerIntegrationPrereqs(t) + + recorder := tracetest.NewSpanRecorder() + provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)) + previousProvider := otel.GetTracerProvider() + otel.SetTracerProvider(provider) + t.Cleanup(func() { + otel.SetTracerProvider(previousProvider) + _ = provider.Shutdown(context.Background()) + }) + + ctx := context.Background() + mgr, tmpDir := setupTestManagerForFirecracker(t) + p := paths.New(tmpDir) + imageManager, err := images.NewManager(p, 1, nil) + require.NoError(t, err) + imageName := integrationTestImageRef(t, "docker.io/library/alpine:latest") + snapshottest.EnsureImageReady(t, ctx, p, imageManager, imageName) + + systemManager := system.NewManager(p) + require.NoError(t, systemManager.EnsureSystemFiles(ctx)) + require.NoError(t, mgr.networkManager.Initialize(ctx, nil)) + + signal := strings.TrimSpace(os.Getenv(resumeNetworkSignalGuestEnv)) + if signal == "" { + signal = "auto" + } + waitForNetwork := strings.TrimSpace(os.Getenv(resumeNetworkSignalPerfWaitEnv)) != "0" + env := map[string]string{ + guestResumeNetworkMailboxEnv: "1", + guestResumeNetworkMailboxTokenEnv: fmt.Sprintf("perf-%d", time.Now().UnixNano()), + resumeNetworkSignalGuestEnv: signal, + resumeNetworkAckStagesGuestEnv: "1", + } + + source, err := mgr.CreateInstance(ctx, CreateInstanceRequest{ + Name: "fc-rn-signal-src", + Image: imageName, + Size: 1024 * 1024 * 1024, + OverlaySize: 1024 * 1024 * 1024, + Vcpus: 1, + NetworkEnabled: true, + Hypervisor: hypervisor.TypeFirecracker, + Cmd: []string{"sleep", "infinity"}, + Env: env, + }) + require.NoError(t, err) + sourceID := source.Id + t.Cleanup(func() { _ = mgr.DeleteInstance(context.Background(), sourceID) }) + + source, err = waitForInstanceState(ctx, mgr, sourceID, StateRunning, integrationTestTimeout(45*time.Second)) + require.NoError(t, err) + require.NoError(t, waitForExecAgent(ctx, mgr, sourceID, 45*time.Second)) + + snapshot, err := mgr.CreateSnapshot(ctx, sourceID, CreateSnapshotRequest{ + Kind: SnapshotKindStandby, + Name: "fc-rn-signal-snap", + }) + require.NoError(t, err) + t.Cleanup(func() { _ = mgr.DeleteSnapshot(context.Background(), snapshot.Id) }) + + iterations := resumeNetworkSignalPerfIterations(t, 10) + for i := 1; i <= iterations; i++ { + before := len(recorder.Ended()) + start := time.Now() + fork, err := mgr.ForkSnapshot(ctx, snapshot.Id, ForkSnapshotRequest{ + Name: fmt.Sprintf("fc-rn-signal-%02d", i), + TargetState: StateRunning, + WaitForNetwork: &waitForNetwork, + }) + forkElapsed := time.Since(start) + require.NoError(t, err) + require.Equal(t, StateRunning, fork.State) + + spans := append([]sdktrace.ReadOnlySpan(nil), recorder.Ended()[before:]...) + t.Log(formatResumeNetworkSignalPerfLine(i, signal, waitForNetwork, forkElapsed, spans)) + + require.NoError(t, waitForExecAgent(ctx, mgr, fork.Id, 45*time.Second)) + _ = mgr.DeleteInstance(context.Background(), fork.Id) + } +} + +func resumeNetworkSignalPerfIterations(t *testing.T, fallback int) int { + t.Helper() + raw := strings.TrimSpace(os.Getenv(resumeNetworkSignalPerfItersEnv)) + if raw == "" { + return fallback + } + n, err := strconv.Atoi(raw) + require.NoError(t, err) + require.Positive(t, n) + return n +} + +func formatResumeNetworkSignalPerfLine(iter int, signal string, waitForNetwork bool, forkElapsed time.Duration, spans []sdktrace.ReadOnlySpan) string { + ackWait := lastSpanNamed(spans, "guest.resume_network.udp_ack_wait") + mailboxAckMS := spanAttrInt64(ackWait, "guest_resume_network_ack_mailbox_ms") + appliedAckMS := spanAttrInt64(ackWait, "guest_resume_network_ack_applied_ms") + applyAfterMailboxMS := int64(-1) + if mailboxAckMS >= 0 && appliedAckMS >= 0 { + applyAfterMailboxMS = appliedAckMS - mailboxAckMS + } + return fmt.Sprintf( + "PERF_SIGNAL iter=%d signal=%s wait_for_network=%t fork_total_ms=%d restore_from_snapshot_ms=%d resume_vm_ms=%d reconfigure_guest_network_ms=%d guest_resume_network_udp_ack_wait_ms=%d guest_mailbox_ack_ms=%d guest_applied_ack_ms=%d guest_apply_after_mailbox_ms=%d", + iter, + signal, + waitForNetwork, + forkElapsed.Milliseconds(), + spanDurationMS(lastSpanNamed(spans, "restore_from_snapshot")), + spanDurationMS(lastSpanNamed(spans, "resume_vm")), + spanDurationMS(lastSpanNamed(spans, "reconfigure_guest_network")), + spanDurationMS(ackWait), + mailboxAckMS, + appliedAckMS, + applyAfterMailboxMS, + ) +} + +func lastSpanNamed(spans []sdktrace.ReadOnlySpan, name string) sdktrace.ReadOnlySpan { + for i := len(spans) - 1; i >= 0; i-- { + if spans[i].Name() == name { + return spans[i] + } + } + return nil +} + +func spanDurationMS(span sdktrace.ReadOnlySpan) int64 { + if span == nil { + return -1 + } + return span.EndTime().Sub(span.StartTime()).Milliseconds() +} + +func spanAttrInt64(span sdktrace.ReadOnlySpan, key string) int64 { + if span == nil { + return -1 + } + for _, attr := range span.Attributes() { + if string(attr.Key) != key { + continue + } + switch attr.Value.Type() { + case attribute.INT64: + return attr.Value.AsInt64() + } + } + return -1 +} diff --git a/lib/system/guest_agent/resume_network.go b/lib/system/guest_agent/resume_network.go index 060cd687..f5344b76 100644 --- a/lib/system/guest_agent/resume_network.go +++ b/lib/system/guest_agent/resume_network.go @@ -24,11 +24,21 @@ import ( const resumeNetworkMailboxEnv = "HYPEMAN_RESUME_NETWORK_MAILBOX" const resumeNetworkMailboxTokenEnv = "HYPEMAN_RESUME_NETWORK_MAILBOX_TOKEN" +const resumeNetworkSignalEnv = "HYPEMAN_RESUME_NETWORK_SIGNAL" +const resumeNetworkAckStagesEnv = "HYPEMAN_RESUME_NETWORK_ACK_STAGES" const vmgenIDKmsgSignal = "crng reseeded due to virtual machine fork" const resumeNetworkMailboxSize = 4096 const resumeNetworkMailboxSeqOffset = 64 const resumeNetworkMailboxLengthOffset = 68 const resumeNetworkMailboxPayloadOffset = 72 +const vmClockDevicePath = "/dev/vmclock0" + +const vmClockABISize = 112 +const vmClockMagic = 0x4b4c4356 +const vmClockFlagsOffset = 24 +const vmClockGenerationCounterOffset = 104 +const vmClockFlagGenerationCounterPresent = 1 << 8 +const vmClockFlagNotificationPresent = 1 << 9 var resumeNetworkMailboxMagic = []byte("HYPEMAN_RESUME_NETWORK_MAILBOX_V1\x00") @@ -46,6 +56,23 @@ type vmGenIDResumeWaiter struct { reader *bufio.Reader } +type vmClockResumeWaiter struct { + file *os.File + generation uint64 +} + +type vmClockSpinResumeWaiter struct { + file *os.File + data []byte + generation uint64 +} + +type resumeNetworkWaiter interface { + Close() + Name() string + Wait() error +} + func startResumeNetworkWatcher(s *guestServer) { if strings.TrimSpace(os.Getenv(resumeNetworkMailboxEnv)) != "1" { return @@ -82,20 +109,22 @@ func newResumeNetworkMailbox() []byte { func resumeNetworkMailboxLoop(s *guestServer, mailbox []byte) { for { - waiter, err := newVMGenIDResumeWaiter() + waiter, err := newResumeNetworkWaiter() if err != nil { - log.Printf("[guest-agent] resume network VMGenID prepare failed: %v", err) + log.Printf("[guest-agent] resume network signal prepare failed: %v", err) time.Sleep(100 * time.Millisecond) continue } start := time.Now() if err := waiter.Wait(); err != nil { + name := waiter.Name() waiter.Close() - log.Printf("[guest-agent] resume network VMGenID wait failed: %v", err) + log.Printf("[guest-agent] resume network %s wait failed: %v", name, err) time.Sleep(100 * time.Millisecond) continue } + name := waiter.Name() waiter.Close() if err := waitAndApplyResumeNetworkMailbox(s, mailbox); err != nil { @@ -103,7 +132,7 @@ func resumeNetworkMailboxLoop(s *guestServer, mailbox []byte) { time.Sleep(25 * time.Millisecond) continue } - log.Printf("[guest-agent] resume network mailbox applied in %s", time.Since(start)) + log.Printf("[guest-agent] resume network mailbox applied after %s signal in %s", name, time.Since(start)) } } @@ -125,6 +154,10 @@ func waitAndApplyResumeNetworkMailbox(s *guestServer, buf []byte) error { return fmt.Errorf("decode mailbox payload: %w", err) } + if strings.TrimSpace(os.Getenv(resumeNetworkAckStagesEnv)) == "1" { + sendResumeNetworkAck(payload, "mailbox") + } + _, err := s.ReconfigureNetwork(context.Background(), &pb.ReconfigureNetworkRequest{ InterfaceName: payload.InterfaceName, Mac: payload.MAC, @@ -165,6 +198,27 @@ func atomicStoreUint32(buf []byte, value uint32) { atomic.StoreUint32((*uint32)(unsafe.Pointer(&buf[0])), value) } +func newResumeNetworkWaiter() (resumeNetworkWaiter, error) { + signal := strings.ToLower(strings.TrimSpace(os.Getenv(resumeNetworkSignalEnv))) + switch signal { + case "", "auto": + waiter, err := newVMClockResumeWaiter() + if err == nil { + return waiter, nil + } + log.Printf("[guest-agent] resume network VMClock unavailable, falling back to VMGenID: %v", err) + return newVMGenIDResumeWaiter() + case "vmclock": + return newVMClockResumeWaiter() + case "vmclock-spin": + return newVMClockSpinResumeWaiter() + case "vmgenid": + return newVMGenIDResumeWaiter() + default: + return nil, fmt.Errorf("unknown %s value %q", resumeNetworkSignalEnv, signal) + } +} + func newVMGenIDResumeWaiter() (*vmGenIDResumeWaiter, error) { f, err := os.Open("/dev/kmsg") if err != nil { @@ -181,6 +235,10 @@ func newVMGenIDResumeWaiter() (*vmGenIDResumeWaiter, error) { }, nil } +func (w *vmGenIDResumeWaiter) Name() string { + return "vmgenid" +} + func (w *vmGenIDResumeWaiter) Close() { if w == nil || w.file == nil { return @@ -199,3 +257,153 @@ func (w *vmGenIDResumeWaiter) Wait() error { } } } + +func newVMClockResumeWaiter() (*vmClockResumeWaiter, error) { + f, err := os.Open(vmClockDevicePath) + if err != nil { + return nil, fmt.Errorf("open %s: %w", vmClockDevicePath, err) + } + + generation, err := readVMClockGeneration(f) + if err != nil { + _ = f.Close() + return nil, err + } + + return &vmClockResumeWaiter{ + file: f, + generation: generation, + }, nil +} + +func (w *vmClockResumeWaiter) Name() string { + return "vmclock" +} + +func (w *vmClockResumeWaiter) Close() { + if w == nil || w.file == nil { + return + } + _ = w.file.Close() +} + +func (w *vmClockResumeWaiter) Wait() error { + for { + fds := []unix.PollFd{{ + Fd: int32(w.file.Fd()), + Events: unix.POLLIN, + }} + _, err := unix.Poll(fds, -1) + if err == unix.EINTR { + continue + } + if err != nil { + return fmt.Errorf("poll %s: %w", vmClockDevicePath, err) + } + if fds[0].Revents&unix.POLLHUP != 0 { + return fmt.Errorf("%s does not support notifications", vmClockDevicePath) + } + if fds[0].Revents&unix.POLLIN == 0 { + continue + } + + generation, err := readVMClockGeneration(w.file) + if err != nil { + return err + } + if generation != w.generation { + w.generation = generation + return nil + } + } +} + +func readVMClockGeneration(f *os.File) (uint64, error) { + generation, flags, err := readVMClockState(f) + if err != nil { + return 0, err + } + if flags&vmClockFlagNotificationPresent == 0 { + return 0, fmt.Errorf("%s missing notification support", vmClockDevicePath) + } + return generation, nil +} + +func newVMClockSpinResumeWaiter() (*vmClockSpinResumeWaiter, error) { + f, err := os.Open(vmClockDevicePath) + if err != nil { + return nil, fmt.Errorf("open %s: %w", vmClockDevicePath, err) + } + + generation, _, err := readVMClockState(f) + if err != nil { + _ = f.Close() + return nil, err + } + + data, err := unix.Mmap(int(f.Fd()), 0, vmClockABISize, unix.PROT_READ, unix.MAP_SHARED) + if err != nil { + _ = f.Close() + return nil, fmt.Errorf("mmap %s: %w", vmClockDevicePath, err) + } + + return &vmClockSpinResumeWaiter{ + file: f, + data: data, + generation: generation, + }, nil +} + +func (w *vmClockSpinResumeWaiter) Name() string { + return "vmclock-spin" +} + +func (w *vmClockSpinResumeWaiter) Close() { + if w == nil { + return + } + if w.data != nil { + _ = unix.Munmap(w.data) + w.data = nil + } + if w.file != nil { + _ = w.file.Close() + } +} + +func (w *vmClockSpinResumeWaiter) Wait() error { + if len(w.data) < vmClockGenerationCounterOffset+8 { + return fmt.Errorf("mmap %s: short mapping %d", vmClockDevicePath, len(w.data)) + } + ptr := (*uint64)(unsafe.Pointer(&w.data[vmClockGenerationCounterOffset])) + for { + generation := atomic.LoadUint64(ptr) + if generation != w.generation { + w.generation = generation + return nil + } + } +} + +func readVMClockState(f *os.File) (uint64, uint64, error) { + buf := make([]byte, vmClockABISize) + n, err := unix.Pread(int(f.Fd()), buf, 0) + if err != nil { + return 0, 0, fmt.Errorf("read %s: %w", vmClockDevicePath, err) + } + if n < vmClockABISize { + return 0, 0, fmt.Errorf("read %s: short read %d", vmClockDevicePath, n) + } + + magic := binary.LittleEndian.Uint32(buf[0:4]) + if magic != vmClockMagic { + return 0, 0, fmt.Errorf("read %s: invalid magic 0x%x", vmClockDevicePath, magic) + } + + flags := binary.LittleEndian.Uint64(buf[vmClockFlagsOffset : vmClockFlagsOffset+8]) + if flags&vmClockFlagGenerationCounterPresent == 0 { + return 0, 0, fmt.Errorf("%s missing generation counter support", vmClockDevicePath) + } + + return binary.LittleEndian.Uint64(buf[vmClockGenerationCounterOffset : vmClockGenerationCounterOffset+8]), flags, nil +} diff --git a/lib/system/versions.go b/lib/system/versions.go index 7743add0..8ff7a37f 100644 --- a/lib/system/versions.go +++ b/lib/system/versions.go @@ -23,14 +23,18 @@ const ( // Kernel_202605291 is the current kernel version with VMGenID support for snapshot resume detection Kernel_202605291 KernelVersion = "ch-6.12.8-kernel-3.0-202605291" + + // Kernel_202605301 is the current kernel version with VMClock generation counter notifications + Kernel_202605301 KernelVersion = "ch-6.16.9-kernel-0.1-202605301" ) var ( // DefaultKernelVersion is the kernel version used for new instances - DefaultKernelVersion = Kernel_202605291 + DefaultKernelVersion = Kernel_202605301 // SupportedKernelVersions lists all supported kernel versions SupportedKernelVersions = []KernelVersion{ + Kernel_202605301, Kernel_202605291, Kernel_202603301, Kernel_202603091, @@ -41,6 +45,10 @@ var ( // KernelDownloadURLs maps kernel versions and architectures to download URLs var KernelDownloadURLs = map[KernelVersion]map[string]string{ + Kernel_202605301: { + "x86_64": "https://github.com/kernel/linux/releases/download/ch-6.16.9-kernel-0.1-202605301/vmlinux-x86_64", + "aarch64": "https://github.com/kernel/linux/releases/download/ch-6.16.9-kernel-0.1-202605301/Image-arm64", + }, Kernel_202605291: { "x86_64": "https://github.com/kernel/linux/releases/download/ch-6.12.8-kernel-3.0-202605291/vmlinux-x86_64", "aarch64": "https://github.com/kernel/linux/releases/download/ch-6.12.8-kernel-3.0-202605291/Image-arm64", @@ -66,6 +74,10 @@ var KernelDownloadURLs = map[KernelVersion]map[string]string{ // KernelHeaderURLs maps kernel versions and architectures to kernel header tarball URLs // These tarballs contain kernel headers needed for DKMS to build out-of-tree modules (e.g., NVIDIA vGPU drivers) var KernelHeaderURLs = map[KernelVersion]map[string]string{ + Kernel_202605301: { + "x86_64": "https://github.com/kernel/linux/releases/download/ch-6.16.9-kernel-0.1-202605301/kernel-headers-x86_64.tar.gz", + "aarch64": "https://github.com/kernel/linux/releases/download/ch-6.16.9-kernel-0.1-202605301/kernel-headers-aarch64.tar.gz", + }, Kernel_202605291: { "x86_64": "https://github.com/kernel/linux/releases/download/ch-6.12.8-kernel-3.0-202605291/kernel-headers-x86_64.tar.gz", "aarch64": "https://github.com/kernel/linux/releases/download/ch-6.12.8-kernel-3.0-202605291/kernel-headers-aarch64.tar.gz",