Skip to content

Commit e51fca1

Browse files
committed
FEATURE: add paravirtualized clock support for guest time access
Hyperlight guests can now read time without expensive VM exits by using a paravirtualized clock shared between host and guest. This enables high-frequency timing operations like benchmarking, rate limiting, and timestamping with minimal overhead. Paravirtualized clocks work by having the hypervisor populate a shared memory page with clock calibration data. The guest reads this data along with the CPU's TSC to compute the current time entirely in userspace, avoiding the cost of a VM exit. Reference: https://docs.kernel.org/virt/kvm/x86/msr.html#pvclock The implementation uses the native mechanism for each hypervisor: - KVM: pvclock (MSR 0x4b564d01) - MSHV: Hyper-V Reference TSC page - WHP: Hyper-V Reference TSC page Guests have access to: - Monotonic time: nanoseconds since sandbox creation, guaranteed to never go backwards - Wall-clock time: UTC nanoseconds since Unix epoch - Local time: wall-clock adjusted for host timezone captured at sandbox creation Rust API (hyperlight_guest_bin::time): - SystemTime/Instant types mirroring std::time - DateTime type for human-readable date/time formatting - Weekday/Month enums with name() and short_name() methods C API (hyperlight_guest_capi): - POSIX-compatible: clock_gettime, gettimeofday, time - Broken-down time: gmtime_r, localtime_r, mktime, timegm - Formatting: strftime with common format specifiers The feature is gated behind `guest_time` (enabled by default) and documented in docs/guest-time.md. Note: The timezone offset is a snapshot from sandbox creation and does not update for DST transitions during the sandbox lifetime. Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
1 parent 5b702fc commit e51fca1

File tree

31 files changed

+3601
-27
lines changed

31 files changed

+3601
-27
lines changed

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ This project is composed internally of several components, depicted in the below
2727
* [Glossary](./glossary.md)
2828
* [How code gets executed in a VM](./hyperlight-execution-details.md)
2929
* [How to build a Hyperlight guest binary](./how-to-build-a-hyperlight-guest-binary.md)
30+
* [Guest Time API](./guest-time.md)
3031
* [Security considerations](./security.md)
3132
* [Technical requirements document](./technical-requirements-document.md)
3233

docs/guest-time.md

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
# Guest Time API
2+
3+
This document describes how to access time from within a Hyperlight guest. Hyperlight provides a paravirtualized clock that allows guests to read time without expensive VM exits.
4+
5+
## Overview
6+
7+
When a sandbox is created, Hyperlight configures a shared clock page between the host and guest. The guest can read time by accessing this shared page and the CPU's Time Stamp Counter (TSC), without requiring any VM exit or host call.
8+
9+
### Supported Hypervisors
10+
11+
- **KVM**: Uses KVM pvclock (MSR `0x4b564d01`)
12+
- **MSHV**: Uses Hyper-V Reference TSC page
13+
- **WHP** (Windows): Uses Hyper-V Reference TSC page
14+
15+
### Clock Types
16+
17+
- **Monotonic time**: Time since sandbox creation. Guaranteed to never go backwards. Use for measuring elapsed time.
18+
- **Wall-clock time**: UTC time since Unix epoch (1970-01-01 00:00:00 UTC). Can be used for timestamps.
19+
- **Local time**: Wall-clock time adjusted for the host's timezone offset (captured at sandbox creation).
20+
21+
## Feature Flag
22+
23+
The time functionality is controlled by the `guest_time` feature flag, which is enabled by default. To disable:
24+
25+
```toml
26+
[dependencies]
27+
hyperlight-guest = { version = "...", default-features = false }
28+
```
29+
30+
## Rust API
31+
32+
### High-Level API (`hyperlight_guest_bin::time`)
33+
34+
The recommended API for Rust guests mirrors `std::time`:
35+
36+
```rust
37+
use hyperlight_guest_bin::time::{SystemTime, Instant, UNIX_EPOCH};
38+
use core::time::Duration;
39+
40+
// Wall-clock time (like std::time::SystemTime)
41+
let now = SystemTime::now();
42+
let duration = now.duration_since(UNIX_EPOCH).unwrap();
43+
let unix_timestamp = duration.as_secs();
44+
45+
// Monotonic time for measuring elapsed time (like std::time::Instant)
46+
let start = Instant::now();
47+
// ... do work ...
48+
let elapsed = start.elapsed();
49+
50+
// Get timezone offset (seconds east of UTC)
51+
use hyperlight_guest_bin::time::utc_offset_seconds;
52+
if let Some(offset) = utc_offset_seconds() {
53+
// offset is seconds to add to UTC for local time
54+
// e.g., +3600 for UTC+1, -18000 for UTC-5
55+
}
56+
```
57+
58+
#### `SystemTime`
59+
60+
Represents wall-clock time (UTC). Methods:
61+
62+
- `SystemTime::now()` - Get current wall-clock time
63+
- `duration_since(earlier)` - Duration between two system times
64+
- `elapsed()` - Duration since this time was captured
65+
- `checked_add(duration)` / `checked_sub(duration)` - Arithmetic operations
66+
67+
#### `Instant`
68+
69+
Represents monotonic time for measuring durations. Methods:
70+
71+
- `Instant::now()` - Get current monotonic time
72+
- `duration_since(earlier)` - Duration between two instants
73+
- `elapsed()` - Duration since this instant was captured
74+
- Supports `+`, `-` operators with `Duration`
75+
- Supports `-` between two `Instant`s to get a `Duration`
76+
77+
#### `DateTime`
78+
79+
For formatting human-readable dates and times:
80+
81+
```rust
82+
use hyperlight_guest_bin::time::DateTime;
83+
84+
// Get current local time
85+
let dt = DateTime::now_local();
86+
87+
// Format: "Thursday 15th January 2026 15:34:56"
88+
let formatted = format!(
89+
"{} {} {} {} {:02}:{:02}:{:02}",
90+
dt.weekday().name(), // "Thursday"
91+
dt.day_ordinal(), // "15th"
92+
dt.month().name(), // "January"
93+
dt.year(), // 2026
94+
dt.hour(), // 15
95+
dt.minute(), // 34
96+
dt.second() // 56
97+
);
98+
```
99+
100+
Available methods on `DateTime`:
101+
102+
| Method | Returns | Description |
103+
|--------|---------|-------------|
104+
| `DateTime::now()` | `DateTime` | Current UTC time |
105+
| `DateTime::now_local()` | `DateTime` | Current local time |
106+
| `year()` | `i32` | Year (e.g., 2026) |
107+
| `month()` | `Month` | Month enum |
108+
| `month_number()` | `u8` | Month (1-12) |
109+
| `day()` | `u8` | Day of month (1-31) |
110+
| `hour()` | `u8` | Hour (0-23) |
111+
| `minute()` | `u8` | Minute (0-59) |
112+
| `second()` | `u8` | Second (0-59) |
113+
| `nanosecond()` | `u32` | Nanosecond |
114+
| `weekday()` | `Weekday` | Day of week enum |
115+
| `day_of_year()` | `u16` | Day of year (1-366) |
116+
| `day_ordinal()` | `&str` | Day with suffix ("15th") |
117+
| `hour12()` | `u8` | 12-hour format (1-12) |
118+
| `is_pm()` | `bool` | True if PM |
119+
| `am_pm()` | `&str` | "AM" or "PM" |
120+
121+
The `Weekday` and `Month` enums provide:
122+
- `name()` - Full name ("Thursday", "January")
123+
- `short_name()` - Abbreviated ("Thu", "Jan")
124+
125+
### Low-Level API (`hyperlight_guest::time`)
126+
127+
For cases where you need direct access or have a custom `GuestHandle`:
128+
129+
```rust
130+
use hyperlight_guest::time::{
131+
monotonic_time_ns,
132+
wall_clock_time_ns,
133+
is_clock_available,
134+
utc_offset_seconds,
135+
};
136+
137+
// Check availability
138+
if is_clock_available(handle) {
139+
// Get raw nanoseconds
140+
let mono_ns = monotonic_time_ns(handle).unwrap();
141+
let wall_ns = wall_clock_time_ns(handle).unwrap();
142+
let offset = utc_offset_seconds(handle).unwrap();
143+
}
144+
```
145+
146+
## C API
147+
148+
The C API provides POSIX-compatible functions:
149+
150+
### `gettimeofday`
151+
152+
```c
153+
#include "hyperlight_guest.h"
154+
155+
hl_timeval tv;
156+
hl_timezone tz;
157+
158+
// Get wall-clock time and timezone
159+
if (gettimeofday(&tv, &tz) == 0) {
160+
// tv.tv_sec is seconds since Unix epoch
161+
// tv.tv_usec is microseconds
162+
// tz.tz_minuteswest is minutes west of UTC
163+
}
164+
```
165+
166+
### `clock_gettime`
167+
168+
```c
169+
#include "hyperlight_guest.h"
170+
171+
hl_timespec ts;
172+
173+
// Wall-clock time (UTC)
174+
if (clock_gettime(hl_CLOCK_REALTIME, &ts) == 0) {
175+
// ts.tv_sec is seconds since Unix epoch
176+
// ts.tv_nsec is nanoseconds
177+
}
178+
179+
// Monotonic time (since sandbox creation)
180+
if (clock_gettime(hl_CLOCK_MONOTONIC, &ts) == 0) {
181+
// ts.tv_sec is seconds since sandbox started
182+
// ts.tv_nsec is nanoseconds
183+
}
184+
```
185+
186+
### `time`
187+
188+
```c
189+
#include "hyperlight_guest.h"
190+
191+
int64_t seconds = time(NULL); // Returns seconds since Unix epoch
192+
```
193+
194+
### Broken-Down Time (`struct tm`)
195+
196+
Convert timestamps to human-readable components:
197+
198+
```c
199+
#include "hyperlight_guest.h"
200+
201+
int64_t now = time(NULL);
202+
hl_tm tm_utc, tm_local;
203+
204+
// UTC time
205+
gmtime_r(&now, &tm_utc);
206+
207+
// Local time (using timezone captured at sandbox creation)
208+
localtime_r(&now, &tm_local);
209+
210+
// Access components
211+
int year = tm_local.tm_year + 1900; // Years since 1900
212+
int month = tm_local.tm_mon + 1; // 0-11, so add 1
213+
int day = tm_local.tm_mday; // 1-31
214+
int hour = tm_local.tm_hour; // 0-23
215+
int minute = tm_local.tm_min; // 0-59
216+
int second = tm_local.tm_sec; // 0-59
217+
int weekday = tm_local.tm_wday; // 0=Sunday, 6=Saturday
218+
int yearday = tm_local.tm_yday; // 0-365
219+
```
220+
221+
### `strftime` - Format Time as String
222+
223+
```c
224+
#include "hyperlight_guest.h"
225+
226+
int64_t now = time(NULL);
227+
hl_tm tm_local;
228+
localtime_r(&now, &tm_local);
229+
230+
char buf[128];
231+
size_t len = strftime((uint8_t*)buf, sizeof(buf),
232+
(const uint8_t*)"%A %d %B %Y %H:%M:%S",
233+
&tm_local);
234+
// buf = "Thursday 15 January 2026 15:34:56"
235+
```
236+
237+
#### Supported Format Specifiers
238+
239+
| Specifier | Description | Example |
240+
|-----------|-------------|---------|
241+
| `%a` | Abbreviated weekday | "Thu" |
242+
| `%A` | Full weekday | "Thursday" |
243+
| `%b`, `%h` | Abbreviated month | "Jan" |
244+
| `%B` | Full month | "January" |
245+
| `%d` | Day of month (01-31) | "15" |
246+
| `%e` | Day of month, space-padded | " 5" |
247+
| `%H` | Hour 24h (00-23) | "15" |
248+
| `%I` | Hour 12h (01-12) | "03" |
249+
| `%j` | Day of year (001-366) | "015" |
250+
| `%m` | Month (01-12) | "01" |
251+
| `%M` | Minute (00-59) | "34" |
252+
| `%p` | AM/PM | "PM" |
253+
| `%P` | am/pm | "pm" |
254+
| `%S` | Second (00-59) | "56" |
255+
| `%u` | Weekday (1-7, Mon=1) | "4" |
256+
| `%w` | Weekday (0-6, Sun=0) | "4" |
257+
| `%y` | Year without century | "26" |
258+
| `%Y` | Year with century | "2026" |
259+
| `%z` | Timezone offset | "+0100" |
260+
| `%Z` | Timezone name | "UTC" or "LOCAL" |
261+
| `%%` | Literal % | "%" |
262+
| `%n` | Newline | "\n" |
263+
| `%t` | Tab | "\t" |
264+
265+
### `mktime` / `timegm` - Convert to Timestamp
266+
267+
```c
268+
hl_tm tm = {
269+
.tm_year = 2026 - 1900, // Years since 1900
270+
.tm_mon = 0, // January (0-11)
271+
.tm_mday = 15, // Day of month
272+
.tm_hour = 15,
273+
.tm_min = 34,
274+
.tm_sec = 56
275+
};
276+
277+
// From local time to UTC timestamp
278+
int64_t local_ts = mktime(&tm);
279+
280+
// From UTC time to UTC timestamp
281+
int64_t utc_ts = timegm(&tm);
282+
```
283+
284+
### Supported Clock IDs
285+
286+
| Clock ID | Description |
287+
|----------|-------------|
288+
| `hl_CLOCK_REALTIME` | Wall-clock time (UTC) |
289+
| `hl_CLOCK_REALTIME_COARSE` | Same as `CLOCK_REALTIME` |
290+
| `hl_CLOCK_MONOTONIC` | Time since sandbox creation |
291+
| `hl_CLOCK_MONOTONIC_COARSE` | Same as `CLOCK_MONOTONIC` |
292+
| `hl_CLOCK_BOOTTIME` | Same as `CLOCK_MONOTONIC` |
293+
294+
Note: `CLOCK_PROCESS_CPUTIME_ID` and `CLOCK_THREAD_CPUTIME_ID` are not supported.
295+
296+
## Timezone Handling
297+
298+
The host's timezone offset is captured when the sandbox is created and stored in the clock region. This allows guests to compute local time without additional host calls.
299+
300+
> **⚠️ Limitation: Static Timezone Offset**
301+
>
302+
> The timezone offset is a snapshot from sandbox creation time. It does **not** update
303+
> if the host's timezone changes during the sandbox lifetime. This means:
304+
>
305+
> - **DST transitions are not reflected**: If a sandbox is created before a DST change
306+
> and continues running after, local time will be off by one hour.
307+
> - **Manual timezone changes are not reflected**: If the host's timezone is changed
308+
> while the sandbox is running, the guest will still use the original offset.
309+
>
310+
> For applications where accurate local time across DST boundaries is critical,
311+
> consider using UTC time and handling timezone conversion on the host side.
312+
313+
```rust
314+
// Rust
315+
use hyperlight_guest_bin::time::{utc_offset_seconds, local_time_ns};
316+
317+
let offset = utc_offset_seconds().unwrap(); // Seconds east of UTC
318+
let local_ns = local_time_ns().unwrap(); // Local time in nanoseconds
319+
```
320+
321+
```c
322+
// C - use gettimeofday with timezone
323+
hl_timeval tv;
324+
hl_timezone tz;
325+
gettimeofday(&tv, &tz);
326+
int offset_seconds = -(tz.tz_minuteswest * 60); // Convert to seconds east
327+
```
328+
329+
## Performance
330+
331+
Reading time via the paravirtualized clock is very fast because:
332+
333+
1. No VM exit is required
334+
2. The clock page is in shared memory accessible to the guest
335+
3. Only a few memory reads and TSC reads are needed
336+
337+
This makes it suitable for high-frequency timing operations like benchmarking or rate limiting.
338+
339+
## Error Handling
340+
341+
Time functions return `None` (Rust) or `-1` (C) if:
342+
343+
- The clock is not available (hypervisor doesn't support pvclock)
344+
- The clock data is being updated (rare, retry will succeed)
345+
346+
For the high-level Rust API, `SystemTime::now()` and `Instant::now()` return a zero time if the clock is unavailable, rather than panicking.

docs/how-to-build-a-hyperlight-guest-binary.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,22 @@ the guest to:
2323
- register functions that can be called by the host application
2424
- call host functions that have been registered by the host.
2525

26+
### Available Features
27+
28+
- **`guest_time`** (enabled by default): Provides time-related functionality via a paravirtualized clock.
29+
This includes `SystemTime`, `Instant`, and `UNIX_EPOCH` in `hyperlight_guest_bin::time` that mirror
30+
the `std::time` API. See [Guest Time API](./guest-time.md) for details.
31+
2632
## C guest binary
2733

2834
For the binary written in C, the generated C bindings can be downloaded from the
2935
latest release page that contain: the `hyperlight_guest.h` header and the
3036
C API library.
3137
The `hyperlight_guest.h` header contains the corresponding APIs to register
3238
guest functions and call host functions from within the guest.
39+
40+
### Available Features
41+
42+
When built with the `guest_time` feature (enabled by default), the C API provides
43+
POSIX-compatible time functions: `gettimeofday()`, `clock_gettime()`, and `time()`.
44+
See [Guest Time API](./guest-time.md) for details.

src/hyperlight_common/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ spin = "0.10.0"
2424
thiserror = { version = "2.0.16", default-features = false }
2525

2626
[features]
27-
default = ["tracing"]
27+
default = ["tracing", "guest_time"]
2828
tracing = ["dep:tracing"]
2929
fuzzing = ["dep:arbitrary"]
3030
trace_guest = []
3131
mem_profile = []
3232
std = ["thiserror/std", "log/std", "tracing/std"]
3333
init-paging = []
34+
# Enable paravirtualized clock support for guest time functions
35+
guest_time = []
3436

3537
[lib]
3638
bench = false # see https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options

0 commit comments

Comments
 (0)