|
| 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. |
0 commit comments