Skip to content

Commit 81dbeb2

Browse files
committed
vmclock: test snapshot safety features
Extend VMClock integration tests to also account for the vm_generation_counter field and notification support flag. Signed-off-by: Babis Chalios <bchalios@amazon.es>
1 parent aa53b46 commit 81dbeb2

File tree

4 files changed

+235
-27
lines changed

4 files changed

+235
-27
lines changed

tests/conftest.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,13 @@ def bin_vsock_path(test_fc_session_root_path):
236236
build_tools.gcc_compile("host_tools/vsock_helper.c", vsock_helper_bin_path)
237237
yield vsock_helper_bin_path
238238

239+
@pytest.fixture(scope="session")
240+
def bin_vmclock_path(test_fc_session_root_path):
241+
"""Build a simple util for test VMclock device"""
242+
vmclock_helper_bin_path = os.path.join(test_fc_session_root_path, "vmclock")
243+
build_tools.gcc_compile("host_tools/vmclock.c", vmclock_helper_bin_path)
244+
yield vmclock_helper_bin_path
245+
239246

240247
@pytest.fixture(scope="session")
241248
def bin_vmclock_path(test_fc_session_root_path):

tests/host_tools/vmclock-abi.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,17 @@ struct vmclock_abi {
115115
* bit again after the update, using the about-to-be-valid fields.
116116
*/
117117
#define VMCLOCK_FLAG_TIME_MONOTONIC (1 << 7)
118+
/*
119+
* If the VM_GEN_COUNTER_PRESENT flag is set, the hypervisor will
120+
* bump the vm_generation_counter field every time the guest is
121+
* loaded from some save state (restored from a snapshot).
122+
*/
123+
#define VMCLOCK_FLAG_VM_GEN_COUNTER_PRESENT (1 << 8)
124+
/*
125+
* If the NOTIFICATION_PRESENT flag is set, the hypervisor will send
126+
* a notification every time it updates seq_count to a new even number.
127+
*/
128+
#define VMCLOCK_FLAG_NOTIFICATION_PRESENT (1 << 9)
118129

119130
__u8 pad[2];
120131
__u8 clock_status;
@@ -177,6 +188,19 @@ struct vmclock_abi {
177188
__le64 time_frac_sec; /* Units of 1/2^64 of a second */
178189
__le64 time_esterror_nanosec;
179190
__le64 time_maxerror_nanosec;
191+
192+
/*
193+
* This field changes to another non-repeating value when the VM
194+
* is loaded from a snapshot. This event, typically, represents a
195+
* "jump" forward in time. As a result, in this case as well, the
196+
* guest needs to discard any calibrarion against external sources.
197+
* Loading a snapshot in a VM has different semantics than other VM
198+
* events such as live migration, i.e. apart from re-adjusting guest
199+
* clocks a guest user space might want to discard UUIDs, reset
200+
* network connections or reseed entropy, etc. As a result, we
201+
* use a dedicated marker for such events.
202+
*/
203+
__le64 vm_generation_counter;
180204
};
181205

182206
#endif /* __VMCLOCK_ABI_H__ */

tests/host_tools/vmclock.c

Lines changed: 134 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
#include <errno.h>
54
#include <stdatomic.h>
65
#include <stdint.h>
76
#include <stdio.h>
87
#include <stdlib.h>
98
#include <string.h>
9+
#include <sys/epoll.h>
1010
#include <sys/stat.h>
1111
#include <fcntl.h>
1212
#include <sys/mman.h>
@@ -16,23 +16,26 @@
1616

1717
const char *VMCLOCK_DEV_PATH = "/dev/vmclock0";
1818

19-
int get_vmclock_handle(struct vmclock_abi **vmclock)
19+
int open_vmclock(void)
2020
{
2121
int fd = open(VMCLOCK_DEV_PATH, 0);
22-
if (fd == -1)
23-
goto out_err;
22+
if (fd == -1) {
23+
perror("open");
24+
exit(1);
25+
}
2426

25-
void *ptr = mmap(NULL, sizeof(struct vmclock_abi), PROT_READ, MAP_SHARED, fd, 0);
26-
if (ptr == MAP_FAILED)
27-
goto out_err_mmap;
27+
return fd;
28+
}
2829

29-
*vmclock = ptr;
30-
return 0;
30+
struct vmclock_abi *get_vmclock_handle(int fd)
31+
{
32+
void *ptr = mmap(NULL, sizeof(struct vmclock_abi), PROT_READ, MAP_SHARED, fd, 0);
33+
if (ptr == MAP_FAILED) {
34+
perror("mmap");
35+
exit(1);
36+
}
3137

32-
out_err_mmap:
33-
close(fd);
34-
out_err:
35-
return errno;
38+
return ptr;
3639
}
3740

3841
#define READ_VMCLOCK_FIELD_FN(type, field) \
@@ -56,23 +59,136 @@ type read##_##field (struct vmclock_abi *vmclock) { \
5659
}
5760

5861
READ_VMCLOCK_FIELD_FN(uint64_t, disruption_marker);
62+
READ_VMCLOCK_FIELD_FN(uint64_t, vm_generation_counter);
5963

60-
int main()
64+
/*
65+
* Read `vmclock_abi` structure using a file descriptor pointing to
66+
* `/dev/vmclock0`.
67+
*/
68+
void read_vmclock(int fd, struct vmclock_abi *vmclock)
6169
{
62-
struct vmclock_abi *vmclock;
70+
int ret;
6371

64-
int err = get_vmclock_handle(&vmclock);
65-
if (err) {
66-
printf("Could not mmap vmclock struct: %s\n", strerror(err));
72+
/*
73+
* Use `pread()`, since the device doesn't implement lseek(), so
74+
* we can't reset `fp`.
75+
*/
76+
ret = pread(fd, vmclock, sizeof(*vmclock), 0);
77+
if (ret < 0) {
78+
perror("read");
79+
exit(1);
80+
} else if (ret < (int) sizeof(*vmclock)) {
81+
fprintf(stderr, "We don't handle partial writes (%d). Exiting!\n", ret);
6782
exit(1);
6883
}
84+
}
85+
86+
void print_vmclock(struct vmclock_abi *vmclock)
87+
{
88+
if (vmclock->flags & VMCLOCK_FLAG_VM_GEN_COUNTER_PRESENT) {
89+
printf("VMCLOCK_FLAG_VM_GEN_COUNTER_PRESENT: true\n");
90+
} else {
91+
printf("VMCLOCK_FLAG_VM_GEN_COUNTER_PRESENT: false\n");
92+
}
93+
94+
if (vmclock->flags & VMCLOCK_FLAG_NOTIFICATION_PRESENT) {
95+
printf("VMCLOCK_FLAG_NOTIFICATION_PRESENT: true\n");
96+
} else {
97+
printf("VMCLOCK_FLAG_NOTIFICATION_PRESENT: false\n");
98+
}
6999

70100
printf("VMCLOCK_MAGIC: 0x%x\n", vmclock->magic);
71101
printf("VMCLOCK_SIZE: 0x%x\n", vmclock->size);
72102
printf("VMCLOCK_VERSION: %u\n", vmclock->version);
73103
printf("VMCLOCK_CLOCK_STATUS: %u\n", vmclock->clock_status);
74104
printf("VMCLOCK_COUNTER_ID: %u\n", vmclock->counter_id);
75105
printf("VMCLOCK_DISRUPTION_MARKER: %lu\n", read_disruption_marker(vmclock));
106+
printf("VMCLOCK_VM_GENERATION_COUNTER: %lu\n", read_vm_generation_counter(vmclock));
107+
fflush(stdout);
108+
}
109+
110+
void run_poll(int fd)
111+
{
112+
struct vmclock_abi vmclock;
113+
int epfd, ret, nfds;
114+
struct epoll_event ev;
115+
116+
read_vmclock(fd, &vmclock);
117+
print_vmclock(&vmclock);
118+
119+
epfd = epoll_create(1);
120+
if (epfd < 0) {
121+
perror("epoll_create");
122+
exit(1);
123+
}
124+
125+
ev.events = EPOLLIN | EPOLLRDNORM;
126+
ev.data.fd = fd;
127+
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
128+
if (ret < 0) {
129+
perror("epoll_add");
130+
exit(1);
131+
}
132+
133+
while (1) {
134+
nfds = epoll_wait(epfd, &ev, 1, -1);
135+
if (nfds < 0) {
136+
perror("epoll_wait");
137+
exit(1);
138+
}
139+
140+
if (ev.data.fd != fd) {
141+
fprintf(stderr, "Unknown file descriptor %d\n", ev.data.fd);
142+
exit(1);
143+
}
144+
145+
if (ev.events & EPOLLHUP) {
146+
fprintf(stderr, "Device does not support notifications. Stop polling\n");
147+
exit(1);
148+
} else if (ev.events & EPOLLIN) {
149+
fprintf(stdout, "Got VMClock notification\n");
150+
read_vmclock(fd, &vmclock);
151+
print_vmclock(&vmclock);
152+
}
153+
}
154+
}
155+
156+
void print_help_message()
157+
{
158+
fprintf(stderr, "usage: vmclock MODE\n");
159+
fprintf(stderr, "Available modes:\n");
160+
fprintf(stderr, " -r\tRead vmclock_abi using read()\n");
161+
fprintf(stderr, " -m\tRead vmclock_abi using mmap()\n");
162+
fprintf(stderr, " -p\tPoll VMClock for changes\n");
163+
}
164+
165+
int main(int argc, char *argv[])
166+
{
167+
int fd;
168+
struct vmclock_abi vmclock, *vmclock_ptr;
169+
170+
if (argc != 2) {
171+
print_help_message();
172+
exit(1);
173+
}
174+
175+
fd = open_vmclock();
176+
177+
if (!strncmp(argv[1], "-r", 3)) {
178+
printf("Reading VMClock with read()\n");
179+
read_vmclock(fd, &vmclock);
180+
print_vmclock(&vmclock);
181+
} else if (!strncmp(argv[1], "-m", 3)) {
182+
printf("Reading VMClock with mmap()\n");
183+
vmclock_ptr = get_vmclock_handle(fd);
184+
print_vmclock(vmclock_ptr);
185+
} else if (!strncmp(argv[1], "-p", 3)) {
186+
printf("Polling VMClock\n");
187+
run_poll(fd);
188+
} else {
189+
print_help_message();
190+
exit(1);
191+
}
76192

77193
return 0;
78194
}

tests/integration_tests/functional/test_vmclock.py

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,46 +21,107 @@ def vm_with_vmclock(uvm_plain_acpi, bin_vmclock_path):
2121
yield basevm
2222

2323

24-
def parse_vmclock(vm):
24+
def parse_vmclock(vm, use_mmap=False):
2525
"""Parse the VMclock struct inside the guest and return a dictionary with its fields"""
26-
_, stdout, _ = vm.ssh.check_output("/tmp/vmclock")
26+
27+
cmd = "/tmp/vmclock -m" if use_mmap else "/tmp/vmclock -r"
28+
_, stdout, _ = vm.ssh.check_output(cmd)
29+
fields = stdout.strip().split("\n")
30+
if use_mmap:
31+
assert fields[0] == "Reading VMClock with mmap()"
32+
else:
33+
assert fields[0] == "Reading VMClock with read()"
34+
35+
return dict(item.split(": ") for item in fields if item.startswith("VMCLOCK"))
36+
37+
38+
def parse_vmclock_from_poll(vm, expected_notifications):
39+
"""Parse the output of the 'vmclock -p' command in the guest"""
40+
41+
_, stdout, _ = vm.ssh.check_output("cat /tmp/vmclock.out")
2742
fields = stdout.strip().split("\n")
28-
return dict(item.split(": ") for item in fields)
43+
44+
nr_notifications = 0
45+
for line in fields:
46+
if line == "Got VMClock notification":
47+
nr_notifications += 1
48+
49+
# assert nr_notifications == expected_notifications
50+
return dict(item.split(": ") for item in fields if item.startswith("VMCLOCK"))
2951

3052

3153
@pytest.mark.skipif(
3254
platform.machine() != "x86_64",
3355
reason="VMClock device is currently supported only on x86 systems",
3456
)
35-
def test_vmclock_fields(vm_with_vmclock):
57+
@pytest.mark.parametrize("use_mmap", [False, True], ids=["read()", "mmap()"])
58+
def test_vmclock_read_fields(vm_with_vmclock, use_mmap):
3659
"""Make sure that we expose the expected values in the VMclock struct"""
3760
vm = vm_with_vmclock
38-
vmclock = parse_vmclock(vm)
61+
vmclock = parse_vmclock(vm, use_mmap)
3962

63+
assert vmclock["VMCLOCK_FLAG_VM_GEN_COUNTER_PRESENT"] == "true"
64+
assert vmclock["VMCLOCK_FLAG_NOTIFICATION_PRESENT"] == "true"
4065
assert vmclock["VMCLOCK_MAGIC"] == "0x4b4c4356"
4166
assert vmclock["VMCLOCK_SIZE"] == "0x1000"
4267
assert vmclock["VMCLOCK_VERSION"] == "1"
4368
assert vmclock["VMCLOCK_CLOCK_STATUS"] == "0"
4469
assert vmclock["VMCLOCK_COUNTER_ID"] == "255"
4570
assert vmclock["VMCLOCK_DISRUPTION_MARKER"] == "0"
71+
assert vmclock["VMCLOCK_VM_GENERATION_COUNTER"] == "0"
4672

4773

4874
@pytest.mark.skipif(
4975
platform.machine() != "x86_64",
5076
reason="VMClock device is currently supported only on x86 systems",
5177
)
52-
def test_snapshot_update(vm_with_vmclock, microvm_factory, snapshot_type):
53-
"""Test that `disruption_marker` is updated upon snapshot resume"""
78+
@pytest.mark.parametrize("use_mmap", [False, True], ids=["read()", "mmap()"])
79+
def test_snapshot_update(vm_with_vmclock, microvm_factory, snapshot_type, use_mmap):
80+
"""Test that `disruption_marker` and `vm_generation_counter` are updated
81+
upon snapshot resume"""
5482
basevm = vm_with_vmclock
5583

56-
vmclock = parse_vmclock(basevm)
84+
vmclock = parse_vmclock(basevm, use_mmap)
85+
assert vmclock["VMCLOCK_FLAG_VM_GEN_COUNTER_PRESENT"] == "true"
86+
assert vmclock["VMCLOCK_FLAG_NOTIFICATION_PRESENT"] == "true"
87+
assert vmclock["VMCLOCK_DISRUPTION_MARKER"] == "0"
88+
assert vmclock["VMCLOCK_VM_GENERATION_COUNTER"] == "0"
89+
90+
snapshot = basevm.make_snapshot(snapshot_type)
91+
basevm.kill()
92+
93+
for i, vm in enumerate(
94+
microvm_factory.build_n_from_snapshot(snapshot, 5, incremental=True)
95+
):
96+
vmclock = parse_vmclock(vm, use_mmap)
97+
assert vmclock["VMCLOCK_DISRUPTION_MARKER"] == f"{i+1}"
98+
assert vmclock["VMCLOCK_VM_GENERATION_COUNTER"] == f"{i+1}"
99+
100+
101+
# TODO: remove this skip when we backport VMClock snapshot safety patches to 5.10 and 6.1
102+
@pytest.mark.skip(
103+
reason="Skip until we get guest microVM kernels with support for the notification mechanism",
104+
)
105+
def test_vmclock_notifications(vm_with_vmclock, microvm_factory, snapshot_type):
106+
"""Test that Firecracker will send a notification on snapshot load"""
107+
basevm = vm_with_vmclock
108+
109+
# Launch vmclock utility in polling mode
110+
basevm.ssh.check_output("/tmp/vmclock -p > /tmp/vmclock.out 2>&1 &")
111+
112+
# We should not have received any notification yet
113+
vmclock = parse_vmclock_from_poll(basevm, 0)
114+
assert vmclock["VMCLOCK_FLAG_VM_GEN_COUNTER_PRESENT"] == "true"
115+
assert vmclock["VMCLOCK_FLAG_NOTIFICATION_PRESENT"] == "true"
57116
assert vmclock["VMCLOCK_DISRUPTION_MARKER"] == "0"
117+
assert vmclock["VMCLOCK_VM_GENERATION_COUNTER"] == "0"
58118

59119
snapshot = basevm.make_snapshot(snapshot_type)
60120
basevm.kill()
61121

62122
for i, vm in enumerate(
63123
microvm_factory.build_n_from_snapshot(snapshot, 5, incremental=True)
64124
):
65-
vmclock = parse_vmclock(vm)
125+
vmclock = parse_vmclock_from_poll(vm, i + 1)
66126
assert vmclock["VMCLOCK_DISRUPTION_MARKER"] == f"{i+1}"
127+
assert vmclock["VMCLOCK_VM_GENERATION_COUNTER"] == f"{i+1}"

0 commit comments

Comments
 (0)