diff --git a/docs/README.md b/docs/README.md index 3b36c5db1..69d81e6c7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,6 +27,7 @@ This project is composed internally of several components, depicted in the below * [Glossary](./glossary.md) * [How code gets executed in a VM](./hyperlight-execution-details.md) * [How to build a Hyperlight guest binary](./how-to-build-a-hyperlight-guest-binary.md) +* [Guest Time API](./guest-time.md) * [Security considerations](./security.md) * [Technical requirements document](./technical-requirements-document.md) diff --git a/docs/guest-time.md b/docs/guest-time.md new file mode 100644 index 000000000..52040ee5e --- /dev/null +++ b/docs/guest-time.md @@ -0,0 +1,346 @@ +# Guest Time API + +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. + +## Overview + +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. + +### Supported Hypervisors + +- **KVM**: Uses KVM pvclock (MSR `0x4b564d01`) +- **MSHV**: Uses Hyper-V Reference TSC page +- **WHP** (Windows): Uses Hyper-V Reference TSC page + +### Clock Types + +- **Monotonic time**: Time since sandbox creation. Guaranteed to never go backwards. Use for measuring elapsed time. +- **Wall-clock time**: UTC time since Unix epoch (1970-01-01 00:00:00 UTC). Can be used for timestamps. +- **Local time**: Wall-clock time adjusted for the host's timezone offset (captured at sandbox creation). + +## Feature Flag + +The time functionality is controlled by the `guest_time` feature flag, which is enabled by default. To disable: + +```toml +[dependencies] +hyperlight-guest = { version = "...", default-features = false } +``` + +## Rust API + +### High-Level API (`hyperlight_guest_bin::time`) + +The recommended API for Rust guests mirrors `std::time`: + +```rust +use hyperlight_guest_bin::time::{SystemTime, Instant, UNIX_EPOCH}; +use core::time::Duration; + +// Wall-clock time (like std::time::SystemTime) +let now = SystemTime::now(); +let duration = now.duration_since(UNIX_EPOCH).unwrap(); +let unix_timestamp = duration.as_secs(); + +// Monotonic time for measuring elapsed time (like std::time::Instant) +let start = Instant::now(); +// ... do work ... +let elapsed = start.elapsed(); + +// Get timezone offset (seconds east of UTC) +use hyperlight_guest_bin::time::utc_offset_seconds; +if let Some(offset) = utc_offset_seconds() { + // offset is seconds to add to UTC for local time + // e.g., +3600 for UTC+1, -18000 for UTC-5 +} +``` + +#### `SystemTime` + +Represents wall-clock time (UTC). Methods: + +- `SystemTime::now()` - Get current wall-clock time +- `duration_since(earlier)` - Duration between two system times +- `elapsed()` - Duration since this time was captured +- `checked_add(duration)` / `checked_sub(duration)` - Arithmetic operations + +#### `Instant` + +Represents monotonic time for measuring durations. Methods: + +- `Instant::now()` - Get current monotonic time +- `duration_since(earlier)` - Duration between two instants +- `elapsed()` - Duration since this instant was captured +- Supports `+`, `-` operators with `Duration` +- Supports `-` between two `Instant`s to get a `Duration` + +#### `DateTime` + +For formatting human-readable dates and times: + +```rust +use hyperlight_guest_bin::time::DateTime; + +// Get current local time +let dt = DateTime::now_local(); + +// Format: "Thursday 15th January 2026 15:34:56" +let formatted = format!( + "{} {} {} {} {:02}:{:02}:{:02}", + dt.weekday().name(), // "Thursday" + dt.day_ordinal(), // "15th" + dt.month().name(), // "January" + dt.year(), // 2026 + dt.hour(), // 15 + dt.minute(), // 34 + dt.second() // 56 +); +``` + +Available methods on `DateTime`: + +| Method | Returns | Description | +|--------|---------|-------------| +| `DateTime::now()` | `DateTime` | Current UTC time | +| `DateTime::now_local()` | `DateTime` | Current local time | +| `year()` | `i32` | Year (e.g., 2026) | +| `month()` | `Month` | Month enum | +| `month_number()` | `u8` | Month (1-12) | +| `day()` | `u8` | Day of month (1-31) | +| `hour()` | `u8` | Hour (0-23) | +| `minute()` | `u8` | Minute (0-59) | +| `second()` | `u8` | Second (0-59) | +| `nanosecond()` | `u32` | Nanosecond | +| `weekday()` | `Weekday` | Day of week enum | +| `day_of_year()` | `u16` | Day of year (1-366) | +| `day_ordinal()` | `&str` | Day with suffix ("15th") | +| `hour12()` | `u8` | 12-hour format (1-12) | +| `is_pm()` | `bool` | True if PM | +| `am_pm()` | `&str` | "AM" or "PM" | + +The `Weekday` and `Month` enums provide: +- `name()` - Full name ("Thursday", "January") +- `short_name()` - Abbreviated ("Thu", "Jan") + +### Low-Level API (`hyperlight_guest::time`) + +For cases where you need direct access or have a custom `GuestHandle`: + +```rust +use hyperlight_guest::time::{ + monotonic_time_ns, + wall_clock_time_ns, + is_clock_available, + utc_offset_seconds, +}; + +// Check availability +if is_clock_available(handle) { + // Get raw nanoseconds + let mono_ns = monotonic_time_ns(handle).unwrap(); + let wall_ns = wall_clock_time_ns(handle).unwrap(); + let offset = utc_offset_seconds(handle).unwrap(); +} +``` + +## C API + +The C API provides POSIX-compatible functions: + +### `gettimeofday` + +```c +#include "hyperlight_guest.h" + +hl_timeval tv; +hl_timezone tz; + +// Get wall-clock time and timezone +if (gettimeofday(&tv, &tz) == 0) { + // tv.tv_sec is seconds since Unix epoch + // tv.tv_usec is microseconds + // tz.tz_minuteswest is minutes west of UTC +} +``` + +### `clock_gettime` + +```c +#include "hyperlight_guest.h" + +hl_timespec ts; + +// Wall-clock time (UTC) +if (clock_gettime(hl_CLOCK_REALTIME, &ts) == 0) { + // ts.tv_sec is seconds since Unix epoch + // ts.tv_nsec is nanoseconds +} + +// Monotonic time (since sandbox creation) +if (clock_gettime(hl_CLOCK_MONOTONIC, &ts) == 0) { + // ts.tv_sec is seconds since sandbox started + // ts.tv_nsec is nanoseconds +} +``` + +### `time` + +```c +#include "hyperlight_guest.h" + +int64_t seconds = time(NULL); // Returns seconds since Unix epoch +``` + +### Broken-Down Time (`struct tm`) + +Convert timestamps to human-readable components: + +```c +#include "hyperlight_guest.h" + +int64_t now = time(NULL); +hl_tm tm_utc, tm_local; + +// UTC time +gmtime_r(&now, &tm_utc); + +// Local time (using timezone captured at sandbox creation) +localtime_r(&now, &tm_local); + +// Access components +int year = tm_local.tm_year + 1900; // Years since 1900 +int month = tm_local.tm_mon + 1; // 0-11, so add 1 +int day = tm_local.tm_mday; // 1-31 +int hour = tm_local.tm_hour; // 0-23 +int minute = tm_local.tm_min; // 0-59 +int second = tm_local.tm_sec; // 0-59 +int weekday = tm_local.tm_wday; // 0=Sunday, 6=Saturday +int yearday = tm_local.tm_yday; // 0-365 +``` + +### `strftime` - Format Time as String + +```c +#include "hyperlight_guest.h" + +int64_t now = time(NULL); +hl_tm tm_local; +localtime_r(&now, &tm_local); + +char buf[128]; +size_t len = strftime((uint8_t*)buf, sizeof(buf), + (const uint8_t*)"%A %d %B %Y %H:%M:%S", + &tm_local); +// buf = "Thursday 15 January 2026 15:34:56" +``` + +#### Supported Format Specifiers + +| Specifier | Description | Example | +|-----------|-------------|---------| +| `%a` | Abbreviated weekday | "Thu" | +| `%A` | Full weekday | "Thursday" | +| `%b`, `%h` | Abbreviated month | "Jan" | +| `%B` | Full month | "January" | +| `%d` | Day of month (01-31) | "15" | +| `%e` | Day of month, space-padded | " 5" | +| `%H` | Hour 24h (00-23) | "15" | +| `%I` | Hour 12h (01-12) | "03" | +| `%j` | Day of year (001-366) | "015" | +| `%m` | Month (01-12) | "01" | +| `%M` | Minute (00-59) | "34" | +| `%p` | AM/PM | "PM" | +| `%P` | am/pm | "pm" | +| `%S` | Second (00-59) | "56" | +| `%u` | Weekday (1-7, Mon=1) | "4" | +| `%w` | Weekday (0-6, Sun=0) | "4" | +| `%y` | Year without century | "26" | +| `%Y` | Year with century | "2026" | +| `%z` | Timezone offset | "+0100" | +| `%Z` | Timezone name | "UTC" or "LOCAL" | +| `%%` | Literal % | "%" | +| `%n` | Newline | "\n" | +| `%t` | Tab | "\t" | + +### `mktime` / `timegm` - Convert to Timestamp + +```c +hl_tm tm = { + .tm_year = 2026 - 1900, // Years since 1900 + .tm_mon = 0, // January (0-11) + .tm_mday = 15, // Day of month + .tm_hour = 15, + .tm_min = 34, + .tm_sec = 56 +}; + +// From local time to UTC timestamp +int64_t local_ts = mktime(&tm); + +// From UTC time to UTC timestamp +int64_t utc_ts = timegm(&tm); +``` + +### Supported Clock IDs + +| Clock ID | Description | +|----------|-------------| +| `hl_CLOCK_REALTIME` | Wall-clock time (UTC) | +| `hl_CLOCK_REALTIME_COARSE` | Same as `CLOCK_REALTIME` | +| `hl_CLOCK_MONOTONIC` | Time since sandbox creation | +| `hl_CLOCK_MONOTONIC_COARSE` | Same as `CLOCK_MONOTONIC` | +| `hl_CLOCK_BOOTTIME` | Same as `CLOCK_MONOTONIC` | + +Note: `CLOCK_PROCESS_CPUTIME_ID` and `CLOCK_THREAD_CPUTIME_ID` are not supported. + +## Timezone Handling + +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. + +> **⚠️ Limitation: Static Timezone Offset** +> +> The timezone offset is a snapshot from sandbox creation time. It does **not** update +> if the host's timezone changes during the sandbox lifetime. This means: +> +> - **DST transitions are not reflected**: If a sandbox is created before a DST change +> and continues running after, local time will be off by one hour. +> - **Manual timezone changes are not reflected**: If the host's timezone is changed +> while the sandbox is running, the guest will still use the original offset. +> +> For applications where accurate local time across DST boundaries is critical, +> consider using UTC time and handling timezone conversion on the host side. + +```rust +// Rust +use hyperlight_guest_bin::time::{utc_offset_seconds, local_time_ns}; + +let offset = utc_offset_seconds().unwrap(); // Seconds east of UTC +let local_ns = local_time_ns().unwrap(); // Local time in nanoseconds +``` + +```c +// C - use gettimeofday with timezone +hl_timeval tv; +hl_timezone tz; +gettimeofday(&tv, &tz); +int offset_seconds = -(tz.tz_minuteswest * 60); // Convert to seconds east +``` + +## Performance + +Reading time via the paravirtualized clock is very fast because: + +1. No VM exit is required +2. The clock page is in shared memory accessible to the guest +3. Only a few memory reads and TSC reads are needed + +This makes it suitable for high-frequency timing operations like benchmarking or rate limiting. + +## Error Handling + +Time functions return `None` (Rust) or `-1` (C) if: + +- The clock is not available (hypervisor doesn't support pvclock) +- The clock data is being updated (rare, retry will succeed) + +For the high-level Rust API, `SystemTime::now()` and `Instant::now()` return a zero time if the clock is unavailable, rather than panicking. diff --git a/docs/how-to-build-a-hyperlight-guest-binary.md b/docs/how-to-build-a-hyperlight-guest-binary.md index a76e43d4b..a6773c570 100644 --- a/docs/how-to-build-a-hyperlight-guest-binary.md +++ b/docs/how-to-build-a-hyperlight-guest-binary.md @@ -23,6 +23,12 @@ the guest to: - register functions that can be called by the host application - call host functions that have been registered by the host. +### Available Features + +- **`guest_time`** (enabled by default): Provides time-related functionality via a paravirtualized clock. + This includes `SystemTime`, `Instant`, and `UNIX_EPOCH` in `hyperlight_guest_bin::time` that mirror + the `std::time` API. See [Guest Time API](./guest-time.md) for details. + ## C guest binary For the binary written in C, the generated C bindings can be downloaded from the @@ -30,3 +36,9 @@ latest release page that contain: the `hyperlight_guest.h` header and the C API library. The `hyperlight_guest.h` header contains the corresponding APIs to register guest functions and call host functions from within the guest. + +### Available Features + +When built with the `guest_time` feature (enabled by default), the C API provides +POSIX-compatible time functions: `gettimeofday()`, `clock_gettime()`, and `time()`. +See [Guest Time API](./guest-time.md) for details. diff --git a/src/hyperlight_common/Cargo.toml b/src/hyperlight_common/Cargo.toml index 2f54a2f6d..ad1832adf 100644 --- a/src/hyperlight_common/Cargo.toml +++ b/src/hyperlight_common/Cargo.toml @@ -24,13 +24,15 @@ spin = "0.10.0" thiserror = { version = "2.0.16", default-features = false } [features] -default = ["tracing"] +default = ["tracing", "guest_time"] tracing = ["dep:tracing"] fuzzing = ["dep:arbitrary"] trace_guest = [] mem_profile = [] std = ["thiserror/std", "log/std", "tracing/std"] init-paging = [] +# Enable paravirtualized clock support for guest time functions +guest_time = [] [lib] bench = false # see https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options diff --git a/src/hyperlight_common/src/lib.rs b/src/hyperlight_common/src/lib.rs index da9294c19..4338e6d14 100644 --- a/src/hyperlight_common/src/lib.rs +++ b/src/hyperlight_common/src/lib.rs @@ -39,6 +39,9 @@ pub mod outb; /// cbindgen:ignore pub mod resource; +/// cbindgen:ignore +pub mod time; + /// cbindgen:ignore pub mod func; // cbindgen:ignore diff --git a/src/hyperlight_common/src/mem.rs b/src/hyperlight_common/src/mem.rs index 2f1f03e07..1a0a04f87 100644 --- a/src/hyperlight_common/src/mem.rs +++ b/src/hyperlight_common/src/mem.rs @@ -38,6 +38,8 @@ pub struct GuestStack { pub user_stack_address: u64, } +use crate::time::GuestClockRegion; + #[derive(Debug, Clone, Copy)] #[repr(C)] pub struct HyperlightPEB { @@ -49,4 +51,6 @@ pub struct HyperlightPEB { pub guest_heap: GuestMemoryRegion, pub guest_stack: GuestStack, pub host_function_definitions: GuestMemoryRegion, + /// Guest clock region for paravirtualized time support + pub guest_clock: GuestClockRegion, } diff --git a/src/hyperlight_common/src/time.rs b/src/hyperlight_common/src/time.rs new file mode 100644 index 000000000..a82ac87c7 --- /dev/null +++ b/src/hyperlight_common/src/time.rs @@ -0,0 +1,205 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +//! Paravirtualized clock structures shared between host and guest. +//! +//! These structures enable guests to read time without VM exits by using +//! shared memory pages that the hypervisor updates. + +/// KVM pvclock structure (defined by KVM ABI). +/// +/// The host writes to this structure, and the guest reads it to compute +/// the current time in nanoseconds. +/// +/// Reference: Linux kernel `arch/x86/include/asm/pvclock.h` +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct KvmPvclockVcpuTimeInfo { + /// Version counter - odd means update in progress. + /// Guest must re-read if this changes during read. + pub version: u32, + pub pad0: u32, + /// TSC value when `system_time` was captured. + pub tsc_timestamp: u64, + /// System time in nanoseconds at `tsc_timestamp`. + pub system_time: u64, + /// Multiplier for TSC -> nanoseconds conversion. + pub tsc_to_system_mul: u32, + /// Shift for TSC -> nanoseconds conversion (can be negative). + pub tsc_shift: i8, + /// Flags (e.g., TSC stable bit). + pub flags: u8, + pub pad: [u8; 2], +} + +/// Hyper-V Reference TSC page structure (defined by Hyper-V ABI). +/// +/// Used by both MSHV (Linux) and WHP (Windows). +/// Time is in 100-nanosecond intervals. +/// +/// Reference: Hyper-V TLFS (Top Level Functional Specification) +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct HvReferenceTscPage { + /// Sequence counter. If 0, guest must fall back to MSR read. + /// Guest must re-read if this changes during read. + pub tsc_sequence: u32, + pub reserved1: u32, + /// Scale factor for TSC -> time conversion. + /// Formula: time = (tsc * tsc_scale) >> 64 + pub tsc_scale: u64, + /// Offset to add after scaling (in 100ns units). + pub tsc_offset: i64, + /// Rest of the 4KB page is reserved. + pub reserved2: [u64; 509], +} + +/// Type of paravirtualized clock configured for the guest. +#[repr(u64)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ClockType { + /// No clock configured - time functions will return None. + None = 0, + /// KVM pvclock (Linux KVM hypervisor). + KvmPvclock = 1, + /// Hyper-V Reference TSC (MSHV on Linux, WHP on Windows). + HyperVReferenceTsc = 2, +} + +impl From for ClockType { + fn from(value: u64) -> Self { + match value { + 1 => ClockType::KvmPvclock, + 2 => ClockType::HyperVReferenceTsc, + _ => ClockType::None, + } + } +} + +impl From for u64 { + fn from(value: ClockType) -> Self { + value as u64 + } +} + +/// Clock region in the PEB (Process Environment Block). +/// +/// Contains a pointer to the clock page and metadata needed to +/// compute wall-clock time. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct GuestClockRegion { + /// Guest virtual address of the clock page. + /// 0 if clock is not configured. + pub clock_page_ptr: u64, + /// Type of clock (see [`ClockType`]). + pub clock_type: u64, + /// UTC time in nanoseconds since Unix epoch (1970-01-01 00:00:00 UTC) + /// at the moment the sandbox was created. + /// + /// Wall-clock time = boot_time_ns + monotonic_time_ns + pub boot_time_ns: u64, + /// UTC offset in seconds at the time the sandbox was created. + /// + /// This captures the host's timezone offset from UTC. Positive values are + /// east of UTC (e.g., +3600 for UTC+1), negative values are west (e.g., + /// -18000 for UTC-5/EST). + /// + /// Local time = wall_clock_time + utc_offset_seconds * 1_000_000_000 + pub utc_offset_seconds: i32, + /// Padding to maintain 8-byte alignment. + _padding: u32, +} + +impl Default for GuestClockRegion { + fn default() -> Self { + Self { + clock_page_ptr: 0, + clock_type: ClockType::None as u64, + boot_time_ns: 0, + utc_offset_seconds: 0, + _padding: 0, + } + } +} + +impl GuestClockRegion { + /// Creates a new `GuestClockRegion` with the specified parameters. + pub fn new( + clock_page_ptr: u64, + clock_type: ClockType, + boot_time_ns: u64, + utc_offset_seconds: i32, + ) -> Self { + Self { + clock_page_ptr, + clock_type: clock_type as u64, + boot_time_ns, + utc_offset_seconds, + _padding: 0, + } + } + + /// Returns true if a clock is configured. + pub fn is_available(&self) -> bool { + self.clock_page_ptr != 0 && self.clock_type != ClockType::None as u64 + } + + /// Returns the clock type. + pub fn get_clock_type(&self) -> ClockType { + ClockType::from(self.clock_type) + } +} + +#[cfg(test)] +mod tests { + use core::mem::size_of; + + use super::*; + + #[test] + fn test_kvm_pvclock_size() { + // KVM pvclock struct must be exactly 32 bytes + assert_eq!(size_of::(), 32); + } + + #[test] + fn test_hv_reference_tsc_size() { + // Hyper-V reference TSC page must be exactly 4KB + assert_eq!(size_of::(), 4096); + } + + #[test] + fn test_guest_clock_region_size() { + // GuestClockRegion should be 32 bytes (4 x u64 equivalent: 3 x u64 + i32 + u32) + assert_eq!(size_of::(), 32); + } + + #[test] + fn test_clock_type_conversion() { + assert_eq!(ClockType::from(0u64), ClockType::None); + assert_eq!(ClockType::from(1u64), ClockType::KvmPvclock); + assert_eq!(ClockType::from(2u64), ClockType::HyperVReferenceTsc); + assert_eq!(ClockType::from(99u64), ClockType::None); + } + + #[test] + fn test_guest_clock_region_default() { + let region = GuestClockRegion::default(); + assert!(!region.is_available()); + assert_eq!(region.get_clock_type(), ClockType::None); + } +} diff --git a/src/hyperlight_guest/Cargo.toml b/src/hyperlight_guest/Cargo.toml index 806ee5a42..a7379dca7 100644 --- a/src/hyperlight_guest/Cargo.toml +++ b/src/hyperlight_guest/Cargo.toml @@ -22,5 +22,7 @@ tracing = { version = "0.1.44", default-features = false, features = ["attribute hyperlight-guest-tracing = { workspace = true, default-features = false, optional = true } [features] -default = [] +default = ["guest_time"] trace_guest = ["dep:hyperlight-guest-tracing", "hyperlight-guest-tracing?/trace"] +# Enable paravirtualized clock support for guest time functions +guest_time = ["hyperlight-common/guest_time"] diff --git a/src/hyperlight_guest/src/lib.rs b/src/hyperlight_guest/src/lib.rs index fce0767c0..25fdd2ae7 100644 --- a/src/hyperlight_guest/src/lib.rs +++ b/src/hyperlight_guest/src/lib.rs @@ -23,6 +23,8 @@ extern crate alloc; // Modules pub mod error; pub mod exit; +#[cfg(feature = "guest_time")] +pub mod time; pub mod guest_handle { pub mod handle; diff --git a/src/hyperlight_guest/src/time.rs b/src/hyperlight_guest/src/time.rs new file mode 100644 index 000000000..0079da814 --- /dev/null +++ b/src/hyperlight_guest/src/time.rs @@ -0,0 +1,645 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +//! Low-level guest time functions using paravirtualized clock. +//! +//! This module provides low-level functions to read time without VM exits by using +//! the shared clock page configured by the hypervisor. These functions require an +//! explicit `GuestHandle` reference. +//! +//! # For most users +//! +//! Use [`hyperlight_guest_bin::time`] instead, which provides a `std::time`-compatible +//! API with `SystemTime` and `Instant` types that don't require passing a handle. +//! +//! # Supported Clock Types +//! +//! - **KVM pvclock**: Used when running under KVM hypervisor +//! - **Hyper-V Reference TSC**: Used when running under MSHV or WHP +//! +//! # Usage +//! +//! ```ignore +//! use hyperlight_guest::time::{monotonic_time_ns, wall_clock_time_ns}; +//! +//! // Get time since sandbox creation (monotonic) +//! if let Some(ns) = monotonic_time_ns(guest_handle) { +//! // ns is nanoseconds since sandbox started +//! } +//! +//! // Get wall-clock time (UTC) +//! if let Some(ns) = wall_clock_time_ns(guest_handle) { +//! // ns is nanoseconds since Unix epoch (1970-01-01 00:00:00 UTC) +//! } +//! ``` + +use core::sync::atomic::{Ordering, compiler_fence}; + +use hyperlight_common::mem::HyperlightPEB; +use hyperlight_common::time::{ + ClockType, GuestClockRegion, HvReferenceTscPage, KvmPvclockVcpuTimeInfo, +}; + +use crate::guest_handle::handle::GuestHandle; + +/// Read the CPU's Time Stamp Counter (TSC). +/// +/// This is a monotonically increasing counter that increments at the CPU's +/// base frequency. +#[inline] +fn rdtsc() -> u64 { + #[cfg(target_arch = "x86_64")] + { + let lo: u32; + let hi: u32; + // SAFETY: RDTSC is always available on x86_64 + unsafe { + core::arch::asm!( + "rdtsc", + out("eax") lo, + out("edx") hi, + options(nostack, nomem, preserves_flags) + ); + } + ((hi as u64) << 32) | (lo as u64) + } + #[cfg(not(target_arch = "x86_64"))] + { + 0 // TSC not available on non-x86_64 architectures + } +} + +/// Read time from KVM pvclock structure. +/// +/// Returns nanoseconds since the clock was initialized, or None if +/// the clock data is invalid or being updated. +fn read_kvm_pvclock(clock_page_ptr: u64) -> Option { + // SAFETY: clock_page_ptr was set by the host and points to valid memory + let pvclock = unsafe { &*(clock_page_ptr as *const KvmPvclockVcpuTimeInfo) }; + + // Read version - odd means update in progress + let version1 = unsafe { core::ptr::read_volatile(&pvclock.version) }; + if version1 & 1 != 0 { + return None; // Update in progress, retry later + } + + compiler_fence(Ordering::Acquire); + + // Read clock data + let tsc_timestamp = pvclock.tsc_timestamp; + let system_time = pvclock.system_time; + let tsc_to_system_mul = pvclock.tsc_to_system_mul; + let tsc_shift = pvclock.tsc_shift; + + compiler_fence(Ordering::Acquire); + + // Check version again - must match + let version2 = unsafe { core::ptr::read_volatile(&pvclock.version) }; + if version1 != version2 { + return None; // Data changed during read, retry later + } + + // Get current TSC + let tsc_now = rdtsc(); + + // Calculate elapsed TSC ticks + let tsc_delta = tsc_now.wrapping_sub(tsc_timestamp); + + // Convert TSC delta to nanoseconds + // Formula: ns = (tsc_delta * tsc_to_system_mul) >> (32 - tsc_shift) + // But tsc_shift can be negative, so we need to handle both cases + let ns_delta = if tsc_shift >= 0 { + ((tsc_delta as u128 * tsc_to_system_mul as u128) >> (32 - tsc_shift as u32)) as u64 + } else { + ((tsc_delta as u128 * tsc_to_system_mul as u128) >> (32 + (-tsc_shift) as u32)) as u64 + }; + + Some(system_time.wrapping_add(ns_delta)) +} + +/// Read time from Hyper-V Reference TSC page. +/// +/// Returns nanoseconds since the clock was initialized, or None if +/// the clock data is invalid. +fn read_hv_reference_tsc(clock_page_ptr: u64) -> Option { + // SAFETY: clock_page_ptr was set by the host and points to valid memory + let tsc_page = unsafe { &*(clock_page_ptr as *const HvReferenceTscPage) }; + + // Read sequence - 0 means fallback to MSR (not supported in guest) + let seq1 = unsafe { core::ptr::read_volatile(&tsc_page.tsc_sequence) }; + if seq1 == 0 { + return None; // Must use MSR fallback, not available in guest + } + + compiler_fence(Ordering::Acquire); + + // Read clock data + let tsc_scale = tsc_page.tsc_scale; + let tsc_offset = tsc_page.tsc_offset; + + compiler_fence(Ordering::Acquire); + + // Check sequence again + let seq2 = unsafe { core::ptr::read_volatile(&tsc_page.tsc_sequence) }; + if seq1 != seq2 { + return None; // Data changed during read, retry later + } + + // Get current TSC + let tsc_now = rdtsc(); + + // Calculate time in 100ns units + // Formula: time_100ns = ((tsc * scale) >> 64) + offset + let scaled = ((tsc_now as u128 * tsc_scale as u128) >> 64) as i64; + let time_100ns = scaled.wrapping_add(tsc_offset); + + if time_100ns < 0 { + return None; // Invalid time + } + + // Convert 100ns units to nanoseconds + Some((time_100ns as u64) * 100) +} + +/// Get the guest clock region from the PEB. +fn get_clock_region(peb: *mut HyperlightPEB) -> Option<&'static GuestClockRegion> { + if peb.is_null() { + return None; + } + // SAFETY: PEB pointer is valid if not null, set during guest init + let peb_ref = unsafe { &*peb }; + Some(&peb_ref.guest_clock) +} + +/// Get monotonic time in nanoseconds since the sandbox was created. +/// +/// This time is monotonically increasing and suitable for measuring +/// elapsed time. It does not represent wall-clock time. +/// +/// # Arguments +/// * `handle` - The guest handle containing the PEB pointer +/// +/// # Returns +/// * `Some(ns)` - Nanoseconds since sandbox creation +/// * `None` - Clock not configured or read failed (caller should retry) +/// +/// # Example +/// ```ignore +/// let start = monotonic_time_ns(handle).unwrap_or(0); +/// // ... do work ... +/// let end = monotonic_time_ns(handle).unwrap_or(0); +/// let elapsed_ns = end - start; +/// ``` +pub fn monotonic_time_ns(handle: &GuestHandle) -> Option { + let peb = handle.peb()?; + let clock_region = get_clock_region(peb)?; + + if !clock_region.is_available() { + return None; + } + + match clock_region.get_clock_type() { + ClockType::KvmPvclock => read_kvm_pvclock(clock_region.clock_page_ptr), + ClockType::HyperVReferenceTsc => read_hv_reference_tsc(clock_region.clock_page_ptr), + ClockType::None => None, + } +} + +/// Get wall-clock time in nanoseconds since the Unix epoch. +/// +/// Returns the current UTC time as nanoseconds since 1970-01-01 00:00:00 UTC. +/// This is computed by adding the boot time (when sandbox was created) to +/// the monotonic time. +/// +/// # Arguments +/// * `handle` - The guest handle containing the PEB pointer +/// +/// # Returns +/// * `Some(ns)` - Nanoseconds since Unix epoch (UTC) +/// * `None` - Clock not configured or read failed (caller should retry) +/// +/// # Example +/// ```ignore +/// if let Some(ns) = wall_clock_time_ns(handle) { +/// let secs = ns / 1_000_000_000; +/// let nsecs = ns % 1_000_000_000; +/// // secs is Unix timestamp, nsecs is sub-second nanoseconds +/// } +/// ``` +pub fn wall_clock_time_ns(handle: &GuestHandle) -> Option { + let peb = handle.peb()?; + let clock_region = get_clock_region(peb)?; + + if !clock_region.is_available() { + return None; + } + + let monotonic = monotonic_time_ns(handle)?; + Some(clock_region.boot_time_ns.wrapping_add(monotonic)) +} + +/// Get monotonic time in microseconds since the sandbox was created. +/// +/// Convenience function that returns time in microseconds instead of nanoseconds. +/// +/// # Arguments +/// * `handle` - The guest handle containing the PEB pointer +/// +/// # Returns +/// * `Some(us)` - Microseconds since sandbox creation +/// * `None` - Clock not configured or read failed +pub fn monotonic_time_us(handle: &GuestHandle) -> Option { + monotonic_time_ns(handle).map(|ns| ns / 1_000) +} + +/// Get wall-clock time as seconds and nanoseconds since Unix epoch. +/// +/// Returns a tuple of (seconds, nanoseconds) suitable for use with +/// `timespec` structures or similar APIs. +/// +/// # Arguments +/// * `handle` - The guest handle containing the PEB pointer +/// +/// # Returns +/// * `Some((secs, nsecs))` - Seconds and sub-second nanoseconds since Unix epoch +/// * `None` - Clock not configured or read failed +pub fn wall_clock_time(handle: &GuestHandle) -> Option<(u64, u32)> { + let ns = wall_clock_time_ns(handle)?; + let secs = ns / 1_000_000_000; + let nsecs = (ns % 1_000_000_000) as u32; + Some((secs, nsecs)) +} + +/// Check if the paravirtualized clock is available. +/// +/// # Arguments +/// * `handle` - The guest handle containing the PEB pointer +/// +/// # Returns +/// * `true` - Clock is configured and available +/// * `false` - Clock is not configured +pub fn is_clock_available(handle: &GuestHandle) -> bool { + handle + .peb() + .and_then(get_clock_region) + .is_some_and(|r| r.is_available()) +} + +/// Get the UTC offset in seconds at sandbox creation time. +/// +/// Returns the timezone offset that was captured when the sandbox was created. +/// This represents the host's local timezone offset from UTC at that moment. +/// +/// Positive values are east of UTC (e.g., +3600 for UTC+1), negative values +/// are west (e.g., -18000 for UTC-5/EST). +/// +/// # Arguments +/// * `handle` - The guest handle containing the PEB pointer +/// +/// # Returns +/// * `Some(offset)` - Seconds offset from UTC +/// * `None` - Clock not configured +/// +/// # Example +/// ```ignore +/// if let Some(offset) = utc_offset_seconds(handle) { +/// // Get local time from wall clock time +/// let wall_ns = wall_clock_time_ns(handle).unwrap_or(0); +/// let local_ns = wall_ns.wrapping_add((offset as i64 * 1_000_000_000) as u64); +/// } +/// ``` +pub fn utc_offset_seconds(handle: &GuestHandle) -> Option { + let peb = handle.peb()?; + let clock_region = get_clock_region(peb)?; + + if !clock_region.is_available() { + return None; + } + + Some(clock_region.utc_offset_seconds) +} + +/// Get local time in nanoseconds since the Unix epoch. +/// +/// Returns the current local time as nanoseconds since 1970-01-01 00:00:00 UTC, +/// adjusted for the host's timezone offset at sandbox creation time. +/// +/// Note: This uses a static timezone offset captured at sandbox creation. +/// It does not account for DST changes that might occur during the sandbox +/// lifetime. +/// +/// # Arguments +/// * `handle` - The guest handle containing the PEB pointer +/// +/// # Returns +/// * `Some(ns)` - Nanoseconds since Unix epoch in local time +/// * `None` - Clock not configured or read failed +pub fn local_time_ns(handle: &GuestHandle) -> Option { + let wall_ns = wall_clock_time_ns(handle)?; + let offset = utc_offset_seconds(handle)?; + // Add offset (can be negative, so we use wrapping_add with cast) + Some(wall_ns.wrapping_add((offset as i64 * 1_000_000_000) as u64)) +} + +// ============================================================================ +// Date/time calculation utilities - shared between guest crates +// +// These functions provide pure date/time calculations that don't depend on +// any clock source. They are shared between hyperlight_guest_bin (Rust API) +// and hyperlight_guest_capi (C API). +// ============================================================================ + +// Time constants +/// Seconds per day (86400). +pub(crate) const SECS_PER_DAY: i64 = 86400; +/// Seconds per hour (3600). +pub(crate) const SECS_PER_HOUR: i64 = 3600; +/// Seconds per minute (60). +pub(crate) const SECS_PER_MINUTE: i64 = 60; +/// Nanoseconds per second (1,000,000,000). +pub const NANOS_PER_SEC: u64 = 1_000_000_000; + +// Calendar constants for date calculations +const DAYS_FROM_YEAR_0_TO_1970: i32 = 719528; +const DAYS_PER_400_YEAR_CYCLE: i32 = 146097; +const DAYS_PER_100_YEAR_CYCLE: i32 = 36524; +const DAYS_PER_4_YEAR_CYCLE: i32 = 1461; +const DAYS_PER_YEAR: i32 = 365; +const DAYS_PER_LEAP_YEAR: i32 = 366; + +/// Returns true if the given year is a leap year. +#[inline] +#[must_use] +pub(crate) const fn is_leap_year(year: i32) -> bool { + (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) +} + +/// Returns the number of days in a month (1-12). +/// +/// Returns 0 for invalid month values (outside 1-12). +#[inline] +#[must_use] +pub const fn days_in_month(year: i32, month: u8) -> u8 { + match month { + 1 => 31, + 2 => { + if is_leap_year(year) { + 29 + } else { + 28 + } + } + 3 => 31, + 4 => 30, + 5 => 31, + 6 => 30, + 7 => 31, + 8 => 31, + 9 => 30, + 10 => 31, + 11 => 30, + 12 => 31, + _ => 0, + } +} + +/// Returns the day of week for a date. +/// +/// # Arguments +/// * `year` - The year +/// * `month` - Month (1-12) +/// * `day` - Day of month (1-31) +/// +/// # Returns +/// Day of week where Monday = 0, Sunday = 6 +#[inline] +#[must_use] +pub fn day_of_week_monday(year: i32, month: u8, day: u8) -> u8 { + let h = zeller_congruence(year, month, day); + // Convert Zeller's result (0=Sat, 1=Sun, ..., 6=Fri) to Monday=0 + match h { + 0 => 5, // Sat + 1 => 6, // Sun + 2 => 0, // Mon + 3 => 1, // Tue + 4 => 2, // Wed + 5 => 3, // Thu + 6 => 4, // Fri + _ => 0, + } +} + +/// Returns the day of week for a date (POSIX style). +/// +/// # Arguments +/// * `year` - The year +/// * `month` - Month (1-12) +/// * `day` - Day of month (1-31) +/// +/// # Returns +/// Day of week where Sunday = 0, Saturday = 6 (POSIX tm_wday convention) +#[inline] +#[must_use] +pub fn day_of_week_sunday(year: i32, month: u8, day: u8) -> u8 { + let h = zeller_congruence(year, month, day); + // Convert Zeller's result (0=Sat, 1=Sun, ..., 6=Fri) to Sunday=0 + match h { + 0 => 6, // Sat + 1 => 0, // Sun + 2 => 1, // Mon + 3 => 2, // Tue + 4 => 3, // Wed + 5 => 4, // Thu + 6 => 5, // Fri + _ => 0, + } +} + +/// Zeller's congruence algorithm for calculating day of week. +/// +/// Returns a value 0-6 where: +/// - 0 = Saturday +/// - 1 = Sunday +/// - 2 = Monday +/// - 3 = Tuesday +/// - 4 = Wednesday +/// - 5 = Thursday +/// - 6 = Friday +#[inline] +fn zeller_congruence(year: i32, month: u8, day: u8) -> u8 { + // Adjust for Zeller's: treat Jan/Feb as months 13/14 of previous year + let (y, m) = if month < 3 { + (year - 1, month as i32 + 12) + } else { + (year, month as i32) + }; + + let q = day as i32; + let k = y % 100; + let j = y / 100; + + // Zeller's formula + let h = (q + (13 * (m + 1)) / 5 + k + k / 4 + j / 4 - 2 * j) % 7; + ((h + 7) % 7) as u8 // Ensure positive +} + +/// Returns the day of year (1-366) for a date. +/// +/// # Arguments +/// * `year` - The year (for leap year calculation) +/// * `month` - Month (1-12) +/// * `day` - Day of month (1-31) +#[inline] +#[must_use] +pub fn day_of_year(year: i32, month: u8, day: u8) -> u16 { + let mut doy = day as u16; + for m in 1..month { + doy += days_in_month(year, m) as u16; + } + doy +} + +/// Converts Unix timestamp (seconds since epoch) to date/time components. +/// +/// # Arguments +/// * `secs` - Seconds since Unix epoch (1970-01-01 00:00:00 UTC) +/// +/// # Returns +/// Tuple of (year, month, day, hour, minute, second) +/// - year: Full year (e.g., 2026) +/// - month: 1-12 +/// - day: 1-31 +/// - hour: 0-23 +/// - minute: 0-59 +/// - second: 0-59 +#[must_use] +pub fn timestamp_to_datetime(secs: i64) -> (i32, u8, u8, u8, u8, u8) { + // Handle time of day + let time_of_day = secs.rem_euclid(SECS_PER_DAY) as u32; + let hour = (time_of_day / SECS_PER_HOUR as u32) as u8; + let minute = ((time_of_day % SECS_PER_HOUR as u32) / SECS_PER_MINUTE as u32) as u8; + let second = (time_of_day % SECS_PER_MINUTE as u32) as u8; + + // Calculate days since epoch (can be negative) + let mut days = secs.div_euclid(SECS_PER_DAY) as i32; + + // Add days from 1970 to year 0 for easier calculation + days += DAYS_FROM_YEAR_0_TO_1970; + + // Calculate year using the 400-year cycle + let cycles_400 = days.div_euclid(DAYS_PER_400_YEAR_CYCLE); + days = days.rem_euclid(DAYS_PER_400_YEAR_CYCLE); + + let mut year = cycles_400 * 400; + + // 100-year cycles within 400-year cycle + let cycles_100 = (days / DAYS_PER_100_YEAR_CYCLE).min(3); + days -= cycles_100 * DAYS_PER_100_YEAR_CYCLE; + year += cycles_100 * 100; + + // 4-year cycles + let cycles_4 = days / DAYS_PER_4_YEAR_CYCLE; + days -= cycles_4 * DAYS_PER_4_YEAR_CYCLE; + year += cycles_4 * 4; + + // Remaining years within the 4-year cycle + // The first year of the 4-year cycle is a leap year (366 days), + // remaining years have 365 days each + if days >= DAYS_PER_LEAP_YEAR { + // Past the leap year + days -= DAYS_PER_LEAP_YEAR; + year += 1; + let years_remaining = (days / DAYS_PER_YEAR).min(2); + days -= years_remaining * DAYS_PER_YEAR; + year += years_remaining; + } + + // days is now day of year (0-indexed) + let doy = days as u16; + + // Find month and day using days_in_month + let mut month = 1u8; + let mut remaining = doy as i32; + + while remaining >= days_in_month(year, month) as i32 { + remaining -= days_in_month(year, month) as i32; + month += 1; + } + + let day = (remaining + 1) as u8; + + (year, month, day, hour, minute, second) +} + +/// Converts date/time components to Unix timestamp (seconds since epoch). +/// +/// # Arguments +/// * `year` - Full year (e.g., 2026) +/// * `month` - Month (1-12) +/// * `day` - Day of month (1-31) +/// * `hour` - Hour (0-23) +/// * `minute` - Minute (0-59) +/// * `second` - Second (0-59) +/// +/// # Returns +/// Seconds since Unix epoch (1970-01-01 00:00:00 UTC), or `None` if any +/// input is invalid (month outside 1-12, day outside 1-31, etc.) +#[must_use] +pub fn datetime_to_timestamp( + year: i32, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, +) -> Option { + // Validate inputs + if month == 0 || month > 12 { + return None; + } + if day == 0 || day > 31 { + return None; + } + if hour > 23 || minute > 59 || second > 59 { + return None; + } + // Validate day for the specific month + if day > days_in_month(year, month) { + return None; + } + + // Days from year 0 to the given year + let y = year - 1; + let mut days = y * 365 + y / 4 - y / 100 + y / 400; + + // Add days for completed months + for m in 1..month { + days += days_in_month(year, m) as i32; + } + + // Add days in current month + days += (day - 1) as i32; + + // Subtract days from year 0 to 1970 + days -= DAYS_FROM_YEAR_0_TO_1970; + + // Convert to seconds + let secs = days as i64 * SECS_PER_DAY + + hour as i64 * SECS_PER_HOUR + + minute as i64 * SECS_PER_MINUTE + + second as i64; + Some(secs) +} diff --git a/src/hyperlight_guest_bin/Cargo.toml b/src/hyperlight_guest_bin/Cargo.toml index 4ce3f30ee..76fcd90d3 100644 --- a/src/hyperlight_guest_bin/Cargo.toml +++ b/src/hyperlight_guest_bin/Cargo.toml @@ -14,12 +14,14 @@ and third-party code used by our C-API needed to build a native hyperlight-guest """ [features] -default = ["libc", "printf", "macros"] +default = ["libc", "printf", "macros", "guest_time"] libc = [] # compile musl libc printf = [ "libc" ] # compile printf trace_guest = ["hyperlight-common/trace_guest", "hyperlight-guest/trace_guest", "hyperlight-guest-tracing/trace"] mem_profile = ["hyperlight-common/mem_profile"] macros = ["dep:hyperlight-guest-macro", "dep:linkme"] +# Enable paravirtualized clock support for guest time functions +guest_time = ["hyperlight-common/guest_time", "hyperlight-guest/guest_time"] [dependencies] hyperlight-guest = { workspace = true, default-features = false } diff --git a/src/hyperlight_guest_bin/src/lib.rs b/src/hyperlight_guest_bin/src/lib.rs index d692e0b26..7798ead95 100644 --- a/src/hyperlight_guest_bin/src/lib.rs +++ b/src/hyperlight_guest_bin/src/lib.rs @@ -54,6 +54,8 @@ pub mod guest_logger; pub mod host_comm; pub mod memory; pub mod paging; +#[cfg(feature = "guest_time")] +pub mod time; // Globals #[cfg(feature = "mem_profile")] diff --git a/src/hyperlight_guest_bin/src/time.rs b/src/hyperlight_guest_bin/src/time.rs new file mode 100644 index 000000000..978224b19 --- /dev/null +++ b/src/hyperlight_guest_bin/src/time.rs @@ -0,0 +1,682 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +//! Time types that mirror `std::time` for guest code. +//! +//! This module provides `SystemTime` and `Instant` types that have the same API +//! as `std::time::SystemTime` and `std::time::Instant`, using the paravirtualized +//! clock configured by the hypervisor. +//! +//! # Example +//! +//! ```ignore +//! use hyperlight_guest_bin::time::{SystemTime, Instant, UNIX_EPOCH}; +//! use core::time::Duration; +//! +//! // Wall-clock time (like std::time::SystemTime) +//! let now = SystemTime::now(); +//! let duration = now.duration_since(UNIX_EPOCH).unwrap(); +//! let unix_timestamp = duration.as_secs(); +//! +//! // Monotonic time for measuring elapsed time (like std::time::Instant) +//! let start = Instant::now(); +//! // ... do work ... +//! let elapsed = start.elapsed(); +//! ``` + +use core::time::Duration; + +use hyperlight_guest::time as guest_time; + +use crate::GUEST_HANDLE; + +/// A measurement of the system clock, similar to `std::time::SystemTime`. +/// +/// This represents wall-clock time (UTC) and can be compared to `UNIX_EPOCH` +/// to get a Unix timestamp. +/// +/// Unlike monotonic time, this clock may jump forwards or backwards if the +/// host system's clock is adjusted. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SystemTime(u64); + +/// An anchor in time representing the Unix epoch (1970-01-01 00:00:00 UTC). +pub const UNIX_EPOCH: SystemTime = SystemTime(0); + +/// An error returned when the system time is before the Unix epoch. +#[derive(Clone, Debug)] +pub struct SystemTimeError(Duration); + +impl SystemTimeError { + /// Returns the positive duration representing how far the time is + /// before the reference point. + pub fn duration(&self) -> Duration { + self.0 + } +} + +impl SystemTime { + /// Returns the current system time (UTC wall-clock time). + /// + /// Returns the Unix epoch if the clock is not available. + pub fn now() -> Self { + // SAFETY: GUEST_HANDLE is initialized during entrypoint, we are single-threaded + #[allow(static_mut_refs)] + let handle = unsafe { &GUEST_HANDLE }; + + let ns = guest_time::wall_clock_time_ns(handle).unwrap_or(0); + SystemTime(ns) + } + + /// Returns the amount of time elapsed from an earlier point in time. + /// + /// # Errors + /// + /// Returns `SystemTimeError` if `earlier` is later than `self`. + pub fn duration_since(&self, earlier: SystemTime) -> Result { + if self.0 >= earlier.0 { + Ok(Duration::from_nanos(self.0 - earlier.0)) + } else { + Err(SystemTimeError(Duration::from_nanos(earlier.0 - self.0))) + } + } + + /// Returns the amount of time elapsed since this system time was created. + /// + /// # Errors + /// + /// Returns `SystemTimeError` if the current time is before `self`. + pub fn elapsed(&self) -> Result { + Self::now().duration_since(*self) + } + + /// Returns `Some(t)` where `t` is the time `self + duration` if `t` can + /// be represented, or `None` if the result would overflow. + pub fn checked_add(&self, duration: Duration) -> Option { + self.0 + .checked_add(duration.as_nanos() as u64) + .map(SystemTime) + } + + /// Returns `Some(t)` where `t` is the time `self - duration` if `t` can + /// be represented, or `None` if the result would underflow. + pub fn checked_sub(&self, duration: Duration) -> Option { + self.0 + .checked_sub(duration.as_nanos() as u64) + .map(SystemTime) + } +} + +impl core::ops::Add for SystemTime { + type Output = SystemTime; + + fn add(self, dur: Duration) -> SystemTime { + self.checked_add(dur).unwrap_or(SystemTime(u64::MAX)) + } +} + +impl core::ops::Sub for SystemTime { + type Output = SystemTime; + + fn sub(self, dur: Duration) -> SystemTime { + self.checked_sub(dur).unwrap_or(SystemTime(0)) + } +} + +/// A measurement of a monotonically increasing clock, similar to `std::time::Instant`. +/// +/// This is suitable for measuring elapsed time. Unlike `SystemTime`, this clock +/// is guaranteed to never go backwards. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Instant(u64); + +impl Instant { + /// Returns the current monotonic time. + /// + /// Returns a zero instant if the clock is not available. + pub fn now() -> Self { + // SAFETY: GUEST_HANDLE is initialized during entrypoint, we are single-threaded + #[allow(static_mut_refs)] + let handle = unsafe { &GUEST_HANDLE }; + + let ns = guest_time::monotonic_time_ns(handle).unwrap_or(0); + Instant(ns) + } + + /// Returns the amount of time elapsed from another instant to this one. + /// + /// # Panics + /// + /// Panics if `earlier` is later than `self` (monotonic time should not go backwards). + pub fn duration_since(&self, earlier: Instant) -> Duration { + self.checked_duration_since(earlier) + .expect("supplied instant is later than self") + } + + /// Returns the amount of time elapsed from another instant to this one, + /// or `None` if that instant is later than this one. + pub fn checked_duration_since(&self, earlier: Instant) -> Option { + if self.0 >= earlier.0 { + Some(Duration::from_nanos(self.0 - earlier.0)) + } else { + None + } + } + + /// Returns the amount of time elapsed from another instant to this one, + /// or zero if that instant is later than this one. + pub fn saturating_duration_since(&self, earlier: Instant) -> Duration { + self.checked_duration_since(earlier).unwrap_or_default() + } + + /// Returns the amount of time elapsed since this instant was created. + pub fn elapsed(&self) -> Duration { + Self::now().duration_since(*self) + } + + /// Returns `Some(t)` where `t` is the time `self + duration` if `t` can + /// be represented, or `None` if the result would overflow. + pub fn checked_add(&self, duration: Duration) -> Option { + self.0.checked_add(duration.as_nanos() as u64).map(Instant) + } + + /// Returns `Some(t)` where `t` is the time `self - duration` if `t` can + /// be represented, or `None` if the result would underflow. + pub fn checked_sub(&self, duration: Duration) -> Option { + self.0.checked_sub(duration.as_nanos() as u64).map(Instant) + } +} + +impl core::ops::Add for Instant { + type Output = Instant; + + fn add(self, dur: Duration) -> Instant { + self.checked_add(dur).unwrap_or(Instant(u64::MAX)) + } +} + +impl core::ops::Sub for Instant { + type Output = Instant; + + fn sub(self, dur: Duration) -> Instant { + self.checked_sub(dur).unwrap_or(Instant(0)) + } +} + +impl core::ops::Sub for Instant { + type Output = Duration; + + fn sub(self, other: Instant) -> Duration { + self.duration_since(other) + } +} + +/// Get the UTC offset in seconds that was captured when the sandbox was created. +/// +/// This represents the host's local timezone offset from UTC. Positive values +/// are east of UTC (e.g., +3600 for UTC+1), negative values are west (e.g., +/// -18000 for UTC-5/EST). +/// +/// Returns `None` if the clock is not available. +pub fn utc_offset_seconds() -> Option { + // SAFETY: GUEST_HANDLE is initialized during entrypoint, we are single-threaded + #[allow(static_mut_refs)] + let handle = unsafe { &GUEST_HANDLE }; + + guest_time::utc_offset_seconds(handle) +} + +/// Get the current local time as nanoseconds since the Unix epoch. +/// +/// This returns the wall-clock time adjusted for the host's timezone offset +/// that was captured at sandbox creation time. +/// +/// Note: This uses a static timezone offset and does not account for DST +/// changes that might occur during the sandbox lifetime. +/// +/// Returns `None` if the clock is not available. +pub fn local_time_ns() -> Option { + // SAFETY: GUEST_HANDLE is initialized during entrypoint, we are single-threaded + #[allow(static_mut_refs)] + let handle = unsafe { &GUEST_HANDLE }; + + guest_time::local_time_ns(handle) +} + +/// Days of the week. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum Weekday { + /// Monday (0) + Monday = 0, + /// Tuesday (1) + Tuesday = 1, + /// Wednesday (2) + Wednesday = 2, + /// Thursday (3) + Thursday = 3, + /// Friday (4) + Friday = 4, + /// Saturday (5) + Saturday = 5, + /// Sunday (6) + Sunday = 6, +} + +impl Weekday { + /// Returns the full name of the weekday (e.g., "Thursday"). + pub const fn name(self) -> &'static str { + match self { + Weekday::Monday => "Monday", + Weekday::Tuesday => "Tuesday", + Weekday::Wednesday => "Wednesday", + Weekday::Thursday => "Thursday", + Weekday::Friday => "Friday", + Weekday::Saturday => "Saturday", + Weekday::Sunday => "Sunday", + } + } + + /// Returns the short name of the weekday (e.g., "Thu"). + pub const fn short_name(self) -> &'static str { + match self { + Weekday::Monday => "Mon", + Weekday::Tuesday => "Tue", + Weekday::Wednesday => "Wed", + Weekday::Thursday => "Thu", + Weekday::Friday => "Fri", + Weekday::Saturday => "Sat", + Weekday::Sunday => "Sun", + } + } + + /// Returns the weekday from a number (0 = Monday, 6 = Sunday). + pub const fn from_number(n: u8) -> Option { + match n { + 0 => Some(Weekday::Monday), + 1 => Some(Weekday::Tuesday), + 2 => Some(Weekday::Wednesday), + 3 => Some(Weekday::Thursday), + 4 => Some(Weekday::Friday), + 5 => Some(Weekday::Saturday), + 6 => Some(Weekday::Sunday), + _ => None, + } + } +} + +/// Months of the year. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum Month { + /// January (1) + January = 1, + /// February (2) + February = 2, + /// March (3) + March = 3, + /// April (4) + April = 4, + /// May (5) + May = 5, + /// June (6) + June = 6, + /// July (7) + July = 7, + /// August (8) + August = 8, + /// September (9) + September = 9, + /// October (10) + October = 10, + /// November (11) + November = 11, + /// December (12) + December = 12, +} + +impl Month { + /// Returns the full name of the month (e.g., "January"). + pub const fn name(self) -> &'static str { + match self { + Month::January => "January", + Month::February => "February", + Month::March => "March", + Month::April => "April", + Month::May => "May", + Month::June => "June", + Month::July => "July", + Month::August => "August", + Month::September => "September", + Month::October => "October", + Month::November => "November", + Month::December => "December", + } + } + + /// Returns the short name of the month (e.g., "Jan"). + pub const fn short_name(self) -> &'static str { + match self { + Month::January => "Jan", + Month::February => "Feb", + Month::March => "Mar", + Month::April => "Apr", + Month::May => "May", + Month::June => "Jun", + Month::July => "Jul", + Month::August => "Aug", + Month::September => "Sep", + Month::October => "Oct", + Month::November => "Nov", + Month::December => "Dec", + } + } + + /// Returns the month from a number (1 = January, 12 = December). + pub const fn from_number(n: u8) -> Option { + match n { + 1 => Some(Month::January), + 2 => Some(Month::February), + 3 => Some(Month::March), + 4 => Some(Month::April), + 5 => Some(Month::May), + 6 => Some(Month::June), + 7 => Some(Month::July), + 8 => Some(Month::August), + 9 => Some(Month::September), + 10 => Some(Month::October), + 11 => Some(Month::November), + 12 => Some(Month::December), + _ => None, + } + } +} + +/// A broken-down date and time. +/// +/// This provides a human-readable representation of a point in time, +/// with year, month, day, hour, minute, second, and nanosecond components. +/// +/// # Example +/// +/// ```ignore +/// use hyperlight_guest_bin::time::DateTime; +/// +/// let dt = DateTime::now(); +/// // "Thursday 15th January 2026 15:34" +/// hl_print!("{} {} {} {} {:02}:{:02}", +/// dt.weekday().name(), +/// dt.day_ordinal(), +/// dt.month().name(), +/// dt.year(), +/// dt.hour(), +/// dt.minute()); +/// ``` +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct DateTime { + year: i32, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + nanosecond: u32, + weekday: u8, +} + +impl DateTime { + /// Creates a new DateTime from components. + /// + /// # Arguments + /// * `year` - Year (can be negative for BCE) + /// * `month` - Month (1-12) + /// * `day` - Day of month (1-31) + /// * `hour` - Hour (0-23) + /// * `minute` - Minute (0-59) + /// * `second` - Second (0-59) + /// * `nanosecond` - Nanosecond (0-999_999_999) + /// + /// Returns `None` if any component is out of range. + pub fn new( + year: i32, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + nanosecond: u32, + ) -> Option { + if !(1..=12).contains(&month) + || !(1..=31).contains(&day) + || hour > 23 + || minute > 59 + || second > 59 + || nanosecond > 999_999_999 + { + return None; + } + + // Validate day for the given month + let max_day = days_in_month(year, month); + if day > max_day { + return None; + } + + let weekday = day_of_week(year, month, day); + + Some(Self { + year, + month, + day, + hour, + minute, + second, + nanosecond, + weekday, + }) + } + + /// Creates a DateTime from nanoseconds since Unix epoch (UTC). + pub fn from_timestamp_nanos(nanos: u64) -> Self { + let total_secs = (nanos / NANOS_PER_SEC) as i64; + let ns = (nanos % NANOS_PER_SEC) as u32; + + let (year, month, day, hour, minute, second) = timestamp_to_datetime(total_secs); + let weekday = day_of_week(year, month, day); + + Self { + year, + month, + day, + hour, + minute, + second, + nanosecond: ns, + weekday, + } + } + + /// Creates a DateTime from a SystemTime. + pub fn from_system_time(time: SystemTime) -> Self { + Self::from_timestamp_nanos(time.0) + } + + /// Returns the current UTC time as a DateTime. + pub fn now() -> Self { + Self::from_system_time(SystemTime::now()) + } + + /// Returns the current local time as a DateTime. + /// + /// This uses the timezone offset captured at sandbox creation. + pub fn now_local() -> Self { + match local_time_ns() { + Some(ns) => Self::from_timestamp_nanos(ns), + None => Self::now(), // Fall back to UTC + } + } + + /// Returns the year. + pub const fn year(&self) -> i32 { + self.year + } + + /// Returns the month (1-12). + pub const fn month_number(&self) -> u8 { + self.month + } + + /// Returns the month as a Month enum. + pub fn month(&self) -> Month { + Month::from_number(self.month).unwrap_or(Month::January) + } + + /// Returns the day of month (1-31). + pub const fn day(&self) -> u8 { + self.day + } + + /// Returns the hour (0-23). + pub const fn hour(&self) -> u8 { + self.hour + } + + /// Returns the minute (0-59). + pub const fn minute(&self) -> u8 { + self.minute + } + + /// Returns the second (0-59). + pub const fn second(&self) -> u8 { + self.second + } + + /// Returns the nanosecond (0-999_999_999). + pub const fn nanosecond(&self) -> u32 { + self.nanosecond + } + + /// Returns the weekday as a Weekday enum. + pub fn weekday(&self) -> Weekday { + Weekday::from_number(self.weekday).unwrap_or(Weekday::Monday) + } + + /// Returns the day of year (1-366). + pub fn day_of_year(&self) -> u16 { + calc_day_of_year(self.year, self.month, self.day) + } + + /// Returns the day with an ordinal suffix (e.g., "1st", "2nd", "15th"). + pub fn day_ordinal(&self) -> &'static str { + ordinal_suffix(self.day) + } + + /// Returns hours in 12-hour format (1-12). + pub fn hour12(&self) -> u8 { + match self.hour { + 0 => 12, + 1..=12 => self.hour, + _ => self.hour - 12, + } + } + + /// Returns true if the time is PM (12:00-23:59). + pub fn is_pm(&self) -> bool { + self.hour >= 12 + } + + /// Returns "AM" or "PM". + pub fn am_pm(&self) -> &'static str { + if self.is_pm() { "PM" } else { "AM" } + } + + /// Returns "am" or "pm" (lowercase). + pub fn am_pm_lower(&self) -> &'static str { + if self.is_pm() { "pm" } else { "am" } + } + + /// Converts this DateTime to nanoseconds since Unix epoch. + /// + /// Returns `None` if the datetime is before the Unix epoch. + pub fn to_timestamp_nanos(&self) -> Option { + let secs = datetime_to_timestamp( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + )?; + if secs < 0 { + return None; + } + Some(secs as u64 * NANOS_PER_SEC + self.nanosecond as u64) + } +} + +// ============================================================================ +// Date/time calculation helpers - use shared implementations from hyperlight_guest +// ============================================================================ + +// Re-export shared date/time utilities +use guest_time::{ + NANOS_PER_SEC, datetime_to_timestamp, day_of_year as calc_day_of_year, days_in_month, + timestamp_to_datetime, +}; + +/// Returns the day of week (0 = Monday, 6 = Sunday) for a date. +#[inline] +fn day_of_week(year: i32, month: u8, day: u8) -> u8 { + guest_time::day_of_week_monday(year, month, day) +} + +/// Returns ordinal suffix for a day number (e.g., "1st", "2nd", "15th"). +const fn ordinal_suffix(day: u8) -> &'static str { + match day { + 1 => "1st", + 2 => "2nd", + 3 => "3rd", + 4 => "4th", + 5 => "5th", + 6 => "6th", + 7 => "7th", + 8 => "8th", + 9 => "9th", + 10 => "10th", + 11 => "11th", + 12 => "12th", + 13 => "13th", + 14 => "14th", + 15 => "15th", + 16 => "16th", + 17 => "17th", + 18 => "18th", + 19 => "19th", + 20 => "20th", + 21 => "21st", + 22 => "22nd", + 23 => "23rd", + 24 => "24th", + 25 => "25th", + 26 => "26th", + 27 => "27th", + 28 => "28th", + 29 => "29th", + 30 => "30th", + 31 => "31st", + _ => "th", + } +} diff --git a/src/hyperlight_guest_capi/Cargo.toml b/src/hyperlight_guest_capi/Cargo.toml index bd580b468..2d29a24ad 100644 --- a/src/hyperlight_guest_capi/Cargo.toml +++ b/src/hyperlight_guest_capi/Cargo.toml @@ -11,6 +11,11 @@ crate-type = ["staticlib"] [lints] workspace = true +[features] +default = ["guest_time"] +# Enable paravirtualized clock support for guest time functions +guest_time = ["hyperlight-guest/guest_time", "hyperlight-guest-bin/guest_time"] + [dependencies] hyperlight-guest = { workspace = true, default-features = false } hyperlight-guest-bin = { workspace = true, default-features = true } diff --git a/src/hyperlight_guest_capi/src/lib.rs b/src/hyperlight_guest_capi/src/lib.rs index fcf9aa2c1..c0186f501 100644 --- a/src/hyperlight_guest_capi/src/lib.rs +++ b/src/hyperlight_guest_capi/src/lib.rs @@ -23,4 +23,6 @@ pub mod dispatch; pub mod error; pub mod flatbuffer; pub mod logging; +#[cfg(feature = "guest_time")] +pub mod time; pub mod types; diff --git a/src/hyperlight_guest_capi/src/time.rs b/src/hyperlight_guest_capi/src/time.rs new file mode 100644 index 000000000..48e0ecd13 --- /dev/null +++ b/src/hyperlight_guest_capi/src/time.rs @@ -0,0 +1,812 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +//! C API for time functions. +//! +//! Provides POSIX-compatible `gettimeofday` and `clock_gettime` functions +//! using the paravirtualized clock. + +use core::ffi::c_int; + +use hyperlight_guest::time::{monotonic_time_ns, utc_offset_seconds, wall_clock_time_ns}; +use hyperlight_guest_bin::GUEST_HANDLE; + +/// POSIX timeval structure. +#[repr(C)] +pub struct timeval { + /// Seconds since Unix epoch. + pub tv_sec: i64, + /// Microseconds (0-999999). + pub tv_usec: i64, +} + +/// POSIX timespec structure. +#[repr(C)] +pub struct timespec { + /// Seconds since Unix epoch (for wall clock) or since boot (for monotonic). + pub tv_sec: i64, + /// Nanoseconds (0-999999999). + pub tv_nsec: i64, +} + +/// POSIX timezone structure (deprecated, included for compatibility). +/// +/// The `tz_minuteswest` and `tz_dsttime` fields can be populated using the +/// `utc_offset_seconds` value from the clock region, which captures the host's +/// timezone offset at sandbox creation time. +#[repr(C)] +pub struct timezone { + /// Minutes west of Greenwich. + pub tz_minuteswest: c_int, + /// Type of DST correction. + pub tz_dsttime: c_int, +} + +// Clock IDs for clock_gettime +/// System-wide real-time clock (wall clock). +pub const CLOCK_REALTIME: c_int = 0; +/// Monotonic clock that cannot be set. +pub const CLOCK_MONOTONIC: c_int = 1; +/// High-resolution per-process timer from the CPU (not supported). +pub const CLOCK_PROCESS_CPUTIME_ID: c_int = 2; +/// Thread-specific CPU-time clock (not supported). +pub const CLOCK_THREAD_CPUTIME_ID: c_int = 3; +/// Like CLOCK_MONOTONIC but includes time spent in suspend. +pub const CLOCK_BOOTTIME: c_int = 7; +/// Faster but less precise version of CLOCK_REALTIME. +pub const CLOCK_REALTIME_COARSE: c_int = 5; +/// Faster but less precise version of CLOCK_MONOTONIC. +pub const CLOCK_MONOTONIC_COARSE: c_int = 6; + +/// Get the current wall-clock time (UTC). +/// +/// This is a POSIX-compatible implementation of `gettimeofday(2)`. +/// Returns UTC time as seconds since 1970-01-01 00:00:00. +/// +/// # Arguments +/// * `tv` - Pointer to a `timeval` struct to fill with the current time. +/// * `tz` - Optional pointer to a `timezone` struct. If provided, will be +/// populated with the timezone offset that was captured at sandbox creation. +/// Note: The `tz_dsttime` field is always set to 0 (DST info not available). +/// +/// # Returns +/// * `0` on success +/// * `-1` on error (clock not available or null pointer) +/// +/// # Safety +/// The `tv` and `tz` pointers must be valid and properly aligned if not null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn gettimeofday(tv: *mut timeval, tz: *mut timezone) -> c_int { + if tv.is_null() { + return -1; + } + + // SAFETY: GUEST_HANDLE is initialized during entrypoint, we are single-threaded + #[allow(static_mut_refs)] + let handle = unsafe { &GUEST_HANDLE }; + + match wall_clock_time_ns(handle) { + Some(ns) => { + let secs = (ns / 1_000_000_000) as i64; + let usecs = ((ns % 1_000_000_000) / 1_000) as i64; + + // SAFETY: Caller guarantees tv is valid + unsafe { + (*tv).tv_sec = secs; + (*tv).tv_usec = usecs; + } + + // Populate timezone if requested + if !tz.is_null() { + let offset_secs = utc_offset_seconds(handle).unwrap_or(0); + // Convert seconds east of UTC to minutes west of UTC + let minutes_west = -(offset_secs / 60) as c_int; + // SAFETY: Caller guarantees tz is valid if not null + unsafe { + (*tz).tz_minuteswest = minutes_west; + (*tz).tz_dsttime = 0; // DST info not available + } + } + + 0 + } + None => -1, + } +} + +/// Get the time of a specified clock. +/// +/// This is a POSIX-compatible implementation of `clock_gettime(2)`. +/// +/// # Supported Clocks +/// * `CLOCK_REALTIME` / `CLOCK_REALTIME_COARSE` - Wall-clock time (UTC, seconds since 1970-01-01 00:00:00) +/// * `CLOCK_MONOTONIC` / `CLOCK_MONOTONIC_COARSE` / `CLOCK_BOOTTIME` - Time since sandbox creation +/// +/// # Unsupported Clocks (return -1) +/// * `CLOCK_PROCESS_CPUTIME_ID` - Process CPU time not available +/// * `CLOCK_THREAD_CPUTIME_ID` - Thread CPU time not available +/// +/// # Arguments +/// * `clk_id` - The clock to query. +/// * `tp` - Pointer to a `timespec` struct to fill with the current time. +/// +/// # Returns +/// * `0` on success +/// * `-1` on error (invalid clock ID, clock not available, or null pointer) +/// +/// # Safety +/// The `tp` pointer must be valid and properly aligned if not null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn clock_gettime(clk_id: c_int, tp: *mut timespec) -> c_int { + if tp.is_null() { + return -1; + } + + // SAFETY: GUEST_HANDLE is initialized during entrypoint, we are single-threaded + #[allow(static_mut_refs)] + let handle = unsafe { &GUEST_HANDLE }; + + let ns_result = match clk_id { + CLOCK_REALTIME | CLOCK_REALTIME_COARSE => wall_clock_time_ns(handle), + CLOCK_MONOTONIC | CLOCK_MONOTONIC_COARSE | CLOCK_BOOTTIME => monotonic_time_ns(handle), + // CPU time clocks are not supported in the guest + CLOCK_PROCESS_CPUTIME_ID | CLOCK_THREAD_CPUTIME_ID => return -1, + _ => return -1, // Invalid clock ID + }; + + match ns_result { + Some(ns) => { + let secs = (ns / 1_000_000_000) as i64; + let nsecs = (ns % 1_000_000_000) as i64; + + // SAFETY: Caller guarantees tp is valid + unsafe { + (*tp).tv_sec = secs; + (*tp).tv_nsec = nsecs; + } + 0 + } + None => -1, + } +} + +/// Get the resolution (precision) of a specified clock. +/// +/// This is a POSIX-compatible implementation of `clock_getres(2)`. +/// +/// # Arguments +/// * `clk_id` - The clock to query. +/// * `res` - Pointer to a `timespec` struct to fill with the resolution. +/// +/// # Returns +/// * `0` on success +/// * `-1` on error (invalid clock ID or null pointer) +/// +/// # Safety +/// The `res` pointer must be valid and properly aligned if not null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn clock_getres(clk_id: c_int, res: *mut timespec) -> c_int { + // Validate clock ID - only supported clocks + match clk_id { + CLOCK_REALTIME + | CLOCK_REALTIME_COARSE + | CLOCK_MONOTONIC + | CLOCK_MONOTONIC_COARSE + | CLOCK_BOOTTIME => {} + // CPU time clocks are not supported + CLOCK_PROCESS_CPUTIME_ID | CLOCK_THREAD_CPUTIME_ID => return -1, + _ => return -1, + } + + if res.is_null() { + // POSIX allows res to be NULL, just validate clock ID + return 0; + } + + // Return fixed 1ns resolution + // SAFETY: Caller guarantees res is valid + unsafe { + (*res).tv_sec = 0; + (*res).tv_nsec = 1; + } + 0 +} + +/// Get the current time in seconds since Unix epoch. +/// +/// This is a simplified time function compatible with C's `time()`. +/// +/// # Arguments +/// * `tloc` - Optional pointer to store the time. Can be NULL. +/// +/// # Returns +/// * Seconds since Unix epoch on success +/// * `-1` on error (clock not available) +/// +/// # Safety +/// If `tloc` is not null, it must be valid and properly aligned. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn time(tloc: *mut i64) -> i64 { + // SAFETY: GUEST_HANDLE is initialized during entrypoint, we are single-threaded + #[allow(static_mut_refs)] + let handle = unsafe { &GUEST_HANDLE }; + + match wall_clock_time_ns(handle) { + Some(ns) => { + let secs = (ns / 1_000_000_000) as i64; + if !tloc.is_null() { + // SAFETY: Caller guarantees tloc is valid if not null + unsafe { + *tloc = secs; + } + } + secs + } + None => -1, + } +} + +/// Get the UTC offset in seconds that was captured at sandbox creation. +/// +/// This returns the host's local timezone offset from UTC. Positive values +/// are east of UTC (e.g., +3600 for UTC+1), negative values are west (e.g., +/// -18000 for UTC-5/EST). +/// +/// # Arguments +/// * `offset` - Pointer to store the UTC offset in seconds. +/// +/// # Returns +/// * `0` on success +/// * `-1` on error (clock not available or null pointer) +/// +/// # Safety +/// The `offset` pointer must be valid and properly aligned if not null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn hl_get_utc_offset(offset: *mut c_int) -> c_int { + if offset.is_null() { + return -1; + } + + // SAFETY: GUEST_HANDLE is initialized during entrypoint, we are single-threaded + #[allow(static_mut_refs)] + let handle = unsafe { &GUEST_HANDLE }; + + match utc_offset_seconds(handle) { + Some(secs) => { + // SAFETY: Caller guarantees offset is valid + unsafe { + *offset = secs; + } + 0 + } + None => -1, + } +} + +// ============================================================================ +// Broken-down time (struct tm) and related functions +// ============================================================================ + +/// POSIX tm structure for broken-down time. +/// +/// This structure is compatible with the standard C `struct tm`. +#[repr(C)] +pub struct tm { + /// Seconds after the minute (0-60, 60 for leap second) + pub tm_sec: c_int, + /// Minutes after the hour (0-59) + pub tm_min: c_int, + /// Hours since midnight (0-23) + pub tm_hour: c_int, + /// Day of the month (1-31) + pub tm_mday: c_int, + /// Months since January (0-11) + pub tm_mon: c_int, + /// Years since 1900 + pub tm_year: c_int, + /// Days since Sunday (0-6, Sunday = 0) + pub tm_wday: c_int, + /// Days since January 1 (0-365) + pub tm_yday: c_int, + /// Daylight Saving Time flag (positive if DST, 0 if not, negative if unknown) + pub tm_isdst: c_int, +} + +/// Convert a timestamp to broken-down UTC time. +/// +/// This is a POSIX-compatible implementation of `gmtime_r(3)`. +/// +/// # Arguments +/// * `timep` - Pointer to a time_t (seconds since Unix epoch). +/// * `result` - Pointer to a `tm` struct to fill with the broken-down time. +/// +/// # Returns +/// * Pointer to `result` on success +/// * NULL on error (null pointer) +/// +/// # Safety +/// Both pointers must be valid and properly aligned if not null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn gmtime_r(timep: *const i64, result: *mut tm) -> *mut tm { + if timep.is_null() || result.is_null() { + return core::ptr::null_mut(); + } + + let secs = unsafe { *timep }; + let (year, month, day, hour, minute, second) = timestamp_to_datetime(secs); + + unsafe { + (*result).tm_sec = second as c_int; + (*result).tm_min = minute as c_int; + (*result).tm_hour = hour as c_int; + (*result).tm_mday = day as c_int; + (*result).tm_mon = (month - 1) as c_int; // 0-11 + (*result).tm_year = (year - 1900) as c_int; + (*result).tm_wday = day_of_week_sunday(year, month, day) as c_int; + (*result).tm_yday = (day_of_year(year, month, day) - 1) as c_int; // 0-365 + (*result).tm_isdst = 0; // UTC has no DST + } + + result +} + +/// Convert a timestamp to broken-down local time. +/// +/// This is a POSIX-compatible implementation of `localtime_r(3)`. +/// Uses the timezone offset captured at sandbox creation. +/// +/// # Arguments +/// * `timep` - Pointer to a time_t (seconds since Unix epoch in UTC). +/// * `result` - Pointer to a `tm` struct to fill with the broken-down time. +/// +/// # Returns +/// * Pointer to `result` on success +/// * NULL on error (null pointer or clock not available) +/// +/// # Safety +/// Both pointers must be valid and properly aligned if not null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn localtime_r(timep: *const i64, result: *mut tm) -> *mut tm { + if timep.is_null() || result.is_null() { + return core::ptr::null_mut(); + } + + // SAFETY: GUEST_HANDLE is initialized during entrypoint, we are single-threaded + #[allow(static_mut_refs)] + let handle = unsafe { &GUEST_HANDLE }; + + let offset = utc_offset_seconds(handle).unwrap_or(0) as i64; + let local_secs = unsafe { *timep } + offset; + + let (year, month, day, hour, minute, second) = timestamp_to_datetime(local_secs); + + unsafe { + (*result).tm_sec = second as c_int; + (*result).tm_min = minute as c_int; + (*result).tm_hour = hour as c_int; + (*result).tm_mday = day as c_int; + (*result).tm_mon = (month - 1) as c_int; // 0-11 + (*result).tm_year = (year - 1900) as c_int; + (*result).tm_wday = day_of_week_sunday(year, month, day) as c_int; + (*result).tm_yday = (day_of_year(year, month, day) - 1) as c_int; // 0-365 + (*result).tm_isdst = -1; // DST unknown + } + + result +} + +/// Convert broken-down time to timestamp. +/// +/// This is a POSIX-compatible implementation of `mktime(3)`. +/// Interprets the tm struct as local time. +/// +/// # Arguments +/// * `timeptr` - Pointer to a `tm` struct with the broken-down time. +/// +/// # Returns +/// * Seconds since Unix epoch on success +/// * `-1` on error (null pointer or invalid date) +/// +/// # Safety +/// The pointer must be valid and properly aligned if not null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn mktime(timeptr: *mut tm) -> i64 { + if timeptr.is_null() { + return -1; + } + + let t = unsafe { &mut *timeptr }; + + let year = t.tm_year + 1900; + let month = (t.tm_mon + 1) as u8; + let day = t.tm_mday as u8; + let hour = t.tm_hour as u8; + let minute = t.tm_min as u8; + let second = t.tm_sec as u8; + + // Calculate timestamp (as local time) + let local_secs = match datetime_to_timestamp(year, month, day, hour, minute, second) { + Some(s) => s, + None => return -1, + }; + + // Adjust for timezone to get UTC + // SAFETY: GUEST_HANDLE is initialized during entrypoint, we are single-threaded + #[allow(static_mut_refs)] + let handle = unsafe { &GUEST_HANDLE }; + + let offset = utc_offset_seconds(handle).unwrap_or(0) as i64; + let utc_secs = local_secs - offset; + + // Update the tm struct with normalized values + let (year, month, day, hour, minute, second) = timestamp_to_datetime(local_secs); + t.tm_sec = second as c_int; + t.tm_min = minute as c_int; + t.tm_hour = hour as c_int; + t.tm_mday = day as c_int; + t.tm_mon = (month - 1) as c_int; + t.tm_year = (year - 1900) as c_int; + t.tm_wday = day_of_week_sunday(year, month, day) as c_int; + t.tm_yday = (day_of_year(year, month, day) - 1) as c_int; + + utc_secs +} + +/// Convert broken-down UTC time to timestamp. +/// +/// This is a POSIX-compatible implementation of `timegm(3)`. +/// Interprets the tm struct as UTC time. +/// +/// # Arguments +/// * `timeptr` - Pointer to a `tm` struct with the broken-down time. +/// +/// # Returns +/// * Seconds since Unix epoch on success +/// * `-1` on error (null pointer or invalid date) +/// +/// # Safety +/// The pointer must be valid and properly aligned if not null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn timegm(timeptr: *mut tm) -> i64 { + if timeptr.is_null() { + return -1; + } + + let t = unsafe { &mut *timeptr }; + + let year = t.tm_year + 1900; + let month = (t.tm_mon + 1) as u8; + let day = t.tm_mday as u8; + let hour = t.tm_hour as u8; + let minute = t.tm_min as u8; + let second = t.tm_sec as u8; + + match datetime_to_timestamp(year, month, day, hour, minute, second) { + Some(secs) => { + // Update the tm struct with normalized values and weekday/yearday + t.tm_wday = day_of_week_sunday(year, month, day) as c_int; + t.tm_yday = (day_of_year(year, month, day) - 1) as c_int; + secs + } + None => -1, + } +} + +/// Format time according to a format string. +/// +/// This is a POSIX-compatible implementation of `strftime(3)`. +/// +/// # Supported Format Specifiers +/// * `%a` - Abbreviated weekday name (Sun-Sat) +/// * `%A` - Full weekday name (Sunday-Saturday) +/// * `%b` - Abbreviated month name (Jan-Dec) +/// * `%B` - Full month name (January-December) +/// * `%d` - Day of month (01-31) +/// * `%e` - Day of month, space-padded ( 1-31) +/// * `%H` - Hour in 24h format (00-23) +/// * `%I` - Hour in 12h format (01-12) +/// * `%j` - Day of year (001-366) +/// * `%m` - Month as decimal (01-12) +/// * `%M` - Minute (00-59) +/// * `%p` - AM or PM +/// * `%P` - am or pm +/// * `%S` - Second (00-59) +/// * `%u` - Day of week (1-7, Monday = 1) +/// * `%w` - Day of week (0-6, Sunday = 0) +/// * `%y` - Year without century (00-99) +/// * `%Y` - Year with century +/// * `%z` - Timezone offset (+0000) +/// * `%Z` - Timezone name (always "UTC" or "LOCAL") +/// * `%%` - Literal % +/// * `%n` - Newline +/// * `%t` - Tab +/// +/// # Arguments +/// * `s` - Output buffer. +/// * `maxsize` - Maximum bytes to write (including null terminator). +/// * `format` - Format string. +/// * `timeptr` - Pointer to a `tm` struct. +/// +/// # Returns +/// * Number of bytes written (excluding null terminator) on success +/// * `0` if the buffer is too small or on error +/// +/// # Safety +/// All pointers must be valid. `s` must have at least `maxsize` bytes available. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn strftime( + s: *mut u8, + maxsize: usize, + format: *const u8, + timeptr: *const tm, +) -> usize { + if s.is_null() || format.is_null() || timeptr.is_null() || maxsize == 0 { + return 0; + } + + let t = unsafe { &*timeptr }; + let mut writer = StrftimeWriter::new(s, maxsize); + + // Get format string length + let mut fmt_len = 0; + while unsafe { *format.add(fmt_len) } != 0 { + fmt_len += 1; + } + + let mut fmt_pos = 0; + while fmt_pos < fmt_len { + let c = unsafe { *format.add(fmt_pos) }; + fmt_pos += 1; + + if c != b'%' { + if !writer.write_byte(c) { + return 0; + } + continue; + } + + // Handle format specifier + if fmt_pos >= fmt_len { + break; + } + + let spec = unsafe { *format.add(fmt_pos) }; + fmt_pos += 1; + + let success = match spec { + b'%' => writer.write_byte(b'%'), + b'n' => writer.write_byte(b'\n'), + b't' => writer.write_byte(b'\t'), + b'a' => writer.write_bytes(short_weekday_name(t.tm_wday)), + b'A' => writer.write_bytes(full_weekday_name(t.tm_wday)), + b'b' | b'h' => writer.write_bytes(short_month_name(t.tm_mon)), + b'B' => writer.write_bytes(full_month_name(t.tm_mon)), + b'd' => writer.write_num_padded(t.tm_mday, 2, b'0'), + b'e' => writer.write_num_padded(t.tm_mday, 2, b' '), + b'H' => writer.write_num_padded(t.tm_hour, 2, b'0'), + b'I' => { + let h = match t.tm_hour { + 0 => 12, + 1..=12 => t.tm_hour, + _ => t.tm_hour - 12, + }; + writer.write_num_padded(h, 2, b'0') + } + b'j' => writer.write_num_padded(t.tm_yday + 1, 3, b'0'), + b'm' => writer.write_num_padded(t.tm_mon + 1, 2, b'0'), + b'M' => writer.write_num_padded(t.tm_min, 2, b'0'), + b'p' => writer.write_bytes(if t.tm_hour >= 12 { b"PM" } else { b"AM" }), + b'P' => writer.write_bytes(if t.tm_hour >= 12 { b"pm" } else { b"am" }), + b'S' => writer.write_num_padded(t.tm_sec, 2, b'0'), + b'u' => { + // Monday = 1, Sunday = 7 + let day = if t.tm_wday == 0 { 7 } else { t.tm_wday }; + writer.write_num_padded(day, 1, b'0') + } + b'w' => writer.write_num_padded(t.tm_wday, 1, b'0'), + b'y' => writer.write_num_padded((t.tm_year + 1900) % 100, 2, b'0'), + b'Y' => writer.write_num_padded(t.tm_year + 1900, 4, b'0'), + b'z' => { + // Timezone offset + // SAFETY: GUEST_HANDLE is initialized during entrypoint + #[allow(static_mut_refs)] + let handle = unsafe { &GUEST_HANDLE }; + let offset = utc_offset_seconds(handle).unwrap_or(0); + let (sign, abs_offset) = if offset >= 0 { + (b'+', offset as u32) + } else { + (b'-', (-offset) as u32) + }; + let hours = (abs_offset / 3600) as i32; + let mins = ((abs_offset % 3600) / 60) as i32; + writer.write_byte(sign) + && writer.write_num_padded(hours, 2, b'0') + && writer.write_num_padded(mins, 2, b'0') + } + b'Z' => { + // Timezone name (simplified) + if t.tm_isdst == 0 { + writer.write_bytes(b"UTC") + } else { + writer.write_bytes(b"LOCAL") + } + } + _ => writer.write_byte(b'%') && writer.write_byte(spec), // Unknown specifier, output as-is + }; + + if !success { + return 0; + } + } + + // Null terminate + writer.null_terminate(); + writer.len() +} + +/// Helper struct for strftime output. +struct StrftimeWriter { + buf: *mut u8, + maxsize: usize, + pos: usize, +} + +impl StrftimeWriter { + fn new(buf: *mut u8, maxsize: usize) -> Self { + Self { + buf, + maxsize, + pos: 0, + } + } + + fn len(&self) -> usize { + self.pos + } + + fn write_byte(&mut self, b: u8) -> bool { + if self.pos + 1 >= self.maxsize { + return false; + } + unsafe { *self.buf.add(self.pos) = b }; + self.pos += 1; + true + } + + fn write_bytes(&mut self, bytes: &[u8]) -> bool { + if self.pos + bytes.len() >= self.maxsize { + return false; + } + for &b in bytes { + unsafe { *self.buf.add(self.pos) = b }; + self.pos += 1; + } + true + } + + fn write_num_padded(&mut self, n: c_int, width: usize, pad: u8) -> bool { + let mut buf = [0u8; 16]; + let mut num = n.unsigned_abs(); + let mut pos = buf.len(); + + loop { + pos -= 1; + buf[pos] = b'0' + (num % 10) as u8; + num /= 10; + if num == 0 { + break; + } + } + + let digits = buf.len() - pos; + let padding = if width > digits { width - digits } else { 0 }; + + // Handle negative numbers + if n < 0 && !self.write_byte(b'-') { + return false; + } + + // Add padding + for _ in 0..padding { + if !self.write_byte(pad) { + return false; + } + } + + self.write_bytes(&buf[pos..]) + } + + fn null_terminate(&mut self) { + if self.pos < self.maxsize { + unsafe { *self.buf.add(self.pos) = 0 }; + } + } +} + +// ============================================================================ +// Date/time calculation helpers - use shared implementations from hyperlight_guest +// ============================================================================ + +// Re-export shared date/time utilities +use hyperlight_guest::time::{ + datetime_to_timestamp, day_of_week_sunday, day_of_year, timestamp_to_datetime, +}; + +/// Short weekday name from tm_wday (0=Sunday). +const fn short_weekday_name(wday: c_int) -> &'static [u8] { + match wday { + 0 => b"Sun", + 1 => b"Mon", + 2 => b"Tue", + 3 => b"Wed", + 4 => b"Thu", + 5 => b"Fri", + 6 => b"Sat", + _ => b"???", + } +} + +/// Full weekday name from tm_wday (0=Sunday). +const fn full_weekday_name(wday: c_int) -> &'static [u8] { + match wday { + 0 => b"Sunday", + 1 => b"Monday", + 2 => b"Tuesday", + 3 => b"Wednesday", + 4 => b"Thursday", + 5 => b"Friday", + 6 => b"Saturday", + _ => b"???", + } +} + +/// Short month name from tm_mon (0=January). +const fn short_month_name(mon: c_int) -> &'static [u8] { + match mon { + 0 => b"Jan", + 1 => b"Feb", + 2 => b"Mar", + 3 => b"Apr", + 4 => b"May", + 5 => b"Jun", + 6 => b"Jul", + 7 => b"Aug", + 8 => b"Sep", + 9 => b"Oct", + 10 => b"Nov", + 11 => b"Dec", + _ => b"???", + } +} + +/// Full month name from tm_mon (0=January). +const fn full_month_name(mon: c_int) -> &'static [u8] { + match mon { + 0 => b"January", + 1 => b"February", + 2 => b"March", + 3 => b"April", + 4 => b"May", + 5 => b"June", + 6 => b"July", + 7 => b"August", + 8 => b"September", + 9 => b"October", + 10 => b"November", + 11 => b"December", + _ => b"???", + } +} diff --git a/src/hyperlight_host/Cargo.toml b/src/hyperlight_host/Cargo.toml index cf9f07245..431d7ced0 100644 --- a/src/hyperlight_host/Cargo.toml +++ b/src/hyperlight_host/Cargo.toml @@ -66,6 +66,7 @@ windows = { version = "0.62", features = [ "Win32_System_Threading", "Win32_System_JobObjects", "Win32_System_SystemServices", + "Win32_System_Time", ] } windows-sys = { version = "0.61", features = ["Win32"] } windows-result = "0.4" @@ -122,8 +123,10 @@ cfg_aliases = "0.2.1" built = { version = "0.8.0", optional = true, features = ["chrono", "git2"] } [features] -default = ["kvm", "mshv3", "build-metadata", "init-paging"] +default = ["kvm", "mshv3", "build-metadata", "init-paging", "guest_time"] function_call_metrics = [] +# Enable paravirtualized clock support for guest time functions +guest_time = ["hyperlight-common/guest_time"] executable_heap = [] # This feature enables printing of debug information to stdout in debug builds print_debug = [] diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs index c836d3d80..10d52dedd 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs @@ -25,6 +25,7 @@ use std::sync::atomic::AtomicU8; use std::sync::atomic::AtomicU64; use std::sync::{Arc, Mutex}; +use hyperlight_common::time::ClockType; use log::LevelFilter; use tracing::{Span, instrument}; #[cfg(feature = "trace_guest")] @@ -212,6 +213,42 @@ impl HyperlightVm { Ok(ret) } + /// Setup paravirtualized clock for the guest. + /// + /// This configures the hypervisor to provide time information to the guest + /// via a shared memory page at the given GPA. The clock type is determined + /// by the hypervisor in use. + /// + /// # Arguments + /// * `clock_page_gpa` - Guest physical address of the clock page (must be 4KB aligned) + /// + /// # Returns + /// * `ClockType` indicating which clock format was configured + #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] + pub(crate) fn setup_pvclock_for_guest(&mut self, clock_page_gpa: u64) -> Result { + // Setup the pvclock in the hypervisor + self.vm.setup_pvclock(clock_page_gpa)?; + + // Determine clock type based on hypervisor + let clock_type = match get_available_hypervisor() { + #[cfg(kvm)] + Some(HypervisorType::Kvm) => ClockType::KvmPvclock, + #[cfg(mshv3)] + Some(HypervisorType::Mshv) => ClockType::HyperVReferenceTsc, + #[cfg(target_os = "windows")] + Some(HypervisorType::Whp) => ClockType::HyperVReferenceTsc, + None => ClockType::None, + }; + + log::debug!( + "Configured pvclock: type={:?}, page_gpa={:#x}", + clock_type, + clock_page_gpa + ); + + Ok(clock_type) + } + /// Initialise the internally stored vCPU with the given PEB address and /// random number seed, then run it until a HLT instruction. #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm.rs index b5ca402fe..89c1e9954 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm.rs @@ -18,7 +18,9 @@ use std::sync::LazyLock; #[cfg(gdb)] use kvm_bindings::kvm_guest_debug; -use kvm_bindings::{kvm_fpu, kvm_regs, kvm_sregs, kvm_userspace_memory_region}; +use kvm_bindings::{ + Msrs, kvm_fpu, kvm_msr_entry, kvm_regs, kvm_sregs, kvm_userspace_memory_region, +}; use kvm_ioctls::Cap::UserMemory; use kvm_ioctls::{Kvm, VcpuExit, VcpuFd, VmFd}; use tracing::{Span, instrument}; @@ -30,6 +32,11 @@ use crate::hypervisor::virtual_machine::{VirtualMachine, VmExit}; use crate::mem::memory_region::MemoryRegion; use crate::{Result, new_error}; +/// MSR for KVM pvclock system time (new version) +/// This MSR enables paravirtualized time for the guest. +/// The value written is the GPA of the pvclock structure with bit 0 set to enable. +const MSR_KVM_SYSTEM_TIME_NEW: u32 = 0x4b564d01; + /// Return `true` if the KVM API is available, version 12, and has UserMemory capability, or `false` otherwise #[instrument(skip_all, parent = Span::current(), level = "Trace")] pub(crate) fn is_hypervisor_present() -> bool { @@ -170,6 +177,32 @@ impl VirtualMachine for KvmVm { .flat_map(u32::to_le_bytes) .collect()) } + + fn setup_pvclock(&mut self, clock_page_gpa: u64) -> Result<()> { + // Enable KVM pvclock by writing the MSR_KVM_SYSTEM_TIME_NEW MSR. + // The value is the GPA of the pvclock structure with bit 0 set to enable. + let msr_value = clock_page_gpa | 1; // bit 0 = enable + + let mut msrs = Msrs::new(1).map_err(|e| new_error!("Failed to create MSRs: {}", e))?; + let entries = msrs.as_mut_slice(); + entries[0] = kvm_msr_entry { + index: MSR_KVM_SYSTEM_TIME_NEW, + data: msr_value, + ..Default::default() + }; + + self.vcpu_fd + .set_msrs(&msrs) + .map_err(|e| new_error!("Failed to set pvclock MSR: {}", e))?; + + log::debug!( + "KVM pvclock enabled at GPA {:#x} (MSR value: {:#x})", + clock_page_gpa, + msr_value + ); + + Ok(()) + } } #[cfg(gdb)] diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs index 55af78ebb..aaa04b03f 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs @@ -169,6 +169,23 @@ pub(crate) trait VirtualMachine: Debug + Send { #[cfg(crashdump)] fn xsave(&self) -> Result>; + /// Setup paravirtualized clock for the guest. + /// + /// This configures the hypervisor to provide time information to the guest + /// via a shared memory page. The guest can read time without VM exits. + /// + /// # Arguments + /// * `clock_page_gpa` - Guest physical address of the clock page (must be 4KB aligned) + /// + /// # Returns + /// * `Ok(())` if clock was successfully configured + /// * `Err` if the hypervisor doesn't support pvclock or configuration failed + fn setup_pvclock(&mut self, _clock_page_gpa: u64) -> Result<()> { + Err(crate::new_error!( + "Paravirtualized clock setup not implemented for this hypervisor", + )) + } + /// Get partition handle #[cfg(target_os = "windows")] fn partition_handle(&self) -> windows::Win32::System::Hypervisor::WHV_PARTITION_HANDLE; diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mshv.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv.rs index c0e352604..cbe9e088d 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/mshv.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv.rs @@ -26,7 +26,8 @@ use mshv_bindings::{ hv_message_type_HVMSG_X64_HALT, hv_message_type_HVMSG_X64_IO_PORT_INTERCEPT, hv_partition_property_code_HV_PARTITION_PROPERTY_SYNTHETIC_PROC_FEATURES, hv_partition_synthetic_processor_features, hv_register_assoc, - hv_register_name_HV_X64_REGISTER_RIP, hv_register_value, mshv_user_mem_region, + hv_register_name_HV_REGISTER_REFERENCE_TSC, hv_register_name_HV_X64_REGISTER_RIP, + hv_register_value, mshv_user_mem_region, }; use mshv_ioctls::{Mshv, VcpuFd, VmFd}; use tracing::{Span, instrument}; @@ -214,6 +215,28 @@ impl VirtualMachine for MshvVm { let xsave = self.vcpu_fd.get_xsave()?; Ok(xsave.buffer.to_vec()) } + + fn setup_pvclock(&mut self, clock_page_gpa: u64) -> Result<()> { + // Enable Hyper-V Reference TSC by setting the HV_REGISTER_REFERENCE_TSC register. + // The value is the GPA of the Reference TSC page with bit 0 set to enable. + let reg_value = clock_page_gpa | 1; // bit 0 = enable + + self.vcpu_fd + .set_reg(&[hv_register_assoc { + name: hv_register_name_HV_REGISTER_REFERENCE_TSC, + value: hv_register_value { reg64: reg_value }, + ..Default::default() + }]) + .map_err(|e| new_error!("Failed to set Reference TSC register: {}", e))?; + + log::debug!( + "MSHV Reference TSC enabled at GPA {:#x} (reg value: {:#x})", + clock_page_gpa, + reg_value + ); + + Ok(()) + } } #[cfg(gdb)] diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/whp.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp.rs index 7effa3e9f..a6f478577 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/whp.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/whp.rs @@ -448,6 +448,24 @@ impl VirtualMachine for WhpVm { fn partition_handle(&self) -> WHV_PARTITION_HANDLE { self.partition } + + fn setup_pvclock(&mut self, clock_page_gpa: u64) -> Result<()> { + // Enable Hyper-V Reference TSC by setting the WHvRegisterReferenceTsc register. + // The value is the GPA of the Reference TSC page with bit 0 set to enable. + let reg_value = clock_page_gpa | 1; // bit 0 = enable + + let reg = WHV_REGISTER_VALUE { Reg64: reg_value }; + + self.set_registers(&[(WHvRegisterReferenceTsc, Align16(reg))])?; + + log::debug!( + "WHP Reference TSC enabled at GPA {:#x} (reg value: {:#x})", + clock_page_gpa, + reg_value + ); + + Ok(()) + } } #[cfg(gdb)] diff --git a/src/hyperlight_host/src/mem/layout.rs b/src/hyperlight_host/src/mem/layout.rs index 13e78692e..bdab1c6dd 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -74,13 +74,15 @@ use std::fmt::Debug; use std::mem::{offset_of, size_of}; use hyperlight_common::mem::{GuestMemoryRegion, HyperlightPEB, PAGE_SIZE_USIZE}; +use hyperlight_common::time::GuestClockRegion; use rand::{RngCore, rng}; use tracing::{Span, instrument}; #[cfg(feature = "init-paging")] use super::memory_region::MemoryRegionType::PageTables; use super::memory_region::MemoryRegionType::{ - Code, GuardPage, Heap, HostFunctionDefinitions, InitData, InputData, OutputData, Peb, Stack, + ClockPage, Code, GuardPage, Heap, HostFunctionDefinitions, InitData, InputData, OutputData, + Peb, Stack, }; use super::memory_region::{ DEFAULT_GUEST_BLOB_MEM_FLAGS, MemoryRegion, MemoryRegion_, MemoryRegionFlags, MemoryRegionKind, @@ -111,12 +113,15 @@ pub(crate) struct SandboxMemoryLayout { peb_init_data_offset: usize, peb_heap_data_offset: usize, peb_guest_stack_data_offset: usize, + peb_guest_clock_offset: usize, // The following are the actual values // that are written to the PEB struct pub(crate) host_function_definitions_buffer_offset: usize, pub(super) input_data_buffer_offset: usize, pub(super) output_data_buffer_offset: usize, + /// Offset to the clock page (4KB page for pvclock/Reference TSC) + pub(crate) clock_page_offset: usize, guest_heap_buffer_offset: usize, guard_page_offset: usize, guest_user_stack_buffer_offset: usize, // the lowest address of the user stack @@ -180,6 +185,10 @@ impl Debug for SandboxMemoryLayout { "Guest Stack Offset", &format_args!("{:#x}", self.peb_guest_stack_data_offset), ) + .field( + "Guest Clock Offset", + &format_args!("{:#x}", self.peb_guest_clock_offset), + ) .field( "Host Function Definitions Buffer Offset", &format_args!("{:#x}", self.host_function_definitions_buffer_offset), @@ -192,6 +201,10 @@ impl Debug for SandboxMemoryLayout { "Output Data Buffer Offset", &format_args!("{:#x}", self.output_data_buffer_offset), ) + .field( + "Clock Page Offset", + &format_args!("{:#x}", self.clock_page_offset), + ) .field( "Guest Heap Buffer Offset", &format_args!("{:#x}", self.guest_heap_buffer_offset), @@ -256,6 +269,7 @@ impl SandboxMemoryLayout { let peb_init_data_offset = peb_offset + offset_of!(HyperlightPEB, init_data); let peb_heap_data_offset = peb_offset + offset_of!(HyperlightPEB, guest_heap); let peb_guest_stack_data_offset = peb_offset + offset_of!(HyperlightPEB, guest_stack); + let peb_guest_clock_offset = peb_offset + offset_of!(HyperlightPEB, guest_clock); let peb_host_function_definitions_offset = peb_offset + offset_of!(HyperlightPEB, host_function_definitions); @@ -275,11 +289,13 @@ impl SandboxMemoryLayout { input_data_buffer_offset + cfg.get_input_data_size(), PAGE_SIZE_USIZE, ); - // make sure heap buffer starts at 4K boundary - let guest_heap_buffer_offset = round_up_to( + // Clock page for pvclock/Reference TSC - needs to be page-aligned + let clock_page_offset = round_up_to( output_data_buffer_offset + cfg.get_output_data_size(), PAGE_SIZE_USIZE, ); + // make sure heap buffer starts at 4K boundary (after clock page) + let guest_heap_buffer_offset = clock_page_offset + PAGE_SIZE_USIZE; // make sure guard page starts at 4K boundary let guard_page_offset = round_up_to(guest_heap_buffer_offset + heap_size, PAGE_SIZE_USIZE); let guest_user_stack_buffer_offset = guard_page_offset + PAGE_SIZE_USIZE; @@ -300,11 +316,13 @@ impl SandboxMemoryLayout { peb_init_data_offset, peb_heap_data_offset, peb_guest_stack_data_offset, + peb_guest_clock_offset, sandbox_memory_config: cfg, code_size, host_function_definitions_buffer_offset, input_data_buffer_offset, output_data_buffer_offset, + clock_page_offset, guest_heap_buffer_offset, guest_user_stack_buffer_offset, peb_address, @@ -360,6 +378,19 @@ impl SandboxMemoryLayout { self.stack_size } + /// Get the offset in guest memory to the guest clock region. + #[instrument(skip_all, parent = Span::current(), level= "Trace")] + pub(crate) fn get_guest_clock_offset(&self) -> usize { + self.peb_guest_clock_offset + } + + /// Get the offset in guest memory to the clock page. + /// This is a 4KB page used for pvclock/Reference TSC data. + #[instrument(skip_all, parent = Span::current(), level= "Trace")] + pub(crate) fn get_clock_page_offset(&self) -> usize { + self.clock_page_offset + } + /// Get the offset in guest memory to the output data pointer. #[instrument(skip_all, parent = Span::current(), level= "Trace")] fn get_output_data_pointer_offset(&self) -> usize { @@ -580,12 +611,29 @@ impl SandboxMemoryLayout { } // guest output data - let heap_offset = builder.push_page_aligned( + let clock_page_offset = builder.push_page_aligned( self.sandbox_memory_config.get_output_data_size(), MemoryRegionFlags::READ | MemoryRegionFlags::WRITE, OutputData, ); + let expected_clock_page_offset = TryInto::::try_into(self.clock_page_offset)?; + + if clock_page_offset != expected_clock_page_offset { + return Err(new_error!( + "Clock Page offset does not match expected Clock Page offset expected: {}, actual: {}", + expected_clock_page_offset, + clock_page_offset + )); + } + + // clock page for pvclock/Reference TSC + let heap_offset = builder.push_page_aligned( + PAGE_SIZE_USIZE, + MemoryRegionFlags::READ | MemoryRegionFlags::WRITE, + ClockPage, + ); + let expected_heap_offset = TryInto::::try_into(self.guest_heap_buffer_offset)?; if heap_offset != expected_heap_offset { @@ -806,6 +854,12 @@ impl SandboxMemoryLayout { shared_mem.write_u64(self.get_user_stack_pointer_offset(), start_of_user_stack)?; + // Initialize guest clock region with default values (no clock configured yet) + // GuestClockRegion is 32 bytes: clock_page_ptr (8) + clock_type (8) + boot_time_ns (8) + utc_offset_seconds (4) + padding (4) + // The actual clock configuration is done by the hypervisor-specific code + let guest_clock_bytes = [0u8; size_of::()]; + shared_mem.copy_from_slice(&guest_clock_bytes, self.get_guest_clock_offset())?; + // End of setting up the PEB // Initialize the stack pointers of input data and output data @@ -867,6 +921,8 @@ mod tests { expected_size += round_up_to(cfg.get_output_data_size(), PAGE_SIZE_USIZE); + expected_size += PAGE_SIZE_USIZE; // clock page + expected_size += round_up_to(layout.heap_size, PAGE_SIZE_USIZE); expected_size += PAGE_SIZE_USIZE; // guard page diff --git a/src/hyperlight_host/src/mem/memory_region.rs b/src/hyperlight_host/src/mem/memory_region.rs index 4d03d83c4..cdedf3982 100644 --- a/src/hyperlight_host/src/mem/memory_region.rs +++ b/src/hyperlight_host/src/mem/memory_region.rs @@ -129,6 +129,8 @@ pub enum MemoryRegionType { InputData, /// The region contains the Output Data OutputData, + /// The region contains the Clock Page (for pvclock/Reference TSC) + ClockPage, /// The region contains the Heap Heap, /// The region contains the Guard Page diff --git a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs index c9f6bc220..4498dd8e5 100644 --- a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs +++ b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs @@ -16,7 +16,9 @@ limitations under the License. #[cfg(gdb)] use std::sync::{Arc, Mutex}; +use std::time::{SystemTime, UNIX_EPOCH}; +use hyperlight_common::time::GuestClockRegion; use rand::Rng; use tracing::{Span, instrument}; @@ -25,10 +27,11 @@ use super::SandboxConfiguration; use super::uninitialized::SandboxRuntimeConfig; use crate::hypervisor::hyperlight_vm::HyperlightVm; use crate::mem::exe::LoadInfo; +use crate::mem::layout::SandboxMemoryLayout; use crate::mem::mgr::SandboxMemoryManager; use crate::mem::ptr::{GuestPtr, RawPtr}; use crate::mem::ptr_offset::Offset; -use crate::mem::shared_mem::GuestSharedMemory; +use crate::mem::shared_mem::{GuestSharedMemory, SharedMemory}; #[cfg(gdb)] use crate::sandbox::config::DebugInfo; #[cfg(feature = "mem_profile")] @@ -37,6 +40,47 @@ use crate::sandbox::trace::MemTraceInfo; use crate::signal_handlers::setup_signal_handlers; use crate::{MultiUseSandbox, Result, UninitializedSandbox, new_error}; +/// Get the UTC offset in seconds from the local timezone. +/// +/// Returns the number of seconds to add to UTC to get local time. +/// Positive values are east of UTC, negative values are west. +fn get_utc_offset_seconds() -> i32 { + #[cfg(unix)] + { + // Use libc to get the timezone offset + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as libc::time_t) + .unwrap_or(0); + + let mut tm: libc::tm = unsafe { std::mem::zeroed() }; + // SAFETY: localtime_r is thread-safe and writes to our stack-allocated tm + unsafe { + libc::localtime_r(&now, &mut tm); + } + // tm_gmtoff is the offset from UTC in seconds + tm.tm_gmtoff as i32 + } + #[cfg(windows)] + { + // On Windows, use Windows API to get timezone info + use windows::Win32::System::Time::{GetTimeZoneInformation, TIME_ZONE_INFORMATION}; + + let mut tzi: TIME_ZONE_INFORMATION = unsafe { std::mem::zeroed() }; + // SAFETY: GetTimeZoneInformation writes to our stack-allocated tzi + let result = unsafe { GetTimeZoneInformation(&mut tzi) }; + + // TIME_ZONE_ID_INVALID is 0xFFFFFFFF + if result == 0xFFFFFFFF { + return 0; + } + + // Bias is in minutes, negative for east of UTC + // We negate and convert to seconds + -(tzi.Bias * 60) + } +} + #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] pub(super) fn evolve_impl_multi_use(u_sbox: UninitializedSandbox) -> Result { let (mut hshm, mut gshm) = u_sbox.mgr.build(); @@ -150,7 +194,7 @@ pub(crate) fn set_up_hypervisor_partition( #[cfg(feature = "mem_profile")] let trace_info = MemTraceInfo::new(_load_info.info)?; - HyperlightVm::new( + let mut vm = HyperlightVm::new( regions, pml4_ptr.absolute()?, entrypoint_ptr.absolute()?, @@ -176,7 +220,55 @@ pub(crate) fn set_up_hypervisor_partition( rt_cfg.clone(), #[cfg(feature = "mem_profile")] trace_info, - ) + )?; + + // Setup paravirtualized clock + // Compute clock page GPA (guest physical address) + let clock_page_gpa = + (SandboxMemoryLayout::BASE_ADDRESS + mgr.layout.get_clock_page_offset()) as u64; + + // Get boot time (UTC nanoseconds since Unix epoch) + let boot_time_ns = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_nanos() as u64) + .unwrap_or(0); + + // Get UTC offset from local timezone + let utc_offset_seconds = get_utc_offset_seconds(); + + // Setup pvclock in hypervisor and get clock type + let clock_type = vm.setup_pvclock_for_guest(clock_page_gpa)?; + + // Write GuestClockRegion to PEB + let guest_clock = + GuestClockRegion::new(clock_page_gpa, clock_type, boot_time_ns, utc_offset_seconds); + + // Write the GuestClockRegion to shared memory + let guest_clock_offset = mgr.layout.get_guest_clock_offset(); + mgr.shared_mem.with_exclusivity(|shared_mem| { + // Write clock_page_ptr + shared_mem.write_u64(guest_clock_offset, guest_clock.clock_page_ptr)?; + // Write clock_type + shared_mem.write_u64(guest_clock_offset + 8, guest_clock.clock_type)?; + // Write boot_time_ns + shared_mem.write_u64(guest_clock_offset + 16, guest_clock.boot_time_ns)?; + // Write utc_offset_seconds (i32) as a u64 (with zero padding) + shared_mem.write_u64( + guest_clock_offset + 24, + guest_clock.utc_offset_seconds as u32 as u64, + )?; + Ok::<(), crate::HyperlightError>(()) + })??; + + log::debug!( + "Guest clock configured: type={:?}, page_gpa={:#x}, boot_time_ns={}, utc_offset_seconds={}", + clock_type, + clock_page_gpa, + boot_time_ns, + utc_offset_seconds + ); + + Ok(vm) } #[cfg(test)] diff --git a/src/hyperlight_host/tests/integration_test.rs b/src/hyperlight_host/tests/integration_test.rs index 656aa796a..b6c1eb12e 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -1760,3 +1760,363 @@ fn interrupt_cancel_delete_race() { handle.join().unwrap(); } } + +// Paravirtualized clock tests + +#[cfg(feature = "guest_time")] +#[test] +fn test_pvclock_available() { + let mut sandbox: MultiUseSandbox = new_uninit().unwrap().evolve().unwrap(); + let available = sandbox.call::("TestClockAvailable", ()).unwrap(); + assert_eq!(available, 1, "Paravirtualized clock should be available"); +} + +#[cfg(feature = "guest_time")] +#[test] +fn test_monotonic_time_increases() { + let mut sandbox: MultiUseSandbox = new_uninit().unwrap().evolve().unwrap(); + let result = sandbox.call::("TestMonotonicIncreases", ()).unwrap(); + assert_eq!(result, 1, "Monotonic time should increase between reads"); +} + +#[cfg(feature = "guest_time")] +#[test] +fn test_wall_clock_reasonable() { + use std::time::{SystemTime, UNIX_EPOCH}; + + let mut sandbox: MultiUseSandbox = new_uninit().unwrap().evolve().unwrap(); + + // First call - guest time should be between host before and after times + let host_before_ns = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; + let guest_ns_1 = sandbox.call::("GetWallClockTimeNs", ()).unwrap(); + let host_after_ns = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; + + // Allow 100ms skew for any timing differences + let skew_ns = 100_000_000u64; + assert!( + guest_ns_1 >= host_before_ns.saturating_sub(skew_ns), + "Guest clock too early: guest={}, host_before={}", + guest_ns_1, + host_before_ns + ); + assert!( + guest_ns_1 <= host_after_ns + skew_ns, + "Guest clock too late: guest={}, host_after={}", + guest_ns_1, + host_after_ns + ); + + // Second call - should be later + let guest_ns_2 = sandbox.call::("GetWallClockTimeNs", ()).unwrap(); + assert!( + guest_ns_2 >= guest_ns_1, + "Second call should be >= first: t1={}, t2={}", + guest_ns_1, + guest_ns_2 + ); +} + +#[cfg(feature = "guest_time")] +#[test] +fn test_utc_offset_matches_host() { + let mut sandbox: MultiUseSandbox = new_uninit().unwrap().evolve().unwrap(); + + // Get the UTC offset from the guest + let guest_offset = sandbox.call::("GetUtcOffsetSeconds", ()).unwrap(); + + // Get the host's UTC offset using platform-specific APIs + #[cfg(unix)] + let host_offset = { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs() as libc::time_t) + .unwrap_or(0); + + let mut tm: libc::tm = unsafe { std::mem::zeroed() }; + unsafe { + libc::localtime_r(&now, &mut tm); + } + tm.tm_gmtoff as i32 + }; + + #[cfg(windows)] + let host_offset = { + use windows::Win32::System::Time::{GetTimeZoneInformation, TIME_ZONE_INFORMATION}; + + let mut tzi: TIME_ZONE_INFORMATION = unsafe { std::mem::zeroed() }; + let result = unsafe { GetTimeZoneInformation(&mut tzi) }; + + if result == 0xFFFFFFFF { + 0 + } else { + // Bias is in minutes, negative for east of UTC + // We negate and convert to seconds + -(tzi.Bias * 60) + } + }; + + // The guest offset should match the host offset (captured at sandbox creation) + assert_eq!( + guest_offset, host_offset, + "Guest UTC offset should match host: guest={}, host={}", + guest_offset, host_offset + ); +} + +#[cfg(feature = "guest_time")] +#[test] +fn test_datetime_formatting() { + let mut sandbox: MultiUseSandbox = new_uninit().unwrap().evolve().unwrap(); + + // Get formatted datetime from guest + let formatted = sandbox.call::("FormatCurrentDateTime", ()).unwrap(); + + // Should contain expected components (not empty, has day/month/year) + assert!( + !formatted.is_empty(), + "Formatted datetime should not be empty" + ); + assert!( + formatted.contains(' '), + "Formatted datetime should contain spaces: '{}'", + formatted + ); + + // The datetime should have reasonable content - check for common weekday/month words + let has_weekday = [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + ] + .iter() + .any(|day| formatted.contains(day)); + let has_month = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ] + .iter() + .any(|month| formatted.contains(month)); + + assert!( + has_weekday, + "Formatted datetime should contain a weekday name: '{}'", + formatted + ); + assert!( + has_month, + "Formatted datetime should contain a month name: '{}'", + formatted + ); + + println!("Guest formatted datetime: {}", formatted); +} + +/// Test data structure for timestamp formatting tests +#[cfg(feature = "guest_time")] +struct TimestampTestCase { + /// Unix timestamp in nanoseconds + timestamp_ns: u64, + /// Expected formatted output + expected: &'static str, + /// Description of what this test case verifies + description: &'static str, +} + +#[cfg(feature = "guest_time")] +fn get_timestamp_test_cases() -> Vec { + vec![ + // Unix epoch + TimestampTestCase { + timestamp_ns: 0, + expected: "Thursday 01 January 1970 00:00:00", + description: "Unix epoch (1970-01-01 00:00:00 UTC)", + }, + // One day after epoch + TimestampTestCase { + timestamp_ns: 86400 * 1_000_000_000, + expected: "Friday 02 January 1970 00:00:00", + description: "One day after epoch", + }, + // End of January 1970 + TimestampTestCase { + timestamp_ns: 30 * 86400 * 1_000_000_000, + expected: "Saturday 31 January 1970 00:00:00", + description: "31st January 1970", + }, + // 21st day + TimestampTestCase { + timestamp_ns: 20 * 86400 * 1_000_000_000, + expected: "Wednesday 21 January 1970 00:00:00", + description: "21st January 1970", + }, + // 22nd day + TimestampTestCase { + timestamp_ns: 21 * 86400 * 1_000_000_000, + expected: "Thursday 22 January 1970 00:00:00", + description: "22nd January 1970", + }, + // 23rd day + TimestampTestCase { + timestamp_ns: 22 * 86400 * 1_000_000_000, + expected: "Friday 23 January 1970 00:00:00", + description: "23rd January 1970", + }, + // Y2K (test year 2000, leap year) + TimestampTestCase { + timestamp_ns: 946684800 * 1_000_000_000, + expected: "Saturday 01 January 2000 00:00:00", + description: "Y2K - January 1st, 2000", + }, + // Feb 29, 2000 (leap year) + TimestampTestCase { + timestamp_ns: 951782400 * 1_000_000_000, + expected: "Tuesday 29 February 2000 00:00:00", + description: "Leap day Feb 29, 2000", + }, + // March 1, 2000 (day after leap day) + TimestampTestCase { + timestamp_ns: 951868800 * 1_000_000_000, + expected: "Wednesday 01 March 2000 00:00:00", + description: "March 1, 2000 (day after leap day)", + }, + // Current era: January 15, 2026 at 12:30:45 + TimestampTestCase { + timestamp_ns: 1768480245 * 1_000_000_000, + expected: "Thursday 15 January 2026 12:30:45", + description: "January 15, 2026 at 12:30:45 UTC", + }, + // Test all months - pick 15th of each month 2025 + TimestampTestCase { + timestamp_ns: 1736899200 * 1_000_000_000, // Jan 15, 2025 + expected: "Wednesday 15 January 2025 00:00:00", + description: "January 2025", + }, + TimestampTestCase { + timestamp_ns: 1739577600 * 1_000_000_000, // Feb 15, 2025 + expected: "Saturday 15 February 2025 00:00:00", + description: "February 2025", + }, + TimestampTestCase { + timestamp_ns: 1741996800 * 1_000_000_000, // Mar 15, 2025 + expected: "Saturday 15 March 2025 00:00:00", + description: "March 2025", + }, + TimestampTestCase { + timestamp_ns: 1744675200 * 1_000_000_000, // Apr 15, 2025 + expected: "Tuesday 15 April 2025 00:00:00", + description: "April 2025", + }, + TimestampTestCase { + timestamp_ns: 1747267200 * 1_000_000_000, // May 15, 2025 + expected: "Thursday 15 May 2025 00:00:00", + description: "May 2025", + }, + TimestampTestCase { + timestamp_ns: 1749945600 * 1_000_000_000, // Jun 15, 2025 + expected: "Sunday 15 June 2025 00:00:00", + description: "June 2025", + }, + TimestampTestCase { + timestamp_ns: 1752537600 * 1_000_000_000, // Jul 15, 2025 + expected: "Tuesday 15 July 2025 00:00:00", + description: "July 2025", + }, + TimestampTestCase { + timestamp_ns: 1755216000 * 1_000_000_000, // Aug 15, 2025 + expected: "Friday 15 August 2025 00:00:00", + description: "August 2025", + }, + TimestampTestCase { + timestamp_ns: 1757894400 * 1_000_000_000, // Sep 15, 2025 + expected: "Monday 15 September 2025 00:00:00", + description: "September 2025", + }, + TimestampTestCase { + timestamp_ns: 1760486400 * 1_000_000_000, // Oct 15, 2025 + expected: "Wednesday 15 October 2025 00:00:00", + description: "October 2025", + }, + TimestampTestCase { + timestamp_ns: 1763164800 * 1_000_000_000, // Nov 15, 2025 + expected: "Saturday 15 November 2025 00:00:00", + description: "November 2025", + }, + TimestampTestCase { + timestamp_ns: 1765756800 * 1_000_000_000, // Dec 15, 2025 + expected: "Monday 15 December 2025 00:00:00", + description: "December 2025", + }, + // Test non-leap year Feb 28 and Mar 1 (2025) + TimestampTestCase { + timestamp_ns: 1740700800 * 1_000_000_000, // Feb 28, 2025 + expected: "Friday 28 February 2025 00:00:00", + description: "Feb 28, 2025 (non-leap year)", + }, + TimestampTestCase { + timestamp_ns: 1740787200 * 1_000_000_000, // Mar 1, 2025 + expected: "Saturday 01 March 2025 00:00:00", + description: "Mar 1, 2025 (day after non-leap Feb 28)", + }, + // Test leap year 2024 + TimestampTestCase { + timestamp_ns: 1709164800 * 1_000_000_000, // Feb 29, 2024 + expected: "Thursday 29 February 2024 00:00:00", + description: "Feb 29, 2024 (leap day)", + }, + // Test time components + TimestampTestCase { + timestamp_ns: 1704067199 * 1_000_000_000, // Dec 31, 2023 23:59:59 + expected: "Sunday 31 December 2023 23:59:59", + description: "End of 2023 (test time components)", + }, + ] +} + +#[cfg(feature = "guest_time")] +#[test] +fn test_timestamp_formatting() { + // Uses GUEST env var to select Rust or C guest (default: Rust) + let mut sandbox: MultiUseSandbox = new_uninit().unwrap().evolve().unwrap(); + + let test_cases = get_timestamp_test_cases(); + let test_count = test_cases.len(); + + for tc in test_cases { + let result = sandbox + .call::("FormatTimestampNs", tc.timestamp_ns) + .unwrap_or_else(|e| { + panic!( + "Failed to call FormatTimestampNs for {}: {:?}", + tc.description, e + ) + }); + + assert_eq!( + result, tc.expected, + "Timestamp formatting failed for {}:\n timestamp_ns: {}\n expected: '{}'\n got: '{}'", + tc.description, tc.timestamp_ns, tc.expected, result + ); + } + + println!("All {} timestamp formatting tests passed", test_count); +} diff --git a/src/tests/c_guests/c_simpleguest/main.c b/src/tests/c_guests/c_simpleguest/main.c index 30caff590..5c5a52f76 100644 --- a/src/tests/c_guests/c_simpleguest/main.c +++ b/src/tests/c_guests/c_simpleguest/main.c @@ -8,6 +8,109 @@ // Included from hyperlight_guest_bin/third_party/printf #include "printf.h" +// ============================================================================ +// Time API tests - exercise the POSIX-style C time functions from hyperlight_guest_capi +// ============================================================================ + +// Check if the paravirtualized clock is available +int is_clock_available(void) { + hl_timespec ts; + return clock_gettime(hl_CLOCK_REALTIME, &ts) == 0 ? 1 : 0; +} + +// Get the current monotonic time in nanoseconds +uint64_t monotonic_time_ns(void) { + hl_timespec ts; + if (clock_gettime(hl_CLOCK_MONOTONIC, &ts) != 0) { + return 0; + } + return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; +} + +// Get the current wall clock time in nanoseconds (UTC since Unix epoch) +uint64_t wall_clock_time_ns(void) { + hl_timespec ts; + if (clock_gettime(hl_CLOCK_REALTIME, &ts) != 0) { + return 0; + } + return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; +} + +// Test that monotonic time increases between reads +int monotonic_increases(void) { + hl_timespec ts1, ts2; + + if (clock_gettime(hl_CLOCK_MONOTONIC, &ts1) != 0) { + return 0; + } + + // Small busy loop to ensure time passes + for (volatile int i = 0; i < 10000; i++) {} + + if (clock_gettime(hl_CLOCK_MONOTONIC, &ts2) != 0) { + return 0; + } + + // Second reading should be > first + uint64_t t1 = (uint64_t)ts1.tv_sec * 1000000000ULL + (uint64_t)ts1.tv_nsec; + uint64_t t2 = (uint64_t)ts2.tv_sec * 1000000000ULL + (uint64_t)ts2.tv_nsec; + + return t2 > t1 ? 1 : 0; +} + +// Get the UTC offset in seconds +int utc_offset_seconds(void) { + hl_timeval tv; + hl_timezone tz; + if (gettimeofday(&tv, &tz) != 0) { + return 0; + } + // tz_minuteswest is minutes WEST of UTC, so negate and convert to seconds + return -(tz.tz_minuteswest * 60); +} + +// Static buffers for formatted strings (safe - guests are single-threaded) +static char datetime_buffer[128]; +static char timestamp_buffer[128]; + +// Format the current local time using strftime from hyperlight_guest_capi +const char* format_current_datetime(void) { + int64_t now = time(NULL); + if (now == -1) { + return "Error: clock not available"; + } + + hl_tm tm_local; + if (localtime_r(&now, &tm_local) == NULL) { + return "Error: localtime_r failed"; + } + + // Use strftime from the C API: "Thursday 16 January 2026 15:48:39" + if (strftime((uint8_t*)datetime_buffer, sizeof(datetime_buffer), + (const uint8_t*)"%A %d %B %Y %H:%M:%S", &tm_local) == 0) { + return "Error: strftime failed"; + } + + return datetime_buffer; +} + +// Format a UTC timestamp (nanoseconds) using strftime +const char* format_timestamp_ns(uint64_t timestamp_ns) { + int64_t secs = (int64_t)(timestamp_ns / 1000000000ULL); + + hl_tm tm_utc; + if (gmtime_r(&secs, &tm_utc) == NULL) { + return "Error: gmtime_r failed"; + } + + if (strftime((uint8_t*)timestamp_buffer, sizeof(timestamp_buffer), + (const uint8_t*)"%A %d %B %Y %H:%M:%S", &tm_utc) == 0) { + return "Error: strftime failed"; + } + + return timestamp_buffer; +} + #define GUEST_STACK_SIZE (65536) // default stack size #define MAX_BUFFER_SIZE (1024) @@ -354,6 +457,14 @@ HYPERLIGHT_WRAP_FUNCTION(print_eleven_args, Int, 11, String, Int, Long, String, HYPERLIGHT_WRAP_FUNCTION(echo_float, Float, 1, Float) HYPERLIGHT_WRAP_FUNCTION(echo_double, Double, 1, Double) HYPERLIGHT_WRAP_FUNCTION(set_static, Int, 0) +// Time API test functions +HYPERLIGHT_WRAP_FUNCTION(is_clock_available, Int, 0) +HYPERLIGHT_WRAP_FUNCTION(monotonic_time_ns, ULong, 0) +HYPERLIGHT_WRAP_FUNCTION(wall_clock_time_ns, ULong, 0) +HYPERLIGHT_WRAP_FUNCTION(monotonic_increases, Int, 0) +HYPERLIGHT_WRAP_FUNCTION(utc_offset_seconds, Int, 0) +HYPERLIGHT_WRAP_FUNCTION(format_current_datetime, String, 0) +HYPERLIGHT_WRAP_FUNCTION(format_timestamp_ns, String, 1, ULong) // HYPERLIGHT_WRAP_FUNCTION(get_size_prefixed_buffer, Int, 1, VecBytes) is not valid for functions that return VecBytes HYPERLIGHT_WRAP_FUNCTION(guest_abort_with_msg, Int, 2, Int, String) HYPERLIGHT_WRAP_FUNCTION(guest_abort_with_code, Int, 1, Int) @@ -393,6 +504,14 @@ void hyperlight_main(void) HYPERLIGHT_REGISTER_FUNCTION("EchoFloat", echo_float); HYPERLIGHT_REGISTER_FUNCTION("EchoDouble", echo_double); HYPERLIGHT_REGISTER_FUNCTION("SetStatic", set_static); + // Time API test functions + HYPERLIGHT_REGISTER_FUNCTION("TestClockAvailable", is_clock_available); + HYPERLIGHT_REGISTER_FUNCTION("GetMonotonicTimeNs", monotonic_time_ns); + HYPERLIGHT_REGISTER_FUNCTION("GetWallClockTimeNs", wall_clock_time_ns); + HYPERLIGHT_REGISTER_FUNCTION("TestMonotonicIncreases", monotonic_increases); + HYPERLIGHT_REGISTER_FUNCTION("GetUtcOffsetSeconds", utc_offset_seconds); + HYPERLIGHT_REGISTER_FUNCTION("FormatCurrentDateTime", format_current_datetime); + HYPERLIGHT_REGISTER_FUNCTION("FormatTimestampNs", format_timestamp_ns); // HYPERLIGHT_REGISTER_FUNCTION macro does not work for functions that return VecBytes, // so we use hl_register_function_definition directly hl_register_function_definition("GetSizePrefixedBuffer", get_size_prefixed_buffer, 1, (hl_ParameterType[]){hl_ParameterType_VecBytes}, hl_ReturnType_VecBytes); @@ -416,4 +535,4 @@ hl_Vec *c_guest_dispatch_function(const hl_FunctionCall *function_call) { } return NULL; -} \ No newline at end of file +} diff --git a/src/tests/rust_guests/dummyguest/Cargo.lock b/src/tests/rust_guests/dummyguest/Cargo.lock index 7c63e8c58..ee28b875a 100644 --- a/src/tests/rust_guests/dummyguest/Cargo.lock +++ b/src/tests/rust_guests/dummyguest/Cargo.lock @@ -60,9 +60,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "flatbuffers" -version = "25.9.23" +version = "25.12.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b6620799e7340ebd9968d2e0708eb82cf1971e9a16821e2091b6d6e475eed5" +checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" dependencies = [ "bitflags", "rustc_version", diff --git a/src/tests/rust_guests/simpleguest/Cargo.lock b/src/tests/rust_guests/simpleguest/Cargo.lock index e74c91d02..0872c0780 100644 --- a/src/tests/rust_guests/simpleguest/Cargo.lock +++ b/src/tests/rust_guests/simpleguest/Cargo.lock @@ -52,9 +52,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "flatbuffers" -version = "25.9.23" +version = "25.12.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b6620799e7340ebd9968d2e0708eb82cf1971e9a16821e2091b6d6e475eed5" +checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" dependencies = [ "bitflags", "rustc_version", diff --git a/src/tests/rust_guests/simpleguest/src/main.rs b/src/tests/rust_guests/simpleguest/src/main.rs index 5f8df6a65..a9c8c6766 100644 --- a/src/tests/rust_guests/simpleguest/src/main.rs +++ b/src/tests/rust_guests/simpleguest/src/main.rs @@ -45,6 +45,7 @@ use hyperlight_common::flatbuffer_wrappers::util::get_flatbuffer_result; use hyperlight_common::mem::PAGE_SIZE; use hyperlight_guest::error::{HyperlightGuestError, Result}; use hyperlight_guest::exit::{abort_with_code, abort_with_code_and_message}; +use hyperlight_guest::time::{is_clock_available, monotonic_time_ns}; use hyperlight_guest_bin::exceptions::handler::{Context, ExceptionInfo}; use hyperlight_guest_bin::guest_function::definition::GuestFunctionDefinition; use hyperlight_guest_bin::guest_function::register::register_function; @@ -53,7 +54,10 @@ use hyperlight_guest_bin::host_comm::{ print_output_with_host_print, read_n_bytes_from_user_memory, }; use hyperlight_guest_bin::memory::malloc; -use hyperlight_guest_bin::{MIN_STACK_ADDRESS, guest_function, guest_logger, host_function}; +use hyperlight_guest_bin::time::{DateTime, Instant, SystemTime, UNIX_EPOCH, utc_offset_seconds}; +use hyperlight_guest_bin::{ + GUEST_HANDLE, MIN_STACK_ADDRESS, guest_function, guest_logger, host_function, +}; use log::{LevelFilter, error}; use tracing::{Span, instrument}; @@ -188,6 +192,89 @@ fn echo_float(value: f32) -> f32 { value } +// Time/clock test functions using the std-like API from hyperlight_guest_bin::time + +/// Check if the paravirtualized clock is available +#[guest_function("TestClockAvailable")] +fn test_clock_available() -> i32 { + // SAFETY: GUEST_HANDLE is initialized during entrypoint, we are single-threaded + #[allow(static_mut_refs)] + let handle = unsafe { &GUEST_HANDLE }; + if is_clock_available(handle) { 1 } else { 0 } +} + +/// Get the current monotonic time in nanoseconds using low-level API +#[guest_function("GetMonotonicTimeNs")] +fn get_monotonic_time_ns_fn() -> u64 { + // SAFETY: GUEST_HANDLE is initialized during entrypoint, we are single-threaded + #[allow(static_mut_refs)] + let handle = unsafe { &GUEST_HANDLE }; + monotonic_time_ns(handle).unwrap_or(0) +} + +/// Get the current wall clock time in nanoseconds (UTC since Unix epoch) using SystemTime API +#[guest_function("GetWallClockTimeNs")] +fn get_wall_clock_time_ns_fn() -> u64 { + // Use the std-like SystemTime API + let now = SystemTime::now(); + now.duration_since(UNIX_EPOCH) + .map(|d| d.as_nanos() as u64) + .unwrap_or(0) +} + +/// Test that monotonic time increases between reads using Instant API +#[guest_function("TestMonotonicIncreases")] +fn test_monotonic_increases() -> i32 { + let t1 = Instant::now(); + // Small busy loop to ensure time passes + for _ in 0..10000 { + core::hint::black_box(0); + } + let t2 = Instant::now(); + + // Instant comparison: t2 should be greater than t1 + if t2 > t1 { 1 } else { 0 } +} + +/// Get the UTC offset in seconds that was set at sandbox creation +#[guest_function("GetUtcOffsetSeconds")] +fn get_utc_offset_seconds_fn() -> i32 { + utc_offset_seconds().unwrap_or(0) +} + +/// Format the current local date/time as a string (e.g., "Thursday 15th January 2026 15:34") +#[guest_function("FormatCurrentDateTime")] +fn format_current_datetime() -> String { + let dt = DateTime::now_local(); + format!( + "{} {} {} {} {:02}:{:02}:{:02}", + dt.weekday().name(), + dt.day_ordinal(), + dt.month().name(), + dt.year(), + dt.hour(), + dt.minute(), + dt.second() + ) +} + +/// Format a specific UTC timestamp (nanoseconds since Unix epoch) as a date/time string +/// Returns: "Weekday DD Month Year HH:MM:SS" (e.g., "Thursday 15 January 2026 16:30:00") +#[guest_function("FormatTimestampNs")] +fn format_timestamp_ns(timestamp_ns: u64) -> String { + let dt = DateTime::from_timestamp_nanos(timestamp_ns); + format!( + "{} {:02} {} {} {:02}:{:02}:{:02}", + dt.weekday().name(), + dt.day(), + dt.month().name(), + dt.year(), + dt.hour(), + dt.minute(), + dt.second() + ) +} + #[host_function("HostPrint")] fn host_print(msg: String) -> i32; diff --git a/src/tests/rust_guests/witguest/Cargo.lock b/src/tests/rust_guests/witguest/Cargo.lock index 08d4f9a4b..7ca659721 100644 --- a/src/tests/rust_guests/witguest/Cargo.lock +++ b/src/tests/rust_guests/witguest/Cargo.lock @@ -146,9 +146,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "flatbuffers" -version = "25.9.23" +version = "25.12.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b6620799e7340ebd9968d2e0708eb82cf1971e9a16821e2091b6d6e475eed5" +checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" dependencies = [ "bitflags", "rustc_version", @@ -418,18 +418,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -557,9 +557,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote",