Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stdio support for UEFI #116207

Merged
merged 2 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion library/std/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@
all(target_vendor = "fortanix", target_env = "sgx"),
feature(slice_index_methods, coerce_unsized, sgx_platform)
)]
#![cfg_attr(windows, feature(round_char_boundary))]
#![cfg_attr(any(windows, target_os = "uefi"), feature(round_char_boundary))]
#![cfg_attr(target_os = "xous", feature(slice_ptr_len))]
//
// Language features:
Expand Down
1 change: 0 additions & 1 deletion library/std/src/sys/uefi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ pub mod path;
pub mod pipe;
#[path = "../unsupported/process.rs"]
pub mod process;
#[path = "../unsupported/stdio.rs"]
pub mod stdio;
#[path = "../unsupported/thread.rs"]
pub mod thread;
Expand Down
162 changes: 162 additions & 0 deletions library/std/src/sys/uefi/stdio.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
use crate::io;
use crate::iter::Iterator;
use crate::mem::MaybeUninit;
use crate::os::uefi;
use crate::ptr::NonNull;

const MAX_BUFFER_SIZE: usize = 8192;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this sized based on 2 pages, rather than the more common single page size?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, that size was picked up from Windows implementation. I am open to a more appropriate size.

Copy link
Contributor

Choose a reason for hiding this comment

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

The windows backend picked 8k due to behavior of the windows system libraries (also see f411852). I don't think any of this reasoning applies to UEFI.


pub struct Stdin;
pub struct Stdout;
pub struct Stderr;

impl Stdin {
pub const fn new() -> Stdin {
Stdin
}
}

impl io::Read for Stdin {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let st: NonNull<r_efi::efi::SystemTable> = uefi::env::system_table().cast();
let stdin = unsafe { (*st.as_ptr()).con_in };

// Try reading any pending data
let inp = match read_key_stroke(stdin) {
Ok(x) => x,
Err(e) if e == r_efi::efi::Status::NOT_READY => {
// Wait for keypress for new data
wait_stdin(stdin)?;
read_key_stroke(stdin).map_err(|x| io::Error::from_raw_os_error(x.as_usize()))?
Copy link
Contributor

Choose a reason for hiding this comment

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

Any particular reason you do not loop on NOT_READY? Can we rely on wait_stdin to never report spurious wakeups?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, I am not sure busy waiting would be the best idea. In all the examples of reading I have seen online, everyone seems to rely on the con_in->wait_for_key event.
The spec also states the following: "Event to use with EFI_BOOT_SERVICES.WaitForEvent() to wait for a key to be available." So it should be the best solution.

Copy link
Contributor

Choose a reason for hiding this comment

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

I am not suggesting busy-waiting. I am saying to loop on NOT_READY. So after wait_stdin(), you still check for NOT_READY and wait again if it keeps getting reported.

}
Err(e) => {
return Err(io::Error::from_raw_os_error(e.as_usize()));
}
};

// Check if the key is printiable character
if inp.scan_code != 0x00 {
return Err(io::const_io_error!(io::ErrorKind::Interrupted, "Special Key Press"));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you explain why you strip zeros?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not stripping zeros, scan_code not 0 indicates a non-printable character. See Section 12.3.3 in UEFI Spec

Copy link
Contributor

Choose a reason for hiding this comment

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

unicode_char == 0x00 represents control characters. The scan-code is always set, yet you check the scan-code rather than unicode_char.

If I am not mistaken, your current code suppresses all input.

The ReadKeyStroke() function reads the next keystroke from the input device. If there is no pending keystroke the
function returns EFI_NOT_READY. If there is a pending keystroke, then ScanCode is the EFI scan code defined in
EFI Scan Codes for EFI_SIMPLE_TEXT_INPUT_PROTOCOL . The UnicodeChar is the actual printable character or
is zero if the key does not represent a printable character (control key, function key, etc.)


// SAFETY: Iterator will have only 1 character since we are reading only 1 Key
// SAFETY: This character will always be UCS-2 and thus no surrogates.
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 it is overly optimistic to rely on firmware never returning invalid data. I would prefer to forward errors, but I will not insist.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since we are reading a printable key press (due to the above check), I would hope it would be valid. But since it is possible to do file piping for input, I guess it might be good to return an error.

let ch: char = char::decode_utf16([inp.unicode_char]).next().unwrap().unwrap();
if ch.len_utf8() > buf.len() {
return Ok(0);
Copy link
Contributor

Choose a reason for hiding this comment

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

You drop data if the buffer is short. This is really unfortunate. I think the better alternative would be to have an internal buffer in struct Stdin where you keep the last char and keep trying to return it to the caller until they provide a suitable buffer. This does busy-loop if the caller never provides a suitable buffer, but at least you do not drop data silently.

Alternatively, return exactly the requested amount of bytes, and return the remaining ones in following calls. There is no reason why the caller would expect every read-call to split exactly at UTF8-boundaries, or is there?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it seems like a reasonable strategy. I think an internal buffer of a single character (u16) should be enough.

}

ch.encode_utf8(buf);

Ok(ch.len_utf8())
}
}

impl Stdout {
pub const fn new() -> Stdout {
Stdout
}
}

impl io::Write for Stdout {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let st: NonNull<r_efi::efi::SystemTable> = uefi::env::system_table().cast();
let stdout = unsafe { (*st.as_ptr()).con_out };

write(stdout, buf)
}

fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}

impl Stderr {
pub const fn new() -> Stderr {
Stderr
}
}

impl io::Write for Stderr {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let st: NonNull<r_efi::efi::SystemTable> = uefi::env::system_table().cast();
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not error out if the system-table is not available?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So the current implementation has a default behaviour to panic if system table (and brothers) is not available. The exceptions are when the function might be called before sys::init.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I know. I want to understand what you want the behavior to be when it is called before sys::init? You check for validity in the panic-path, but not here, which seems inconsistent. I am simply trying to understand what you want it to behave like?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So the basic assumptions is that SystemTable and ImageHandle are always valid except in panic-path. Thus, anything that uses Stderr outside of panic-path should panic if it is unavailable.

let stderr = unsafe { (*st.as_ptr()).std_err };

write(stderr, buf)
}

fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}

// UCS-2 character should occupy 3 bytes at most in UTF-8
pub const STDIN_BUF_SIZE: usize = 3;

pub fn is_ebadf(_err: &io::Error) -> bool {
true
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the reasoning here? You effectively fold every error to the default error by treating everything as EBADF, or what am I missing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's just the default unsupported implementation. I am not sure what EBADF corresponds to in UEFI. Feel free to comment about it.

Copy link
Contributor

Choose a reason for hiding this comment

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

is_ebadf() is used to suppress EBADF errors, and avoid panicking on println!() if the output-stream does not exist. Your current implementation for UEFI causes ALL errors for output-streams to be suppressed, because you unconditionally return true.

There is no equivalent to EBADF on UEFI, since you operate on devices directly, rather than on virtualized FDs. However, you likely want to suppress EFI_UNSUPPORTED, since this is triggered when text-mode is currently disabled. But is_ebadf() seems like the wrong place to do this.

}

pub fn panic_output() -> Option<impl io::Write> {
uefi::env::try_system_table().map(|_| Stderr::new())
Copy link
Contributor

Choose a reason for hiding this comment

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

Why checking for the system-table here? It can be invalidated in a parallel call.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not sure what you mean. My intention was to check if the code panics before the system table is initialized in Rust (which can happen) or after it. It is not really meant to check if system table is valid for complete UEFI.

Copy link
Contributor

Choose a reason for hiding this comment

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

But why do you check for it? What is the rationale?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To avoid panic inside panic.

}

fn write(
protocol: *mut r_efi::protocols::simple_text_output::Protocol,
buf: &[u8],
) -> io::Result<usize> {
let mut utf16 = [0; MAX_BUFFER_SIZE / 2];
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe use an explicit type-annotation here? You rely on the size of the type by hard-coding 2, yet no explicit type is used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I guess it can be changed to size_of::<u16>(). I started with the windows implementation so it is from that.

Copy link
Contributor

Choose a reason for hiding this comment

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

I am not talking about the sizeof_of, I am talking about the type-annotation of utf16 (let mut utf16: u16 = ...).


// Get valid UTF-8 buffer
let utf8 = match crate::str::from_utf8(buf) {
Ok(x) => x,
Err(e) => unsafe { crate::str::from_utf8_unchecked(&buf[..e.valid_up_to()]) },
};
// Clip UTF-8 buffer to max UTF-16 buffer we support
let utf8 = &utf8[..utf8.floor_char_boundary(utf16.len() - 1)];
Copy link
Contributor

Choose a reason for hiding this comment

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

The - 1 should be dropped. floor_char_boundary() will correctly use the entire string if the index is beyond the last character.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So the -1 is so that the string is NULL terminated.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fair enough. Though, I think this is really not obvious from this code. And simple_text_output() takes a slice but silently expects it to be NULL-terminated. I think this is very likely to lead to problems. I would prefer at least an assertion in simple_text_output() to catch such errors in the future.


for (i, ch) in utf8.encode_utf16().enumerate() {
utf16[i] = ch;
}
Comment on lines +118 to +120
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not .collect::<u16>() and drop the explicit buffer? It also reduces stack pressure, which is an issue on UEFI systems.

Copy link
Contributor Author

@Ayush1325 Ayush1325 Oct 6, 2023

Choose a reason for hiding this comment

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

I agree about reducing the stack pressure but I think it would still be better to use stack than heap.
Note: It is fine to write less than the supplied buffer. It will be retried, not dropped.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do you want to explain why?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, I am not sure about heap size limitations in UEFI. However, I would imagine they are much less than normal OS. Also, the UEFI heap is rather simple compared to the os heap. A Rust application can end up writing a lot of data to stdio at once if there are no limits.

I do not mind using the heap itself, but I would prefer specifying the max size of the buffer, even on the heap.


unsafe { simple_text_output(protocol, &mut utf16) }?;

Ok(utf8.len())
}

unsafe fn simple_text_output(
protocol: *mut r_efi::protocols::simple_text_output::Protocol,
buf: &mut [u16],
) -> io::Result<()> {
let res = unsafe { ((*protocol).output_string)(protocol, buf.as_mut_ptr()) };
if res.is_error() { Err(io::Error::from_raw_os_error(res.as_usize())) } else { Ok(()) }
}

fn wait_stdin(stdin: *mut r_efi::protocols::simple_text_input::Protocol) -> io::Result<()> {
let boot_services: NonNull<r_efi::efi::BootServices> =
uefi::env::boot_services().unwrap().cast();
let wait_for_event = unsafe { (*boot_services.as_ptr()).wait_for_event };
let wait_for_key_event = unsafe { (*stdin).wait_for_key };

let r = {
let mut x: usize = 0;
(wait_for_event)(1, [wait_for_key_event].as_mut_ptr(), &mut x)
};
if r.is_error() { Err(io::Error::from_raw_os_error(r.as_usize())) } else { Ok(()) }
}

fn read_key_stroke(
stdin: *mut r_efi::protocols::simple_text_input::Protocol,
) -> Result<r_efi::protocols::simple_text_input::InputKey, r_efi::efi::Status> {
let mut input_key: MaybeUninit<r_efi::protocols::simple_text_input::InputKey> =
MaybeUninit::uninit();

let r = unsafe { ((*stdin).read_key_stroke)(stdin, input_key.as_mut_ptr()) };

if r.is_error() {
Err(r)
} else {
let input_key = unsafe { input_key.assume_init() };
Ok(input_key)
}
}
8 changes: 7 additions & 1 deletion src/doc/rustc/src/platform-support/unknown-uefi.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,12 @@ cargo build --target x86_64-unknown-uefi -Zbuild-std=std,panic_abort
#### os_str
- While the strings in UEFI should be valid UCS-2, in practice, many implementations just do not care and use UTF-16 strings.
- Thus, the current implementation supports full UTF-16 strings.
#### stdio
- Uses `Simple Text Input Protocol` and `Simple Text Output Protocol`.
- Note: UEFI uses CRLF for new line. This means Enter key is registered as CR instead of LF.

## Example: Hello World With std
The following code features a valid UEFI application, including stdio and `alloc` (`OsString` and `Vec`):
The following code features a valid UEFI application, including `stdio` and `alloc` (`OsString` and `Vec`):

This example can be compiled as binary crate via `cargo` using the toolchain
compiled from the above source (named custom):
Expand All @@ -286,6 +289,9 @@ use std::{
};

pub fn main() {
println!("Starting Rust Application...");

// Use System Table Directly
let st = env::system_table().as_ptr() as *mut efi::SystemTable;
let mut s: Vec<u16> = OsString::from("Hello World!\n").encode_wide().collect();
s.push(0);
Expand Down
Loading