Skip to content

Commit bf6be7d

Browse files
committed
fix: address code review comments
Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com>
1 parent fb1549f commit bf6be7d

File tree

6 files changed

+176
-60
lines changed

6 files changed

+176
-60
lines changed

docs/picolibc.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Picolibc Integration
2+
3+
Hyperlight uses [picolibc](https://github.com/picolibc/picolibc) as its C
4+
standard library for guest binaries, replacing the previous musl-based
5+
approach. Picolibc is a lightweight C library designed for embedded systems,
6+
making it well-suited for Hyperlight's micro-VM environment.
7+
8+
## Overview
9+
10+
The picolibc integration is controlled by the `libc` feature flag on the
11+
`hyperlight-guest-bin` crate (enabled by default). When enabled, the build
12+
script compiles picolibc from source using the vendored submodule at
13+
`src/hyperlight_guest_bin/third_party/picolibc`.
14+
15+
The build uses a sparse checkout to exclude GPL/AGPL-licensed test and script
16+
files — only BSD/MIT/permissive-licensed source files are included. See
17+
`NOTICE.txt` for full licensing details.
18+
19+
## Host Function Stubs
20+
21+
When the `libc` feature is enabled, the POSIX stubs in
22+
`src/hyperlight_guest_bin/src/host_bridge.rs` provide C-compatible
23+
implementations of `read`, `write`, `clock_gettime`, `gettimeofday`, and
24+
other functions that picolibc calls internally.
25+
26+
These stubs can optionally delegate to host functions. If the host registers
27+
these functions, the corresponding libc functionality becomes available to
28+
guest code. If not registered, the stubs return appropriate errors.
29+
30+
| Host Function | Parameters | Return Type | Description |
31+
|-----------------|------------------------|-------------|-------------|
32+
| `HostPrint` | `String` | `Int` | Used by the `write()` stub. Only stdout (fd 1) and stderr (fd 2) are supported; both delegate to this single host function. Other file descriptors return `EBADF`. |
33+
| `HostRead` | `ULong` (byte count) | `VecBytes` | Used by the `read()` stub. Only stdin (fd 0) is supported; other file descriptors return `EBADF`. |
34+
| `CurrentTime` | _(none)_ | `VecBytes` | Used by `clock_gettime()` and `gettimeofday()`. Should return 16 bytes: 8 bytes of seconds + 8 bytes of nanoseconds. If not provided, a monotonic fallback starting at Unix timestamp `1609459200` (2021-01-01) is used. |
35+
36+
## Build Configuration
37+
38+
The build script (`build.rs`) generates a `picolibc.h` configuration header
39+
that controls which picolibc features are enabled. Key features:
40+
41+
- Single-threaded: no locking or TLS support
42+
- Global errno: uses a single global `errno` variable
43+
- Tiny stdio: minimal stdio implementation
44+
- No malloc: memory allocation is handled by the Rust global allocator
45+
- IEEE math: math library without errno side effects
46+
47+
For full details on available picolibc build options, see the
48+
[picolibc build documentation](https://github.com/picolibc/picolibc/blob/main/doc/build.md).
49+
50+
The file list of picolibc sources to compile is maintained in `build_files.rs`.
51+
52+
## Updating Picolibc
53+
54+
To update picolibc to a new version:
55+
56+
1. Update the submodule:
57+
```bash
58+
cd src/hyperlight_guest_bin/third_party/picolibc
59+
git fetch origin
60+
git checkout <new-version-tag>
61+
cd ../../../..
62+
git add src/hyperlight_guest_bin/third_party/picolibc
63+
```
64+
65+
2. Verify licensing: Check that no new GPL/AGPL-licensed source files
66+
have been added to the directories we compile. The sparse checkout
67+
(configured in `build.rs` `sparse_checkout()`) excludes `test/`,
68+
`scripts/`, and `COPYING.GPL2`, but review any new files.
69+
70+
3. Update `build_files.rs`: Compare the file list against the new
71+
version's meson build files. Files may have been added, removed, or
72+
renamed. The meson build definitions in `libc/meson.build` and
73+
`libm/meson.build` (and their subdirectory `meson.build` files)
74+
are the source of truth for which files to compile.
75+
76+
4. Update version strings in `build.rs`: Update the `__PICOLIBC_VERSION__`,
77+
`__PICOLIBC__`, `__PICOLIBC_MINOR__`, `__PICOLIBC_PATCHLEVEL__`,
78+
`_NEWLIB_VERSION`, and related defines in `gen_config_file()`.
79+
80+
5. Update `NOTICE.txt`: Bump the version number in the picolibc entry.
81+
82+
6. Build and test:
83+
```bash
84+
just guests
85+
just test
86+
```

src/hyperlight_guest/src/error.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ limitations under the License.
1717
use alloc::format;
1818
use alloc::string::{String, ToString as _};
1919

20+
use anyhow;
2021
use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode;
2122
use hyperlight_common::func::Error as FuncError;
22-
use {anyhow, serde_json};
23+
use serde_json;
2324

2425
pub type Result<T> = core::result::Result<T, HyperlightGuestError>;
2526

src/hyperlight_guest_bin/build.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,6 @@ fn gen_config_file(config_dir: &Path) -> Result<()> {
157157
let has = if detect_cc_feature(code)? { 1 } else { 0 };
158158
writeln!(file, "#define __HAVE_COMPLEX {has}")?;
159159

160-
let code = r#"long long x = 0; int main() { return 0; }"#;
161-
let has = if detect_cc_feature(code)? { 1 } else { 0 };
162-
writeln!(file, "#define __IO_LONG_LONG {has}")?;
163-
164160
// Static undefs
165161
writeln!(file, "#undef __ARM_SEMIHOST")?; // -Dsemihost=false
166162
writeln!(file, "#undef __SEMIHOST")?; // -Dsemihost=false
@@ -294,7 +290,6 @@ fn add_libc(build: &mut cc::Build, picolibc_dir: &Path, target: &str) -> Result<
294290
build.file(&source_path);
295291
}
296292

297-
build.file("c/clock.c");
298293
Ok(())
299294
}
300295

src/hyperlight_guest_bin/c/clock.c

Lines changed: 0 additions & 38 deletions
This file was deleted.

src/hyperlight_guest_bin/src/host_bridge.rs

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
use alloc::string::ToString;
17+
use alloc::string::String;
1818
use alloc::vec;
1919
use alloc::vec::Vec;
2020
use core::ffi::*;
@@ -34,14 +34,53 @@ fn set_errno(val: c_int) {
3434
}
3535

3636
// POSIX errno values (matching picolibc sys/errno.h)
37+
const EINVAL: c_int = 22;
3738
const EIO: c_int = 5;
3839
const EBADF: c_int = 9;
3940
const ENOSYS: c_int = 88;
4041

42+
// picolibc clock IDs (from time.h)
43+
const CLOCK_REALTIME: c_ulong = 1;
44+
const CLOCK_MONOTONIC: c_ulong = 4;
45+
4146
static CURRENT_TIME: AtomicU64 = AtomicU64::new(0);
4247

48+
/// Matches picolibc `struct timespec` layout for x86_64.
49+
#[repr(C)]
50+
struct Timespec {
51+
tv_sec: c_long,
52+
tv_nsec: c_long,
53+
}
54+
55+
/// Matches picolibc `struct timeval` layout for x86_64.
56+
#[repr(C)]
57+
struct Timeval {
58+
tv_sec: c_long,
59+
tv_usec: c_long,
60+
}
61+
62+
/// Retrieves the current time from the host as (seconds, nanoseconds).
63+
fn current_time() -> (u64, u64) {
64+
let bytes = call_host_function::<Vec<u8>>("CurrentTime", Some(vec![]), ReturnType::VecBytes)
65+
.unwrap_or_default();
66+
67+
if bytes.len() == 16 {
68+
let secs = u64::from_ne_bytes(bytes[0..8].try_into().unwrap());
69+
let nanos = u64::from_ne_bytes(bytes[8..16].try_into().unwrap());
70+
(secs, nanos)
71+
} else {
72+
let secs = 1609459200 + CURRENT_TIME.fetch_add(1, Ordering::Relaxed);
73+
(secs, 0)
74+
}
75+
}
76+
4377
#[unsafe(no_mangle)]
4478
pub extern "C" fn read(fd: c_int, buf: *mut c_void, count: usize) -> isize {
79+
if buf.is_null() && count > 0 {
80+
set_errno(EINVAL);
81+
return -1;
82+
}
83+
4584
if fd != 0 {
4685
set_errno(EBADF);
4786
return -1;
@@ -53,7 +92,7 @@ pub extern "C" fn read(fd: c_int, buf: *mut c_void, count: usize) -> isize {
5392
ReturnType::VecBytes,
5493
) {
5594
Ok(bytes) => {
56-
let n = bytes.len();
95+
let n = bytes.len().min(count);
5796
unsafe {
5897
core::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, n);
5998
}
@@ -68,16 +107,21 @@ pub extern "C" fn read(fd: c_int, buf: *mut c_void, count: usize) -> isize {
68107

69108
#[unsafe(no_mangle)]
70109
pub extern "C" fn write(fd: c_int, buf: *const c_void, count: usize) -> isize {
110+
if buf.is_null() && count > 0 {
111+
set_errno(EINVAL);
112+
return -1;
113+
}
114+
71115
if fd != 1 && fd != 2 {
72116
set_errno(EBADF);
73117
return -1;
74118
}
75119

76120
let slice = unsafe { core::slice::from_raw_parts(buf as *const u8, count) };
77-
let s = core::str::from_utf8(slice).unwrap_or("<invalid utf8>");
121+
let s = String::from_utf8_lossy(slice);
78122
match call_host_function::<i32>(
79123
"HostPrint",
80-
Some(vec![ParameterValue::String(s.to_string())]),
124+
Some(vec![ParameterValue::String(s.into_owned())]),
81125
ReturnType::Int,
82126
) {
83127
Ok(_) => count as isize,
@@ -90,24 +134,51 @@ pub extern "C" fn write(fd: c_int, buf: *const c_void, count: usize) -> isize {
90134

91135
#[unsafe(no_mangle)]
92136
pub extern "C" fn _current_time(ts: *mut u64) -> c_int {
93-
let bytes = call_host_function::<Vec<u8>>("CurrentTime", Some(vec![]), ReturnType::VecBytes)
94-
.unwrap_or_default();
95-
96-
let (secs, nanos) = if bytes.len() == 16 {
97-
let secs = u64::from_ne_bytes(bytes[0..8].try_into().unwrap());
98-
let nanos = u64::from_ne_bytes(bytes[8..16].try_into().unwrap());
99-
(secs, nanos)
100-
} else {
101-
let secs = 1609459200 + CURRENT_TIME.fetch_add(1, Ordering::Relaxed);
102-
(secs, 0)
103-
};
104-
137+
let (secs, nanos) = current_time();
105138
let ts = unsafe { core::slice::from_raw_parts_mut(ts, 2) };
106139
ts[0] = secs;
107140
ts[1] = nanos;
108141
0
109142
}
110143

144+
#[unsafe(no_mangle)]
145+
pub extern "C" fn clock_gettime(clk_id: c_ulong, tp: *mut Timespec) -> c_int {
146+
if tp.is_null() {
147+
set_errno(EINVAL);
148+
return -1;
149+
}
150+
151+
match clk_id {
152+
CLOCK_REALTIME | CLOCK_MONOTONIC => {
153+
let (secs, nanos) = current_time();
154+
unsafe {
155+
(*tp).tv_sec = secs as c_long;
156+
(*tp).tv_nsec = nanos as c_long;
157+
}
158+
0
159+
}
160+
_ => {
161+
set_errno(EINVAL);
162+
-1
163+
}
164+
}
165+
}
166+
167+
#[unsafe(no_mangle)]
168+
pub extern "C" fn gettimeofday(tv: *mut Timeval, _tz: *mut c_void) -> c_int {
169+
if tv.is_null() {
170+
set_errno(EINVAL);
171+
return -1;
172+
}
173+
174+
let (secs, nanos) = current_time();
175+
unsafe {
176+
(*tv).tv_sec = secs as c_long;
177+
(*tv).tv_usec = (nanos / 1000) as c_long;
178+
}
179+
0
180+
}
181+
111182
#[unsafe(no_mangle)]
112183
pub extern "C" fn _exit(ec: c_int) -> ! {
113184
hyperlight_guest::exit::abort_with_code(&[ec as u8]);

src/hyperlight_guest_bin/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ pub mod host_comm;
5151
pub mod memory;
5252
pub mod paging;
5353

54+
/// Bridge between picolibc's POSIX expectations and the Hyperlight host.
5455
#[cfg(feature = "libc")]
5556
mod host_bridge;
5657

0 commit comments

Comments
 (0)