Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
92 changes: 57 additions & 35 deletions src/bun_core/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,15 @@ impl<T: Copy> Unaligned<T> {
if slice.is_empty() {
return &mut [];
}
debug_assert!(
// Hard `assert!` for the same reason as `bytes_as_slice_mut`: a
// misaligned `&mut [T]` is UB the moment it is formed.
Comment thread
robobun marked this conversation as resolved.
Outdated
assert!(
(slice.as_ptr() as usize).is_multiple_of(core::mem::align_of::<T>()),
"Unaligned::slice_align_cast_mut: pointer is not {}-byte aligned",
core::mem::align_of::<T>(),
);
// SAFETY: `#[repr(C, packed)]` around a single `T` gives identical size
// and field offset 0; the debug-asserted alignment upgrades it to `T`'s
// and field offset 0; the alignment asserted above upgrades it to `T`'s
// layout. `&mut` exclusivity is preserved.
unsafe { core::slice::from_raw_parts_mut(slice.as_mut_ptr().cast::<T>(), slice.len()) }
}
Expand Down Expand Up @@ -106,51 +108,67 @@ impl ZStr {
}
/// Wrap a `&'static [u8]` literal that already includes the trailing
/// `\0` (e.g. `b".\0"`). The returned `&ZStr` excludes the NUL from
/// `len()` per the type invariant. Panics in debug if no trailing NUL.
/// `len()` per the type invariant. Panics (a compile error in const
/// context) if there is no trailing NUL.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[inline]
pub const fn from_static(s: &'static [u8]) -> &'static ZStr {
debug_assert!(!s.is_empty() && s[s.len() - 1] == 0);
// SAFETY: caller-supplied literal ends in NUL; lifetime is 'static.
assert!(
!s.is_empty() && s[s.len() - 1] == 0,
"ZStr::from_static: missing trailing NUL"
);
// SAFETY: `s` ends in NUL (asserted above); lifetime is 'static.
unsafe { Self::from_raw(s.as_ptr(), s.len() - 1) }
}
/// Borrow `buf[..len]` as a `&ZStr`, where `buf[len] == 0`. This is the
/// safe-surface form of [`from_raw`] for the dominant call shape in the
/// install pipeline: a stack `PathBuffer` filled to `len` with a NUL
/// written at `buf[len]`. The slice bound proves `buf[..=len]` is in the
/// same allocation; the NUL is debug-asserted.
/// same allocation; the NUL is asserted.
///
/// These constructors are safe fns, so their checks are hard `assert!`s
/// (one compare each, next to a syscall): a `debug_assert!` would leave
/// release builds handing an over-long or unterminated buffer to the OS.
///
/// # Panics
/// If `len >= buf.len()` or `buf[len] != 0`.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[inline]
pub fn from_buf(buf: &[u8], len: usize) -> &ZStr {
debug_assert!(len < buf.len(), "ZStr::from_buf: NUL must lie within buf");
debug_assert_eq!(buf[len], 0, "ZStr::from_buf: missing NUL at buf[len]");
// SAFETY: `buf[..=len]` is in-bounds (debug-asserted above; release
// relies on caller upholding the documented `buf[len] == 0`
// precondition).
assert!(len < buf.len(), "ZStr::from_buf: NUL must lie within buf");
assert!(buf[len] == 0, "ZStr::from_buf: missing NUL at buf[len]");
// SAFETY: `buf[..=len]` is in-bounds and `buf[len] == 0`, both
// asserted above.
unsafe { Self::from_raw(buf.as_ptr(), len) }
}
/// Borrow `buf[..buf.len()-1]` as a `&ZStr`, where the last byte of `buf`
/// is the NUL terminator. This is [`from_buf`] specialized for the second
/// most common call shape: a slice that already includes its trailing NUL
/// (e.g. a `Vec<u8>` with `0` pushed, or `CStr::to_bytes_with_nul`).
/// Debug-asserts the trailing NUL; release relies on the documented
/// precondition.
///
/// # Panics
/// If `buf` is empty or its last byte is not NUL.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[inline]
pub fn from_slice_with_nul(buf: &[u8]) -> &ZStr {
debug_assert!(!buf.is_empty(), "ZStr::from_slice_with_nul: empty slice");
debug_assert_eq!(
buf[buf.len() - 1],
0,
assert!(!buf.is_empty(), "ZStr::from_slice_with_nul: empty slice");
assert!(
buf[buf.len() - 1] == 0,
"ZStr::from_slice_with_nul: missing trailing NUL"
);
// SAFETY: `buf[buf.len()-1] == 0` (debug-asserted; caller contract in
// release) and `buf[..buf.len()-1]` is in-bounds by slice invariant.
// SAFETY: `buf[buf.len()-1] == 0` (asserted above) and
// `buf[..buf.len()-1]` is in-bounds by slice invariant.
unsafe { Self::from_raw(buf.as_ptr(), buf.len() - 1) }
}
/// Mutable variant of [`from_buf`].
///
/// # Panics
/// If `len >= buf.len()` or `buf[len] != 0`.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[inline]
pub fn from_buf_mut(buf: &mut [u8], len: usize) -> &mut ZStr {
debug_assert!(len < buf.len());
debug_assert_eq!(buf[len], 0);
// SAFETY: see `from_buf`.
assert!(
len < buf.len(),
"ZStr::from_buf_mut: NUL must lie within buf"
);
assert!(buf[len] == 0, "ZStr::from_buf_mut: missing NUL at buf[len]");
// SAFETY: see `from_buf`; `&mut buf` makes `buf[..=len]` writable.
unsafe { Self::from_raw_mut(buf.as_mut_ptr(), len) }
}
#[inline]
Expand Down Expand Up @@ -445,29 +463,33 @@ impl WStr {
/// Borrow `buf[..len]` as a `&WStr`, where `buf[len] == 0`. Safe-surface
/// form of [`from_raw`] for the dominant call shape: a stack `WPathBuffer`
/// filled to `len` with a NUL written at `buf[len]`. The slice bound proves
/// `buf[..=len]` lies in one allocation; the NUL is debug-asserted (release
/// relies on the documented `buf[len] == 0` precondition). Mirrors
/// [`ZStr::from_buf`].
/// `buf[..=len]` lies in one allocation; the NUL is asserted. Mirrors
/// [`ZStr::from_buf`], including why the checks are not `debug_assert!`s.
///
/// # Panics
/// If `len >= buf.len()` or `buf[len] != 0`.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[inline]
pub fn from_buf(buf: &[u16], len: usize) -> &WStr {
debug_assert!(len < buf.len(), "WStr::from_buf: NUL must lie within buf");
debug_assert_eq!(buf[len], 0, "WStr::from_buf: missing NUL at buf[len]");
// SAFETY: `buf[..=len]` is in-bounds (debug-asserted above; caller
// contract in release).
assert!(len < buf.len(), "WStr::from_buf: NUL must lie within buf");
assert!(buf[len] == 0, "WStr::from_buf: missing NUL at buf[len]");
// SAFETY: `buf[..=len]` is in-bounds and `buf[len] == 0`, both
// asserted above.
unsafe { Self::from_raw(buf.as_ptr(), len) }
}
/// Borrow `buf[..buf.len()-1]` as a `&WStr`, where the last unit of `buf`
/// is the NUL terminator. Mirrors [`ZStr::from_slice_with_nul`].
///
/// # Panics
/// If `buf` is empty or its last unit is not NUL.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[inline]
pub fn from_slice_with_nul(buf: &[u16]) -> &WStr {
debug_assert!(!buf.is_empty(), "WStr::from_slice_with_nul: empty slice");
debug_assert_eq!(
buf[buf.len() - 1],
0,
assert!(!buf.is_empty(), "WStr::from_slice_with_nul: empty slice");
assert!(
buf[buf.len() - 1] == 0,
"WStr::from_slice_with_nul: missing trailing NUL"
);
// SAFETY: `buf[buf.len()-1] == 0` (debug-asserted; caller contract in
// release) and `buf[..buf.len()-1]` is in-bounds by slice invariant.
// SAFETY: `buf[buf.len()-1] == 0` (asserted above) and
// `buf[..buf.len()-1]` is in-bounds by slice invariant.
unsafe { Self::from_raw(buf.as_ptr(), buf.len() - 1) }
}
/// Borrow a NUL-terminated FFI wide string as `&WStr`, or [`EMPTY`] if
Expand Down
11 changes: 10 additions & 1 deletion src/errno/darwin_errno.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ pub use crate::posix::mode_t as Mode;

#[repr(u16)]
#[derive(
Copy, Clone, Eq, PartialEq, Hash, Debug, strum::IntoStaticStr, strum::EnumString, enum_map::Enum,
Copy,
Clone,
Eq,
PartialEq,
Hash,
Debug,
strum::IntoStaticStr,
strum::EnumString,
strum::FromRepr,
enum_map::Enum,
)]
pub enum SystemErrno {
SUCCESS = 0,
Expand Down
11 changes: 10 additions & 1 deletion src/errno/freebsd_errno.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ pub use crate::posix::mode_t as Mode;

#[repr(u16)]
#[derive(
Copy, Clone, Eq, PartialEq, Hash, Debug, strum::IntoStaticStr, strum::EnumString, enum_map::Enum,
Copy,
Clone,
Eq,
PartialEq,
Hash,
Debug,
strum::IntoStaticStr,
strum::EnumString,
strum::FromRepr,
enum_map::Enum,
)]
pub enum SystemErrno {
SUCCESS = 0,
Expand Down
137 changes: 123 additions & 14 deletions src/errno/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ macro_rules! impl_get_errno_libc {
// against the type's own all-ones value instead (== -1 for
// signed, == MAX for unsigned — both are libc's failure rc).
if self == !(0 as $t) {
$crate::E::from_raw($crate::posix::errno() as u16)
// Checked: an errno the enum does not declare (a kernel or
// libc newer than the table) collapses to EIO.
Comment thread
robobun marked this conversation as resolved.
Outdated
$crate::from_errno($crate::posix::errno())
} else {
$crate::E::SUCCESS
}
Expand Down Expand Up @@ -276,22 +278,27 @@ pub fn e_from_negated(errno: core::ffi::c_int) -> E {
}

impl SystemErrno {
/// Unchecked discriminant cast.
/// Discriminant → variant for a value the caller already knows is
/// declared (a table index, or an `E` discriminant on Windows, where the
/// two enums share one discriminant set). OS-supplied codes go through
/// [`from_errno`] / [`SystemErrno::init`] instead, which report unknown
/// codes rather than panicking.
Comment thread
robobun marked this conversation as resolved.
Outdated
///
/// On POSIX the enum is dense `0..MAX`, so we debug-assert `n < MAX`.
/// On Windows the enum is **sparse** (dense `0..=137` plus isolated `UV_E*`
/// discriminants in the ~3000-4095 range — see windows_errno.rs), so the
/// `< MAX` bound does not hold for valid tags and the assert is skipped.
/// `from_repr` (`strum::FromRepr`) is the exhaustive match over the
/// declared variants, which is what makes this a real check on Windows
/// too, where the enum is sparse (dense `0..=137` plus isolated `UV_E*`
/// discriminants around 3000-4095, see windows_errno.rs) and `n < MAX`
/// would be the wrong test.
///
/// # Panics
/// If `n` is not a declared discriminant. This is a safe fn, so the check
/// is not a `debug_assert!`.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[inline]
pub const fn from_raw(n: u16) -> SystemErrno {
// `as usize` on both sides papers over per-OS `MAX` typing (POSIX `u16`
// vs Windows `usize`) without normalizing the constant itself.
#[cfg(not(windows))]
debug_assert!((n as usize) < (Self::MAX as usize));
// SAFETY: caller guarantees `n` is a declared `#[repr(u16)]` discriminant
// of `SystemErrno`. The enum is NOT
// contiguous on Windows; do not assume `n < MAX` implies validity there.
unsafe { core::mem::transmute::<u16, SystemErrno>(n) }
match Self::from_repr(n) {
Some(e) => e,
None => panic!("SystemErrno::from_raw: not a declared errno discriminant"),
}
}
}

Expand Down Expand Up @@ -482,6 +489,108 @@ mod errno_name_tests {
assert_eq!(system_errno_name(97), Some("EINTEGRITY"));
}

/// Every declared variant survives a `from_raw` round trip, and on POSIX
/// the declared set is exactly `0..MAX`, which is the range check `init`
/// (and through it `from_errno` / `e_from_negated`) relies on.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[test]
fn from_raw_round_trips_every_declared_discriminant() {
use enum_map::Enum;
for i in 0..SystemErrno::LENGTH {
let e = SystemErrno::from_usize(i);
assert_eq!(SystemErrno::from_raw(e as u16), e);
}
#[cfg(not(windows))]
{
assert_eq!(SystemErrno::LENGTH, usize::from(SystemErrno::MAX));
for n in 0..SystemErrno::MAX {
assert!(SystemErrno::from_repr(n).is_some(), "hole at {n}");
}
}
}

/// `from_raw` is a safe fn, so an undeclared discriminant has to panic in
/// every build rather than produce an invalid enum value. One past the
/// dense head is undeclared on every platform (the Windows `UV_*` tail
/// starts around 3000).
Comment thread
robobun marked this conversation as resolved.
Outdated
#[test]
#[should_panic(expected = "not a declared errno discriminant")]
fn from_raw_rejects_one_past_the_dense_head() {
let _ = SystemErrno::from_raw(system_errno_max_dense() as u16);
}

#[test]
#[should_panic(expected = "not a declared errno discriminant")]
fn from_raw_rejects_u16_max() {
let _ = SystemErrno::from_raw(u16::MAX);
}

/// The OS can report an errno the table does not declare (a kernel or
/// libc newer than the enum); `get_errno` must collapse it to EIO instead
/// of building an invalid `E` out of it.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[cfg(not(windows))]
#[test]
fn get_errno_collapses_undeclared_libc_errno_to_eio() {
let set_errno = |value: core::ffi::c_int| {
// SAFETY: `errno_ptr()` is the calling thread's own errno slot,
// valid for the life of the thread.
unsafe { *bun_core::ffi::errno_ptr() = value };
};
set_errno(i32::from(SystemErrno::MAX));
assert_eq!(get_errno(-1i32), SystemErrno::EIO);
set_errno(libc::ENOENT);
assert_eq!(get_errno(-1i32), SystemErrno::ENOENT);
// A successful return never consults errno.
set_errno(i32::from(SystemErrno::MAX));
assert_eq!(get_errno(0i32), SystemErrno::SUCCESS);
}

/// Raw Linux syscalls return `-errno` in the result itself; the kernel's
/// window for that is `-4095..=-1`, wider than the enum.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[cfg(any(target_os = "linux", target_os = "android"))]
#[test]
fn get_errno_collapses_undeclared_raw_syscall_errno_to_eio() {
let failed_with = |errno: isize| (-errno) as usize;
assert_eq!(
get_errno(failed_with(SystemErrno::MAX as isize)),
SystemErrno::EIO
);
assert_eq!(get_errno(failed_with(4095)), SystemErrno::EIO);
assert_eq!(
get_errno(failed_with(libc::ENOENT as isize)),
SystemErrno::ENOENT
);
// Outside the window the value is a successful result (a length, an
// mmap address), not an errno.
Comment thread
robobun marked this conversation as resolved.
Outdated
assert_eq!(get_errno(0usize), SystemErrno::SUCCESS);
assert_eq!(get_errno(42usize), SystemErrno::SUCCESS);
assert_eq!(get_errno(failed_with(4096)), SystemErrno::SUCCESS);
}

/// `SystemErrno::to_e` and `bun_sys::Error::resolve_system_errno` convert
/// between the two Windows enums by discriminant through `from_raw`, so
/// the two must declare exactly the same discriminants.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[cfg(windows)]
#[test]
fn e_and_system_errno_declare_the_same_discriminants() {
use enum_map::Enum;
assert_eq!(E::LENGTH, SystemErrno::LENGTH);
for i in 0..SystemErrno::LENGTH {
let s = SystemErrno::from_usize(i);
assert_eq!(s.to_e() as u16, s as u16, "{s:?}");
}
for i in 0..E::LENGTH {
let e = E::from_usize(i);
assert_eq!(SystemErrno::from_raw(e as u16) as u16, e as u16, "{e:?}");
}
}

#[cfg(windows)]
#[test]
#[should_panic(expected = "not a declared errno discriminant")]
fn e_from_raw_rejects_the_gap_before_the_uv_tail() {
let _ = E::from_raw(SystemErrno::MAX as u16);
}

/// `win32_errno_name` translation contract: known `GetLastError()` codes
/// map to POSIX names on Windows, unmapped/out-of-range codes are `None`,
/// and the helper is a constant `None` off Windows.
Expand Down
23 changes: 16 additions & 7 deletions src/errno/linux_errno.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ pub use crate::posix::mode_t as Mode;

#[repr(u16)]
#[derive(
Copy, Clone, Eq, PartialEq, Hash, Debug, strum::IntoStaticStr, strum::EnumString, enum_map::Enum,
Copy,
Clone,
Eq,
PartialEq,
Hash,
Debug,
strum::IntoStaticStr,
strum::EnumString,
strum::FromRepr,
enum_map::Enum,
)]
pub enum SystemErrno {
SUCCESS = 0,
Expand Down Expand Up @@ -183,13 +192,13 @@ impl GetErrno for usize {
fn get_errno(self) -> E {
// `as` between same-width usize/isize is a bit-reinterpretation
let signed = self as isize;
let int = if signed > -4096 && signed < 0 {
-signed
if signed > -4096 && signed < 0 {
// The kernel's `-errno` range (1..4096) is wider than the enum
// (`MAX`), so the code is validated; unknown ones collapse to EIO.
Comment thread
robobun marked this conversation as resolved.
Outdated
crate::from_errno((-signed) as i32)
} else {
0
};
// SAFETY: int is in [0, 4096); E is #[repr] over the kernel errno range
unsafe { core::mem::transmute::<u16, E>(int as u16) }
E::SUCCESS
}
}
}

Expand Down
Loading
Loading