Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

When using Apple Virtualization notify guest OS of wake up events #7031

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

kjmph
Copy link

@kjmph kjmph commented Feb 23, 2025

Open a Virtio Socket for the guest OS to receive a wakeup command if it connects to the host CID. This could be the basis for more of a command structure, like the QEMU guest agent. We are only using this to send a wakeup command, however.

This is predominantly useful for the guest OS to resync time against the hardware clock. Otherwise, using network time syncs relies on a timer, which can take between 5 - 30 minutes before its next scheduled sync. Also, network time syncs can sometimes fail with too much drift to resync the local system clock. Yet, while ntp has a skew of 1000s before failing, modern systemd Linux distros have switched to system-timesyncd.service, so the author is unsure what max drift is with this package.

-- Additional Notes --

Note, this caused problems with my VM, as occasionally TLS certs failed when they had been renewed (they had a future validity date). It also caused problems with SSH connections being slow to resync, and not responsive when waking up. Once, I witnessed the VM spinning at 100% CPU after sleeping for 2 weeks, but I'm uncertain if this is related without more debugging.

For this PR to function, it required me to run on port 43218. I couldn't get other ports to call the listener delegate when the client attempted to connect. I got this port number from the vz project, as it has a test that worked. That explains some of the oddity with this PR where I tried really hard to store strong references to the delegate, as I thought it was a scoping issue preventing the delegate from being called.

For other users of UTM, here is the Go program I am using on the client to resync the HW clock on wakeup. I modified this from the host-timesync-daemon in LinuxKit. I modified their program to connect and read from the socket, as well as matching hwclock's method for reading the rtc clock until it is modified (note, I didn't add a timeout to gracefully exit if there was a delay reading the rtc).

Also, this program either needs to run as root, have a setuid bit set, or the user needs proper permissions to read/write /dev/rtc0. If anyone is very interested, we could package this somewhere as an Apple/UTM virtualization agent.

package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"syscall"
	"time"
	"unsafe"

	"github.com/linuxkit/virtsock/pkg/vsock"
)

// Listen for connections on an AF_VSOCK address and update the system time
// from the hardware clock when a connection is received.

// From <linux/rtc.h> struct rtc_time
type rtcTime struct {
	tmSec   uint32
	tmMin   uint32
	tmHour  uint32
	tmMday  uint32
	tmMon   uint32
	tmYear  uint32
	tmWday  uint32
	tmYday  uint32
	tmIsdst uint32
}

const (
	// iocREAD and friends are from <linux/asm-generic/ioctl.h>
	iocREAD      = uintptr(2)
	iocNRBITS    = uintptr(8)
	iocNRSHIFT   = uintptr(0)
	iocTYPEBITS  = uintptr(8)
	iocTYPESHIFT = iocNRSHIFT + iocNRBITS
	iocSIZEBITS  = uintptr(14)
	iocSIZESHIFT = iocTYPESHIFT + iocTYPEBITS
	iocDIRSHIFT  = iocSIZESHIFT + iocSIZEBITS
	// rtcRDTIMENR and friends are from <linux/rtc.h>
	rtcRDTIMENR   = uintptr(0x09)
	rtcRDTIMETYPE = uintptr(112)
)

func rtcReadTime() rtcTime {
	f, err := os.Open("/dev/rtc0")
	if err != nil {
		log.Fatalf("Failed to open /dev/rtc0: %v", err)
	}
	defer f.Close()
	result := rtcTime{}
	arg := uintptr(0)
	arg |= (iocREAD << iocDIRSHIFT)
	arg |= (rtcRDTIMETYPE << iocTYPESHIFT)
	arg |= (rtcRDTIMENR << iocNRSHIFT)
	arg |= (unsafe.Sizeof(result) << iocSIZESHIFT)
	_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), arg, uintptr(unsafe.Pointer(&result)))
	if errno != 0 {
		log.Fatalf("RTC_RD_TIME failed: %v", errno)
	}
	return result
}

func main() {
	// host-timesync-daemon -cid <cid> -port <port>

	cid := flag.Int("cid", 0, "AF_VSOCK CID to listen on")
	port := flag.Int("port", 0, "AF_VSOCK port to listen on")
	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, "%s: set the time after an AH_VSOCK wakeup command is received.\n\n", os.Args[0])
		fmt.Fprintf(os.Stderr, "Example usage:\n")
		fmt.Fprintf(os.Stderr, "%s -port 43218\n", os.Args[0])
		fmt.Fprintf(os.Stderr, "   -- when a wakeup command is received on port 0xf3a4, query the hardware\n")
		fmt.Fprintf(os.Stderr, "      clock and set the system time. The connection will remain open for future\n")
		fmt.Fprintf(os.Stderr, "      wakeup commands.\n\n")
		fmt.Fprintf(os.Stderr, "Arguments:\n")
		flag.PrintDefaults()
	}

	flag.Parse()
	if *port == 0 {
		log.Fatalf("Please supply a -port argument")
	}
	if *cid == 0 {
		// by default connect to the host CID, 2
		*cid = 2
	}

	conn, err := vsock.Dial(2, uint32(*port))
	if err != nil {
		log.Fatalf("Failed to connect to host on port %d: %s", *port, err)
	}
	log.Printf("Connected to host on port %d", *port)
	defer conn.Close()

	buf := make([]byte, 1024)

	for {
		n, err := conn.Read(buf)
		if err != nil {
			log.Printf("Error reading from connection: %s", err)
			// Maybe reconnect here if needed
			break
		}

		// Convert bytes to string and check for "wakeup"
		msg := string(buf[:n])
		if msg == "wakeup" {
			log.Printf("Received wakeup command")

			t := rtcReadTime()
			for {
				r := rtcReadTime()
				if t.tmSec != r.tmSec {
					t = r
					break
				}
			}

			d := time.Date(int(t.tmYear+1900), time.Month(t.tmMon+1), int(t.tmMday), int(t.tmHour), int(t.tmMin), int(t.tmSec), 0, time.UTC)
			log.Printf("Setting system clock to %s", d)
			tv := syscall.Timeval{
				Sec:  d.Unix(),
				Usec: 0, // the RTC only has second granularity
			}
			if err := syscall.Settimeofday(&tv); err != nil {
				log.Printf("Unexpected failure from Settimeofday: %v", err)
			}
		}
	}
}

Open a Virtio Socket for the guest OS to receive a wakeup command if
it connects. This could be the basis for more of a command structure,
like the QEMU guest agent. We are only using this to send a wakeup
command, however.

This is predominantly useful for the guest OS to resync time against
the hardware clock. Otherwise, using network time syncs relies on a
timer, which can take betweem 5 - 30 minutes before its next scheduled
sync. Also, network time syncs can sometimes fail with too much drift
to resync the local system clock. Yet, while ntp has a skew of 1000s
before failing, modern systemd Linux distros have switched to
system-timesyncd.service, so the author is unsure what max drift is
with this package.
@kjmph
Copy link
Author

kjmph commented Feb 23, 2025

@osy, please let me know if I can provide any more details, or assist with next steps. I'd love for this to be readily available for people, as I saw a number of issues related to time desync, and probably people are running with Apple Virtualization, like I am.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant