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

Add linux_raw opt-in backend #572

Merged
merged 10 commits into from
Mar 3, 2025
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
26 changes: 26 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,32 @@ jobs:
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="rdrand"
run: cargo build --features=std

linux-raw:
name: Build Raw Linux
runs-on: ubuntu-24.04
strategy:
matrix:
target: [
arm-unknown-linux-gnueabihf,
aarch64-unknown-linux-gnu,
loongarch64-unknown-linux-gnu,
riscv32gc-unknown-linux-gnu,
riscv64gc-unknown-linux-gnu,
s390x-unknown-linux-gnu,
i686-unknown-linux-gnu,
x86_64-unknown-linux-gnu,
x86_64-unknown-linux-gnux32,
]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2025-02-15
components: rust-src
- env:
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="linux_raw"
run: cargo build -Zbuild-std=core --target=${{ matrix.target }}

web:
name: ${{ matrix.target.description }} ${{ matrix.feature.description }} ${{ matrix.atomic.description }}
runs-on: ubuntu-24.04
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/nopanic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ jobs:
- name: Check (getrandom.rs)
run: (exit $( grep -c panic target/release/libgetrandom_wrapper.so ))

- name: Build (linux_raw.rs)
env:
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="linux_raw"
run: cargo build --release
- name: Check (linux_raw.rs)
run: (exit $( grep -c panic target/release/libgetrandom_wrapper.so ))

- name: Build (rdrand.rs)
env:
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="rdrand"
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ jobs:
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="linux_getrandom"
RUSTDOCFLAGS: -Dwarnings --cfg getrandom_backend="linux_getrandom"
run: cargo test --target=${{ matrix.target }} --features=std
- env:
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="linux_raw"
RUSTDOCFLAGS: -Dwarnings --cfg getrandom_backend="linux_raw"
run: cargo test --target=${{ matrix.target }} --features=std
- env:
RUSTFLAGS: -Dwarnings --cfg getrandom_test_linux_fallback
RUSTDOCFLAGS: -Dwarnings --cfg getrandom_test_linux_fallback
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/workspace.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ jobs:
run: cargo clippy --target x86_64-unknown-linux-gnu
- name: Linux (linux_android_with_fallback.rs)
run: cargo clippy --target x86_64-unknown-linux-gnu
- name: Linux (linux_raw.rs)
env:
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="linux_raw"
run: cargo clippy --target x86_64-unknown-linux-gnu
- name: NetBSD (netbsd.rs)
run: cargo clippy -Zbuild-std=core --target x86_64-unknown-netbsd
- name: Fortranix SGX (rdrand.rs)
Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ compiler_builtins = { version = "0.1", optional = true }
core = { version = "1.0", optional = true, package = "rustc-std-workspace-core" }

# getrandom / linux_android_with_fallback
[target.'cfg(all(any(target_os = "linux", target_os = "android"), not(any(getrandom_backend = "custom", getrandom_backend = "rdrand", getrandom_backend = "rndr"))))'.dependencies]
[target.'cfg(all(any(target_os = "linux", target_os = "android"), not(any(getrandom_backend = "custom", getrandom_backend = "linux_raw", getrandom_backend = "rdrand", getrandom_backend = "rndr"))))'.dependencies]
libc = { version = "0.2.154", default-features = false }

# apple-other
Expand Down Expand Up @@ -81,7 +81,7 @@ wasm-bindgen-test = "0.3"
[lints.rust.unexpected_cfgs]
level = "warn"
check-cfg = [
'cfg(getrandom_backend, values("custom", "rdrand", "rndr", "linux_getrandom", "wasm_js"))',
'cfg(getrandom_backend, values("custom", "rdrand", "rndr", "linux_getrandom", "linux_raw", "wasm_js"))',
'cfg(getrandom_msan)',
'cfg(getrandom_test_linux_fallback)',
'cfg(getrandom_test_linux_without_fallback)',
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ of randomness based on their specific needs:
| Backend name | Target | Target Triple | Implementation
| ----------------- | -------------------- | ------------------------ | --------------
| `linux_getrandom` | Linux, Android | `*‑linux‑*` | [`getrandom`][1] system call (without `/dev/urandom` fallback). Bumps minimum supported Linux kernel version to 3.17 and Android API level to 23 (Marshmallow).
| `linux_raw` | Linux, Android | `*‑linux‑*` | Same as `linux_getrandom`, but uses raw `asm!`-based syscalls instead of `libc`.
| `rdrand` | x86, x86-64 | `x86_64-*`, `i686-*` | [`RDRAND`] instruction
| `rndr` | AArch64 | `aarch64-*` | [`RNDR`] register
| `wasm_js` | Web Browser, Node.js | `wasm32‑unknown‑unknown`, `wasm32v1-none` | [`Crypto.getRandomValues`]. Requires feature `wasm_js` ([see below](#webassembly-support)).
Expand Down Expand Up @@ -110,6 +111,15 @@ WILL NOT have any effect on its downstream users.

[`.cargo/config.toml`]: https://doc.rust-lang.org/cargo/reference/config.html

### Raw Linux syscall support

Currently the `linux_raw` backend supports only targets with stabilized `asm!` macro,
i.e. `arm`, `aarch64`, `loongarch64`, `riscv32`, `riscv64`, `s390x`, `x86`, and `x86_64`.

Note that the raw syscall backend may be slower than backends based on `libc::getrandom`,
e.g. it does not implement vDSO optimizations and on `x86` it uses the infamously slow
`int 0x80` instruction to perform syscall.

### WebAssembly support

This crate fully supports the [WASI] and [Emscripten] targets. However,
Expand Down
3 changes: 3 additions & 0 deletions src/backends.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ cfg_if! {
} else if #[cfg(getrandom_backend = "linux_getrandom")] {
mod getrandom;
pub use getrandom::*;
} else if #[cfg(getrandom_backend = "linux_raw")] {
mod linux_raw;
pub use linux_raw::*;
} else if #[cfg(getrandom_backend = "rdrand")] {
mod rdrand;
pub use rdrand::*;
Expand Down
139 changes: 139 additions & 0 deletions src/backends/linux_raw.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//! Implementation for Linux / Android using `asm!`-based syscalls.
use crate::{Error, MaybeUninit};

pub use crate::util::{inner_u32, inner_u64};

#[cfg(not(any(target_os = "android", target_os = "linux")))]
Copy link
Member

Choose a reason for hiding this comment

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

I'm trying to figure out what the constraints are around directly invoking syscalls on Android. I can try to check with some people at work, but it's unclear if bionic libc (the libc shipped with Android) does any special handling for syscalls. If the only demand for this is on linux targets, it might just be best to initially exclude Android.

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, it would be nice to verify that raw syscalls work on Android without any surprises.

Here is a relevant rustix discussion: bytecodealliance/rustix#1095 No one has mentioned any fundamental technical obstacles to using raw syscalls and it looks like some people use them in practice.

Copy link
Member Author

Choose a reason for hiding this comment

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

Since users have to explicitly enable the backend, I think we can leave it as-is and potentially disable it later if it will be found problematic on Android for some reason.

compile_error!("`linux_raw` backend can be enabled only for Linux/Android targets!");

#[allow(non_upper_case_globals)]
unsafe fn getrandom_syscall(buf: *mut u8, buflen: usize, flags: u32) -> isize {
let r0;

// Based on `rustix` and `linux-raw-sys` code.
cfg_if! {
if #[cfg(target_arch = "arm")] {
const __NR_getrandom: u32 = 384;
// In thumb-mode, r7 is the frame pointer and is not permitted to be used in
// an inline asm operand, so we have to use a different register and copy it
// into r7 inside the inline asm.
// Theoretically, we could detect thumb mode in the build script, but several
// register moves are cheap enough compared to the syscall cost, so we do not
// bother with it.
core::arch::asm!(
"mov {tmp}, r7",
"mov r7, {nr}",
"svc 0",
Copy link
Member

Choose a reason for hiding this comment

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

Looking at the syscall(2) documentation it seems like there (might) be a difference between EABI / OABI targets for 32-bit arm. Also, that documentation suggests that 32-bit arm should use the swi instruction.

Personally, I would be fine omitting 32-bit arm support (similar to x86 support). It's more complicated, and there isn't a 32-bit -unknown-linux-none target.

Copy link
Member

@josephlr josephlr Feb 14, 2025

Choose a reason for hiding this comment

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

Looks like swi is the same as svc (https://developer.arm.com/documentation/ddi0406/cb/Application-Level-Architecture/Instruction-Details/Alphabetical-list-of-instructions/SVC--previously-SWI-?lang=en) I'm still confused about the EABI vs OABI stuff though.

I think this code would be fine provided we simply confirm (via cfgs) that we are targeting eabi.

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 code is based on rustix and it does not look like they differentiate between EABI / OABI: https://github.com/bytecodealliance/rustix/blob/main/src/backend/linux_raw/arch/mod.rs#L27-L28

After a brief search I couldn't find any relevant information about OABI support in Rust. Are you sure that Rust supports it in the first place? For example, it looks like GCC has dropped OABI support completely.

"mov r7, {tmp}",
nr = const __NR_getrandom,
tmp = out(reg) _,
inlateout("r0") buf => r0,
in("r1") buflen,
in("r2") flags,
options(nostack, preserves_flags)
);
} else if #[cfg(target_arch = "aarch64")] {
const __NR_getrandom: u32 = 278;
core::arch::asm!(
"svc 0",
in("x8") __NR_getrandom,
inlateout("x0") buf => r0,
in("x1") buflen,
in("x2") flags,
options(nostack, preserves_flags)
);
} else if #[cfg(target_arch = "loongarch64")] {
const __NR_getrandom: u32 = 278;
core::arch::asm!(
"syscall 0",
in("$a7") __NR_getrandom,
inlateout("$a0") buf => r0,
in("$a1") buflen,
in("$a2") flags,
options(nostack, preserves_flags)
);
} else if #[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] {
const __NR_getrandom: u32 = 278;
core::arch::asm!(
"ecall",
in("a7") __NR_getrandom,
inlateout("a0") buf => r0,
in("a1") buflen,
in("a2") flags,
options(nostack, preserves_flags)
);
} else if #[cfg(target_arch = "s390x")] {
const __NR_getrandom: u32 = 349;
core::arch::asm!(
"svc 0",
in("r1") __NR_getrandom,
inlateout("r2") buf => r0,
in("r3") buflen,
in("r4") flags,
options(nostack, preserves_flags)
);
} else if #[cfg(target_arch = "x86")] {
Copy link
Member

Choose a reason for hiding this comment

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

Give that there is no i686-unknown-linux-none target and invoking raw syscalls on x86 is a notorious foot-gun, could we just not support 32-bit x86? It seems easier and then we don't have to have warnings saying "you probably shouldn't use this backend"

Copy link
Member Author

Choose a reason for hiding this comment

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

IIRC the only problem with int 0x80 is its performance, otherwise it works fine. So I think it's fine to have support for it for completeness sake despite its suboptimality.

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 tweaked the warning a bit. Now it notes that linux_raw may be slower than libc::getrandom-based backends in general, not just on x86.

const __NR_getrandom: u32 = 355;
// `int 0x80` is famously slow, but implementing vDSO is too complex
// and `sysenter`/`syscall` have their own portability issues,
// so we use the simple "legacy" way of doing syscalls.
core::arch::asm!(
"int $$0x80",
in("eax") __NR_getrandom,
in("ebx") buf,
in("ecx") buflen,
in("edx") flags,
lateout("eax") r0,
options(nostack, preserves_flags)
);
} else if #[cfg(target_arch = "x86_64")] {
#[cfg(target_pointer_width = "64")]
const __NR_getrandom: u32 = 318;
#[cfg(target_pointer_width = "32")]
const __NR_getrandom: u32 = (1 << 30) + 318;

core::arch::asm!(
"syscall",
in("rax") __NR_getrandom,
in("rdi") buf,
in("rsi") buflen,
in("rdx") flags,
lateout("rax") r0,
lateout("rcx") _,
lateout("r11") _,
options(nostack, preserves_flags)
);
} else {
compile_error!("`linux_raw` backend does not support this target arch");
}
}

r0
}

#[inline]
pub fn fill_inner(mut dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
// Value of this error code is stable across all target arches.
const EINTR: isize = -4;

loop {
let ret = unsafe { getrandom_syscall(dest.as_mut_ptr().cast(), dest.len(), 0) };
match usize::try_from(ret) {
Ok(0) => return Err(Error::UNEXPECTED),
Ok(len) => {
dest = dest.get_mut(len..).ok_or(Error::UNEXPECTED)?;
if dest.is_empty() {
return Ok(());
}
}
Err(_) if ret == EINTR => continue,
Err(_) => {
let code: u32 = ret
.wrapping_neg()
.try_into()
.map_err(|_| Error::UNEXPECTED)?;
return Err(Error::from_os_error(code));
}
}
}
}