Skip to content

feat(guest): replace musl with picolibc#831

Open
andreiltd wants to merge 6 commits intohyperlight-dev:mainfrom
andreiltd:libc-takeover
Open

feat(guest): replace musl with picolibc#831
andreiltd wants to merge 6 commits intohyperlight-dev:mainfrom
andreiltd:libc-takeover

Conversation

@andreiltd
Copy link
Member

@andreiltd andreiltd commented Aug 28, 2025

This patch removes custom musl implementation used by the guest and replace it with picolibc v1.8.11.

Note: The picolibc submodule uses sparse checkout to exclude GPL/AGPL-licensed test and script files that are not needed for building. Only BSD/MIT/permissive-licensed source files are included.

@andreiltd andreiltd marked this pull request as draft August 28, 2025 11:48
@andreiltd andreiltd added the kind/enhancement For PRs adding features, improving functionality, docs, tests, etc. label Aug 28, 2025
@andreiltd andreiltd force-pushed the libc-takeover branch 6 times, most recently from eaf1d54 to b18a374 Compare August 28, 2025 13:44
Copy link
Contributor

@jprendes jprendes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this PR!

@jsturtevant
Copy link
Contributor

will help with #282

@andreiltd andreiltd force-pushed the libc-takeover branch 2 times, most recently from c24ce45 to d9fb9e5 Compare February 25, 2026 11:33
Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com>
@andreiltd andreiltd force-pushed the libc-takeover branch 2 times, most recently from e4caabe to 7b99307 Compare February 25, 2026 17:59
Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com>
@andreiltd andreiltd marked this pull request as ready for review February 26, 2026 20:11
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did we write this file? is there attribution that is needed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is really necessary, I think @jprendes just wrote a Rust implementation for this in hyperlight-js. maybe we could use it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, those are ours. We cannot use the implementation from hyperlight-js directly, it serves a bit different purpose. That being said I rewrote picolibc stubs to rust. The tradeoff is that some of the types and defines that you normally get from libc headers have to be redifined in Rust with compatible ABI. I think that is OK -- they have to be backwards compatible and I don't see them change anytime soon.

(void)__tz;

_current_time(current_time);
tv->tv_sec = current_time[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to check for null here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's now checked in Rust implementation.

case CLOCK_REALTIME:
case CLOCK_MONOTONIC:
_current_time(current_time);
tp->tv_sec = current_time[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's now checked in Rust implementation.

Comment on lines +52 to +58
Some(vec![ParameterValue::ULong(count as u64)]),
ReturnType::VecBytes,
) {
Ok(bytes) => {
let n = bytes.len();
unsafe {
core::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, n);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should not rely on the host provided length. nonoverlapping could write past the buffer lenghth. We are in the guest here so it would just cause corruption but might be tough to track down I think

return -1;
}

let slice = unsafe { core::slice::from_raw_parts(buf as *const u8, count) };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need a null check on the buf here? https://doc.rust-lang.org/core/slice/fn.from_raw_parts.html#safety

data must be non-null and aligned even for zero-length slices

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's now checked in Rust implementation.

obstacle to adoption, that text has been removed.
Note: The picolibc submodule uses sparse checkout to exclude
GPL/AGPL-licensed test and script files that are not needed for
building. Only BSD/MIT/permissive-licensed source files are included.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wanted to surface this. We should include it in the PR description I think

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the sparsity pattern that makes this actually happen?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@dblnz dblnz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work, Tomasz!! This is one hell of a diff and I like it 😆

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is really necessary, I think @jprendes just wrote a Rust implementation for this in hyperlight-js. maybe we could use it?


extern void _current_time(uint64_t *ts);

int gettimeofday(struct timeval *__restrict tv, void *__restrict __tz) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought: If we were running bindgen on picolib headers, then we could easily implement this one in rust instead of C. IIUC, the main issue is the definition of timeval, clockid_t, and timespec in Rust, that we don't want to get wrong.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is no longer in the diff.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we were running bindgen on picolib headers, then we could easily implement this one in rust

That would be correct yes, but I would rather much have a single c file defining the interface or redefine types in Rust with correct layout. It's hard to justify running the whole bindgen machinery for few u64.

ReturnType::VecBytes,
) {
Ok(bytes) => {
let n = bytes.len();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe check that bytes.lenth() <= count?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think the timing is a bit unfortunate, could you check if your comments are resolved in the newest diff?

) {
Ok(bytes) => {
let n = bytes.len();
unsafe {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check that n <= count

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fixed

}

let slice = unsafe { core::slice::from_raw_parts(buf as *const u8, count) };
let s = core::str::from_utf8(slice).unwrap_or("<invalid utf8>");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would like to insist on the from_utf8_lossy, as mentioned we are allocationg anyway when doing s.to_string()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fixed

@syntactically
Copy link
Member

Is the licensing approval finally done for this, making it ready for review/merge?


#define GUEST_SCRATCH_SIZE (0x40000) // default scratch size
#define MAX_BUFFER_SIZE (1024)

#define printf_f(fmt, ...) \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to see if we can either build picolibc in a configuration without stdio buffers, or to automatically do a flush on the guest-function-call-complete path?

I feel like the buffered-I/O-by-default regime will be very confusing to users---where, for example, if you are doing the "call a function and then restore the sandbox" pattern might often not see any output at all.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think we can disable posix console feature: https://github.com/picolibc/picolibc/blob/main/doc/build.md#stdio-options

Fuzzy on the details but IIRC that will change to char by char sending to host which I thought was insane.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that makes sense... char-by-char is definitely unfortunate. Flush-on-guest-function-return would be nice if we could coalesce the flush exit and the function-return exit, but I'm not sure how easy that is?


#[unsafe(no_mangle)]
pub extern "C" fn _current_time(ts: *mut u64) -> c_int {
let bytes = call_host_function::<Vec<u8>>("CurrentTime", Some(vec![]), ReturnType::VecBytes)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, I think I might like to see this fail at least by default? As with my comments on #1173, I'm against exposing a clock, especially a wall clock, to the guest by default.

Copy link
Member Author

@andreiltd andreiltd Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very common dependency, for example spidermonkey crashes immediately if we don't provide some time interface. Do we really want to error or at least return an unix epoch?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting... I didn't realise so many things were using wall-clock time. Do you know what we were doing for spidermonkey before? I don't think we did actually have the real time?

I wouldn't be against having it always be the epoch personally (at least by default), but I'll let others chime in here as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at some point we had a fallback where we took some epoch (2000?) and added 1s or 1ms for each call

static CURRENT_TIME: AtomicU64 = AtomicU64::new(0);

#[unsafe(no_mangle)]
pub extern "C" fn read(fd: c_int, buf: *mut c_void, count: usize) -> isize {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are the fds referenced in these functions allocated? I don't see an open, so are these always either 0/1/2 for stdin/out/err? I think it might make sense to make the behaviour a bit more limited (maybe reads from fd0 always return eof; writes to fd1 are HostPrint, and writes to fd2 are debug_print, or something like that?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's almost what you have here, right? except that:

  • fd2 also goes to HostPrint instead of debug_print (I didn't know debug_print was a thing!).
  • fd0 first tries HostRead, if that's not present returns -1 and sets errno = EIO (instead of EOF, which would be returning 0 and no errno)
  • any other fd is EBADF

So, do you suggest these changes?

  • fd2 to use debug_print instead of HostPrint
  • fd0 to return 0 (and untouched errno) in case of no HostRead

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't care much about the fd2---it was just the first thing that sprang to mind.

Unless we have a compelling use case, I think I would prefer for read from fd0 to unconditionally be eof, without probing for a host function which seems a bit surprising and magical. If we ever have C code that needs to be able to read() from stdin, we could add some hyperlight_guest_bin API to let a little Rust wrapper code install its own read function.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to go with that logic. @jprendes any objections?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can call fflush(NULL) and that flushes all open FDs.
Can we leave buffering and add that on VM exit (or similar) in an upcoming PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also I think our current _putchar impl can go away, right?

Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com>
Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com>
Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com>
Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com>
Copy link
Contributor

@dblnz dblnz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work, Tomasz!
I have nothing else to add.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great! Thanks for adding this description!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/enhancement For PRs adding features, improving functionality, docs, tests, etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants