about summary refs log tree commit diff
path: root/library/std
diff options
context:
space:
mode:
authorTropical <42101043+Tropix126@users.noreply.github.com>2025-09-24 12:10:15 -0500
committerTropical <42101043+Tropix126@users.noreply.github.com>2025-09-24 12:10:15 -0500
commitb2634e31c435aeff149c5660f9329a0578ad1e72 (patch)
treeb94e9505c4100a618cc00cda79b721c29f8ec4a1 /library/std
parentdd7fda570040e8a736f7d8bc28ddd1b444aabc82 (diff)
downloadrust-b2634e31c435aeff149c5660f9329a0578ad1e72.tar.gz
rust-b2634e31c435aeff149c5660f9329a0578ad1e72.zip
std: add support for armv7a-vex-v5 target
Co-authored-by: Lewis McClelland <lewis@lewismcclelland.me>
Diffstat (limited to 'library/std')
-rw-r--r--library/std/Cargo.toml7
-rw-r--r--library/std/build.rs1
-rw-r--r--library/std/src/env.rs2
-rw-r--r--library/std/src/sys/alloc/mod.rs3
-rw-r--r--library/std/src/sys/alloc/vexos.rs96
-rw-r--r--library/std/src/sys/env_consts.rs11
-rw-r--r--library/std/src/sys/fs/mod.rs4
-rw-r--r--library/std/src/sys/fs/vexos.rs615
-rw-r--r--library/std/src/sys/pal/mod.rs4
-rw-r--r--library/std/src/sys/pal/vexos/mod.rs80
-rw-r--r--library/std/src/sys/pal/vexos/time.rs28
-rw-r--r--library/std/src/sys/random/mod.rs2
-rw-r--r--library/std/src/sys/stdio/mod.rs4
-rw-r--r--library/std/src/sys/stdio/vexos.rs100
-rw-r--r--library/std/src/sys/thread/mod.rs7
-rw-r--r--library/std/src/sys/thread/vexos.rs17
-rw-r--r--library/std/src/sys/thread_local/mod.rs2
17 files changed, 982 insertions, 1 deletions
diff --git a/library/std/Cargo.toml b/library/std/Cargo.toml
index d28a7f0b460..b4d349d66de 100644
--- a/library/std/Cargo.toml
+++ b/library/std/Cargo.toml
@@ -62,7 +62,7 @@ path = "../windows_targets"
 rand = { version = "0.9.0", default-features = false, features = ["alloc"] }
 rand_xorshift = "0.4.0"
 
-[target.'cfg(any(all(target_family = "wasm", target_os = "unknown"), target_os = "xous", all(target_vendor = "fortanix", target_env = "sgx")))'.dependencies]
+[target.'cfg(any(all(target_family = "wasm", target_os = "unknown"), target_os = "xous", target_os = "vexos", all(target_vendor = "fortanix", target_env = "sgx")))'.dependencies]
 dlmalloc = { version = "0.2.10", features = ['rustc-dep-of-std'] }
 
 [target.x86_64-fortanix-unknown-sgx.dependencies]
@@ -89,6 +89,11 @@ wasip2 = { version = '0.14.4', features = [
 r-efi = { version = "5.2.0", features = ['rustc-dep-of-std'] }
 r-efi-alloc = { version = "2.0.0", features = ['rustc-dep-of-std'] }
 
+[target.'cfg(target_os = "vexos")'.dependencies]
+vex-sdk = { version = "0.27.0", features = [
+    'rustc-dep-of-std',
+], default-features = false }
+
 [features]
 backtrace = [
     'addr2line/rustc-dep-of-std',
diff --git a/library/std/build.rs b/library/std/build.rs
index ef695601a44..8a5a785060c 100644
--- a/library/std/build.rs
+++ b/library/std/build.rs
@@ -52,6 +52,7 @@ fn main() {
         || target_os == "rtems"
         || target_os == "nuttx"
         || target_os == "cygwin"
+        || target_os == "vexos"
 
         // See src/bootstrap/src/core/build_steps/synthetic_targets.rs
         || env::var("RUSTC_BOOTSTRAP_SYNTHETIC_TARGET").is_ok()
diff --git a/library/std/src/env.rs b/library/std/src/env.rs
index e457cd61c75..6d716bd8544 100644
--- a/library/std/src/env.rs
+++ b/library/std/src/env.rs
@@ -1098,6 +1098,7 @@ pub mod consts {
     /// * `"redox"`
     /// * `"solaris"`
     /// * `"solid_asp3`
+    /// * `"vexos"`
     /// * `"vita"`
     /// * `"vxworks"`
     /// * `"xous"`
@@ -1148,6 +1149,7 @@ pub mod consts {
     ///
     /// <details><summary>Full list of possible values</summary>
     ///
+    /// * `"bin"`
     /// * `"exe"`
     /// * `"efi"`
     /// * `"js"`
diff --git a/library/std/src/sys/alloc/mod.rs b/library/std/src/sys/alloc/mod.rs
index 6d4b09494a3..2045b2fecc6 100644
--- a/library/std/src/sys/alloc/mod.rs
+++ b/library/std/src/sys/alloc/mod.rs
@@ -92,6 +92,9 @@ cfg_select! {
     target_os = "uefi" => {
         mod uefi;
     }
+    target_os = "vexos" => {
+        mod vexos;
+    }
     target_family = "wasm" => {
         mod wasm;
     }
diff --git a/library/std/src/sys/alloc/vexos.rs b/library/std/src/sys/alloc/vexos.rs
new file mode 100644
index 00000000000..c1fb6896a89
--- /dev/null
+++ b/library/std/src/sys/alloc/vexos.rs
@@ -0,0 +1,96 @@
+// FIXME(static_mut_refs): Do not allow `static_mut_refs` lint
+#![allow(static_mut_refs)]
+
+use crate::alloc::{GlobalAlloc, Layout, System};
+use crate::ptr;
+use crate::sync::atomic::{AtomicBool, Ordering};
+
+// Symbols for heap section boundaries defined in the target's linkerscript
+unsafe extern "C" {
+    static mut __heap_start: u8;
+    static mut __heap_end: u8;
+}
+
+static mut DLMALLOC: dlmalloc::Dlmalloc<Vexos> = dlmalloc::Dlmalloc::new_with_allocator(Vexos);
+
+struct Vexos;
+
+unsafe impl dlmalloc::Allocator for Vexos {
+    /// Allocs system resources
+    fn alloc(&self, _size: usize) -> (*mut u8, usize, u32) {
+        static INIT: AtomicBool = AtomicBool::new(false);
+
+        if !INIT.swap(true, Ordering::Relaxed) {
+            // This target has no growable heap, as user memory has a fixed
+            // size/location and VEXos does not manage allocation for us.
+            unsafe {
+                (
+                    (&raw mut __heap_start).cast::<u8>(),
+                    (&raw const __heap_end).offset_from_unsigned(&raw const __heap_start),
+                    0,
+                )
+            }
+        } else {
+            (ptr::null_mut(), 0, 0)
+        }
+    }
+
+    fn remap(&self, _ptr: *mut u8, _oldsize: usize, _newsize: usize, _can_move: bool) -> *mut u8 {
+        ptr::null_mut()
+    }
+
+    fn free_part(&self, _ptr: *mut u8, _oldsize: usize, _newsize: usize) -> bool {
+        false
+    }
+
+    fn free(&self, _ptr: *mut u8, _size: usize) -> bool {
+        return false;
+    }
+
+    fn can_release_part(&self, _flags: u32) -> bool {
+        false
+    }
+
+    fn allocates_zeros(&self) -> bool {
+        false
+    }
+
+    fn page_size(&self) -> usize {
+        0x1000
+    }
+}
+
+#[stable(feature = "alloc_system_type", since = "1.28.0")]
+unsafe impl GlobalAlloc for System {
+    #[inline]
+    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
+        // SAFETY: DLMALLOC access is guaranteed to be safe because we are a single-threaded target, which
+        // guarantees unique and non-reentrant access to the allocator. As such, no allocator lock is used.
+        // Calling malloc() is safe because preconditions on this function match the trait method preconditions.
+        unsafe { DLMALLOC.malloc(layout.size(), layout.align()) }
+    }
+
+    #[inline]
+    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
+        // SAFETY: DLMALLOC access is guaranteed to be safe because we are a single-threaded target, which
+        // guarantees unique and non-reentrant access to the allocator. As such, no allocator lock is used.
+        // Calling calloc() is safe because preconditions on this function match the trait method preconditions.
+        unsafe { DLMALLOC.calloc(layout.size(), layout.align()) }
+    }
+
+    #[inline]
+    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
+        // SAFETY: DLMALLOC access is guaranteed to be safe because we are a single-threaded target, which
+        // guarantees unique and non-reentrant access to the allocator. As such, no allocator lock is used.
+        // Calling free() is safe because preconditions on this function match the trait method preconditions.
+        unsafe { DLMALLOC.free(ptr, layout.size(), layout.align()) }
+    }
+
+    #[inline]
+    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
+        // SAFETY: DLMALLOC access is guaranteed to be safe because we are a single-threaded target, which
+        // guarantees unique and non-reentrant access to the allocator. As such, no allocator lock is used.
+        // Calling realloc() is safe because preconditions on this function match the trait method preconditions.
+        unsafe { DLMALLOC.realloc(ptr, layout.size(), layout.align(), new_size) }
+    }
+}
diff --git a/library/std/src/sys/env_consts.rs b/library/std/src/sys/env_consts.rs
index 711ba0a5f8a..573f540483b 100644
--- a/library/std/src/sys/env_consts.rs
+++ b/library/std/src/sys/env_consts.rs
@@ -323,6 +323,17 @@ pub mod os {
     pub const EXE_EXTENSION: &str = "efi";
 }
 
+#[cfg(target_os = "vexos")]
+pub mod os {
+    pub const FAMILY: &str = "";
+    pub const OS: &str = "vexos";
+    pub const DLL_PREFIX: &str = "";
+    pub const DLL_SUFFIX: &str = "";
+    pub const DLL_EXTENSION: &str = "";
+    pub const EXE_SUFFIX: &str = ".bin";
+    pub const EXE_EXTENSION: &str = "bin";
+}
+
 #[cfg(target_os = "visionos")]
 pub mod os {
     pub const FAMILY: &str = "unix";
diff --git a/library/std/src/sys/fs/mod.rs b/library/std/src/sys/fs/mod.rs
index 0276bf6e64c..64f5a6b36d3 100644
--- a/library/std/src/sys/fs/mod.rs
+++ b/library/std/src/sys/fs/mod.rs
@@ -35,6 +35,10 @@ cfg_select! {
         mod uefi;
         use uefi as imp;
     }
+    target_os = "vexos" => {
+        mod vexos;
+        use vexos as imp;
+    }
     target_os = "wasi" => {
         mod wasi;
         use wasi as imp;
diff --git a/library/std/src/sys/fs/vexos.rs b/library/std/src/sys/fs/vexos.rs
new file mode 100644
index 00000000000..f642e7cb074
--- /dev/null
+++ b/library/std/src/sys/fs/vexos.rs
@@ -0,0 +1,615 @@
+use crate::ffi::{OsString, c_char};
+use crate::fmt;
+use crate::fs::TryLockError;
+use crate::hash::Hash;
+use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut, SeekFrom};
+use crate::path::{Path, PathBuf};
+use crate::sys::common::small_c_string::run_path_with_cstr;
+use crate::sys::time::SystemTime;
+use crate::sys::{unsupported, unsupported_err};
+
+#[expect(dead_code)]
+#[path = "unsupported.rs"]
+mod unsupported_fs;
+pub use unsupported_fs::{
+    DirBuilder, FileTimes, canonicalize, link, readlink, remove_dir_all, rename, rmdir, symlink,
+    unlink,
+};
+
+/// VEXos file descriptor.
+///
+/// This stores an opaque pointer to a [FatFs file object structure] managed by VEXos
+/// representing an open file on disk.
+///
+/// [FatFs file object structure]: https://github.com/Xilinx/embeddedsw/blob/master/lib/sw_services/xilffs/src/include/ff.h?rgh-link-date=2025-09-23T20%3A03%3A43Z#L215
+///
+/// # Safety
+///
+/// Since this platform uses a pointer to to an internal filesystem structure with a lifetime
+/// associated with it (rather than a UNIX-style file descriptor table), care must be taken to
+/// ensure that the pointer held by `FileDesc` is valid for as long as it exists.
+#[derive(Debug)]
+struct FileDesc(*mut vex_sdk::FIL);
+
+// SAFETY: VEXos's FDs can be used on a thread other than the one they were created on.
+unsafe impl Send for FileDesc {}
+// SAFETY: We assume an environment without threads (i.e. no RTOS).
+// (If there were threads, it is possible that a mutex would be required.)
+unsafe impl Sync for FileDesc {}
+
+pub struct File {
+    fd: FileDesc,
+}
+
+#[derive(Clone)]
+pub enum FileAttr {
+    Dir,
+    File { size: u64 },
+}
+
+pub struct ReadDir(!);
+
+pub struct DirEntry {
+    path: PathBuf,
+}
+
+#[derive(Clone, Debug)]
+pub struct OpenOptions {
+    read: bool,
+    write: bool,
+    append: bool,
+    truncate: bool,
+    create: bool,
+    create_new: bool,
+}
+
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct FilePermissions {}
+
+#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
+pub struct FileType {
+    is_dir: bool,
+}
+
+impl FileAttr {
+    pub fn size(&self) -> u64 {
+        match self {
+            Self::File { size } => *size,
+            Self::Dir => 0,
+        }
+    }
+
+    pub fn perm(&self) -> FilePermissions {
+        FilePermissions {}
+    }
+
+    pub fn file_type(&self) -> FileType {
+        FileType { is_dir: matches!(self, FileAttr::Dir) }
+    }
+
+    pub fn modified(&self) -> io::Result<SystemTime> {
+        unsupported()
+    }
+
+    pub fn accessed(&self) -> io::Result<SystemTime> {
+        unsupported()
+    }
+
+    pub fn created(&self) -> io::Result<SystemTime> {
+        unsupported()
+    }
+}
+
+impl FilePermissions {
+    pub fn readonly(&self) -> bool {
+        false
+    }
+
+    pub fn set_readonly(&mut self, _readonly: bool) {
+        panic!("Perimissions do not exist")
+    }
+}
+
+impl FileType {
+    pub fn is_dir(&self) -> bool {
+        self.is_dir
+    }
+
+    pub fn is_file(&self) -> bool {
+        !self.is_dir
+    }
+
+    pub fn is_symlink(&self) -> bool {
+        // No symlinks in VEXos - entries are either files or directories.
+        false
+    }
+}
+
+impl fmt::Debug for ReadDir {
+    fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.0
+    }
+}
+
+impl Iterator for ReadDir {
+    type Item = io::Result<DirEntry>;
+
+    fn next(&mut self) -> Option<io::Result<DirEntry>> {
+        self.0
+    }
+}
+
+impl DirEntry {
+    pub fn path(&self) -> PathBuf {
+        self.path.clone()
+    }
+
+    pub fn file_name(&self) -> OsString {
+        self.path.file_name().unwrap_or_default().into()
+    }
+
+    pub fn metadata(&self) -> io::Result<FileAttr> {
+        stat(&self.path)
+    }
+
+    pub fn file_type(&self) -> io::Result<FileType> {
+        Ok(self.metadata()?.file_type())
+    }
+}
+
+impl OpenOptions {
+    pub fn new() -> OpenOptions {
+        OpenOptions {
+            read: false,
+            write: false,
+            append: false,
+            truncate: false,
+            create: false,
+            create_new: false,
+        }
+    }
+
+    pub fn read(&mut self, read: bool) {
+        self.read = read;
+    }
+    pub fn write(&mut self, write: bool) {
+        self.write = write;
+    }
+    pub fn append(&mut self, append: bool) {
+        self.append = append;
+    }
+    pub fn truncate(&mut self, truncate: bool) {
+        self.truncate = truncate;
+    }
+    pub fn create(&mut self, create: bool) {
+        self.create = create;
+    }
+    pub fn create_new(&mut self, create_new: bool) {
+        self.create_new = create_new;
+    }
+}
+
+impl File {
+    pub fn open(path: &Path, opts: &OpenOptions) -> io::Result<File> {
+        run_path_with_cstr(path, &|path| {
+            // Enforce the invariants of `create_new`/`create`.
+            //
+            // Since VEXos doesn't have anything akin to POSIX's `oflags`, we need to enforce
+            // the requirements that `create_new` can't have an existing file and `!create`
+            // doesn't create a file ourselves.
+            if !opts.read && (opts.write || opts.append) && (opts.create_new || !opts.create) {
+                let status = unsafe { vex_sdk::vexFileStatus(path.as_ptr()) };
+
+                if opts.create_new && status != 0 {
+                    return Err(io::const_error!(io::ErrorKind::AlreadyExists, "file exists",));
+                } else if !opts.create && status == 0 {
+                    return Err(io::const_error!(
+                        io::ErrorKind::NotFound,
+                        "no such file or directory",
+                    ));
+                }
+            }
+
+            let file = match opts {
+                // read + write - unsupported
+                OpenOptions { read: true, write: true, .. } => {
+                    return Err(io::const_error!(
+                        io::ErrorKind::InvalidInput,
+                        "opening files with read and write access is unsupported on this target",
+                    ));
+                }
+
+                // read
+                OpenOptions {
+                    read: true,
+                    write: false,
+                    append: _,
+                    truncate: false,
+                    create: false,
+                    create_new: false,
+                } => unsafe { vex_sdk::vexFileOpen(path.as_ptr(), c"".as_ptr()) },
+
+                // append
+                OpenOptions {
+                    read: false,
+                    write: _,
+                    append: true,
+                    truncate: false,
+                    create: _,
+                    create_new: _,
+                } => unsafe { vex_sdk::vexFileOpenWrite(path.as_ptr()) },
+
+                // write
+                OpenOptions {
+                    read: false,
+                    write: true,
+                    append: false,
+                    truncate,
+                    create: _,
+                    create_new: _,
+                } => unsafe {
+                    if *truncate {
+                        vex_sdk::vexFileOpenCreate(path.as_ptr())
+                    } else {
+                        // Open in append, but jump to the start of the file.
+                        let fd = vex_sdk::vexFileOpenWrite(path.as_ptr());
+                        vex_sdk::vexFileSeek(fd, 0, 0);
+                        fd
+                    }
+                },
+
+                _ => {
+                    return Err(io::const_error!(io::ErrorKind::InvalidInput, "invalid argument"));
+                }
+            };
+
+            if file.is_null() {
+                Err(io::const_error!(io::ErrorKind::NotFound, "could not open file"))
+            } else {
+                Ok(Self { fd: FileDesc(file) })
+            }
+        })
+    }
+
+    pub fn file_attr(&self) -> io::Result<FileAttr> {
+        // `vexFileSize` returns -1 upon error, so u64::try_from will fail on error.
+        if let Ok(size) = u64::try_from(unsafe {
+            // SAFETY: `self.fd` contains a valid pointer to `FIL` for this struct's lifetime.
+            vex_sdk::vexFileSize(self.fd.0)
+        }) {
+            Ok(FileAttr::File { size })
+        } else {
+            Err(io::const_error!(io::ErrorKind::InvalidData, "failed to get file size"))
+        }
+    }
+
+    pub fn fsync(&self) -> io::Result<()> {
+        self.flush()
+    }
+
+    pub fn datasync(&self) -> io::Result<()> {
+        self.flush()
+    }
+
+    pub fn lock(&self) -> io::Result<()> {
+        unsupported()
+    }
+
+    pub fn lock_shared(&self) -> io::Result<()> {
+        unsupported()
+    }
+
+    pub fn try_lock(&self) -> Result<(), TryLockError> {
+        Err(TryLockError::Error(unsupported_err()))
+    }
+
+    pub fn try_lock_shared(&self) -> Result<(), TryLockError> {
+        Err(TryLockError::Error(unsupported_err()))
+    }
+
+    pub fn unlock(&self) -> io::Result<()> {
+        unsupported()
+    }
+
+    pub fn truncate(&self, _size: u64) -> io::Result<()> {
+        unsupported()
+    }
+
+    pub fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
+        let len = buf.len() as u32;
+        let buf_ptr = buf.as_mut_ptr();
+        let read = unsafe {
+            // SAFETY: `self.fd` contains a valid pointer to `FIL` for this struct's lifetime.
+            vex_sdk::vexFileRead(buf_ptr.cast::<c_char>(), 1, len, self.fd.0)
+        };
+
+        if read < 0 {
+            Err(io::const_error!(io::ErrorKind::Other, "could not read from file"))
+        } else {
+            Ok(read as usize)
+        }
+    }
+
+    pub fn read_vectored(&self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
+        crate::io::default_read_vectored(|b| self.read(b), bufs)
+    }
+
+    #[inline]
+    pub fn is_read_vectored(&self) -> bool {
+        false
+    }
+
+    pub fn read_buf(&self, cursor: BorrowedCursor<'_>) -> io::Result<()> {
+        crate::io::default_read_buf(|b| self.read(b), cursor)
+    }
+
+    pub fn write(&self, buf: &[u8]) -> io::Result<usize> {
+        let len = buf.len() as u32;
+        let buf_ptr = buf.as_ptr();
+        let written = unsafe {
+            // SAFETY: `self.fd` contains a valid pointer to `FIL` for this struct's lifetime.
+            vex_sdk::vexFileWrite(buf_ptr.cast_mut().cast::<c_char>(), 1, len, self.fd.0)
+        };
+
+        if written < 0 {
+            Err(io::const_error!(io::ErrorKind::Other, "could not write to file"))
+        } else {
+            Ok(written as usize)
+        }
+    }
+
+    pub fn write_vectored(&self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
+        crate::io::default_write_vectored(|b| self.write(b), bufs)
+    }
+
+    #[inline]
+    pub fn is_write_vectored(&self) -> bool {
+        false
+    }
+
+    pub fn flush(&self) -> io::Result<()> {
+        unsafe {
+            // SAFETY: `self.fd` contains a valid pointer to `FIL` for this struct's lifetime.
+            vex_sdk::vexFileSync(self.fd.0);
+        }
+        Ok(())
+    }
+
+    pub fn tell(&self) -> io::Result<u64> {
+        // SAFETY: `self.fd` contains a valid pointer to `FIL` for this struct's lifetime.
+        let position = unsafe { vex_sdk::vexFileTell(self.fd.0) };
+
+        position.try_into().map_err(|_| {
+            io::const_error!(io::ErrorKind::InvalidData, "failed to get current location in file")
+        })
+    }
+
+    pub fn size(&self) -> Option<io::Result<u64>> {
+        None
+    }
+
+    pub fn seek(&self, pos: SeekFrom) -> io::Result<u64> {
+        const SEEK_SET: i32 = 0;
+        const SEEK_CUR: i32 = 1;
+        const SEEK_END: i32 = 2;
+
+        fn try_convert_offset<T: TryInto<u32>>(offset: T) -> io::Result<u32> {
+            offset.try_into().map_err(|_| {
+                io::const_error!(
+                    io::ErrorKind::InvalidInput,
+                    "cannot seek to an offset too large to fit in a 32 bit integer",
+                )
+            })
+        }
+
+        // SAFETY: `self.fd` contains a valid pointer to `FIL` for this struct's lifetime.
+        match pos {
+            SeekFrom::Start(offset) => unsafe {
+                map_fresult(vex_sdk::vexFileSeek(self.fd.0, try_convert_offset(offset)?, SEEK_SET))?
+            },
+            SeekFrom::End(offset) => unsafe {
+                if offset >= 0 {
+                    map_fresult(vex_sdk::vexFileSeek(
+                        self.fd.0,
+                        try_convert_offset(offset)?,
+                        SEEK_END,
+                    ))?
+                } else {
+                    // `vexFileSeek` does not support seeking with negative offset, meaning
+                    // we have to calculate the offset from the end of the file ourselves.
+
+                    // Seek to the end of the file to get the end position in the open buffer.
+                    map_fresult(vex_sdk::vexFileSeek(self.fd.0, 0, SEEK_END))?;
+                    let end_position = self.tell()?;
+
+                    map_fresult(vex_sdk::vexFileSeek(
+                        self.fd.0,
+                        // NOTE: Files internally use a 32-bit representation for stream
+                        // position, so `end_position as i64` should never overflow.
+                        try_convert_offset(end_position as i64 + offset)?,
+                        SEEK_SET,
+                    ))?
+                }
+            },
+            SeekFrom::Current(offset) => unsafe {
+                if offset >= 0 {
+                    map_fresult(vex_sdk::vexFileSeek(
+                        self.fd.0,
+                        try_convert_offset(offset)?,
+                        SEEK_CUR,
+                    ))?
+                } else {
+                    // `vexFileSeek` does not support seeking with negative offset, meaning
+                    // we have to calculate the offset from the stream position ourselves.
+                    map_fresult(vex_sdk::vexFileSeek(
+                        self.fd.0,
+                        try_convert_offset((self.tell()? as i64) + offset)?,
+                        SEEK_SET,
+                    ))?
+                }
+            },
+        }
+
+        Ok(self.tell()?)
+    }
+
+    pub fn duplicate(&self) -> io::Result<File> {
+        unsupported()
+    }
+
+    pub fn set_permissions(&self, _perm: FilePermissions) -> io::Result<()> {
+        unsupported()
+    }
+
+    pub fn set_times(&self, _times: FileTimes) -> io::Result<()> {
+        unsupported()
+    }
+}
+
+impl fmt::Debug for File {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("File").field("fd", &self.fd.0).finish()
+    }
+}
+impl Drop for File {
+    fn drop(&mut self) {
+        unsafe { vex_sdk::vexFileClose(self.fd.0) };
+    }
+}
+
+pub fn readdir(_p: &Path) -> io::Result<ReadDir> {
+    // While there *is* a userspace function for reading file directories,
+    // the necessary implementation cannot currently be done cleanly, as
+    // VEXos does not expose directory length to user programs.
+    //
+    // This means that we would need to create a large fixed-length buffer
+    // and hope that the folder's contents didn't exceed that buffer's length,
+    // which obviously isn't behavior we want to rely on in the standard library.
+    unsupported()
+}
+
+pub fn set_perm(_p: &Path, _perm: FilePermissions) -> io::Result<()> {
+    unsupported()
+}
+
+pub fn exists(path: &Path) -> io::Result<bool> {
+    run_path_with_cstr(path, &|path| Ok(unsafe { vex_sdk::vexFileStatus(path.as_ptr()) } != 0))
+}
+
+pub fn stat(p: &Path) -> io::Result<FileAttr> {
+    // `vexFileStatus` returns 3 if the given path is a directory, 1 if the path is a
+    // file, or 0 if no such path exists.
+    const FILE_STATUS_DIR: u32 = 3;
+
+    run_path_with_cstr(p, &|c_path| {
+        let file_type = unsafe { vex_sdk::vexFileStatus(c_path.as_ptr()) };
+
+        // We can't get the size if its a directory because we cant open it as a file
+        if file_type == FILE_STATUS_DIR {
+            Ok(FileAttr::Dir)
+        } else {
+            let mut opts = OpenOptions::new();
+            opts.read(true);
+            let file = File::open(p, &opts)?;
+            file.file_attr()
+        }
+    })
+}
+
+pub fn lstat(p: &Path) -> io::Result<FileAttr> {
+    // Symlinks aren't supported in this filesystem
+    stat(p)
+}
+
+// Cannot use `copy` from `common` here, since `File::set_permissions` is unsupported on this target.
+pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
+    use crate::fs::File;
+
+    // NOTE: If `from` is a directory, this call should fail due to vexFileOpen* returning null.
+    let mut reader = File::open(from)?;
+    let mut writer = File::create(to)?;
+
+    io::copy(&mut reader, &mut writer)
+}
+
+fn map_fresult(fresult: vex_sdk::FRESULT) -> io::Result<()> {
+    // VEX uses a derivative of FatFs (Xilinx's xilffs library) for filesystem operations.
+    match fresult {
+        vex_sdk::FRESULT::FR_OK => Ok(()),
+        vex_sdk::FRESULT::FR_DISK_ERR => Err(io::const_error!(
+            io::ErrorKind::Uncategorized,
+            "internal function reported an unrecoverable hard error",
+        )),
+        vex_sdk::FRESULT::FR_INT_ERR => Err(io::const_error!(
+            io::ErrorKind::Uncategorized,
+            "internal error in filesystem runtime",
+        )),
+        vex_sdk::FRESULT::FR_NOT_READY => Err(io::const_error!(
+            io::ErrorKind::Uncategorized,
+            "the storage device could not be prepared to work",
+        )),
+        vex_sdk::FRESULT::FR_NO_FILE => Err(io::const_error!(
+            io::ErrorKind::NotFound,
+            "could not find the file in the directory"
+        )),
+        vex_sdk::FRESULT::FR_NO_PATH => Err(io::const_error!(
+            io::ErrorKind::NotFound,
+            "a directory in the path name could not be found",
+        )),
+        vex_sdk::FRESULT::FR_INVALID_NAME => Err(io::const_error!(
+            io::ErrorKind::InvalidInput,
+            "the given string is invalid as a path name",
+        )),
+        vex_sdk::FRESULT::FR_DENIED => Err(io::const_error!(
+            io::ErrorKind::PermissionDenied,
+            "the required access for this operation was denied",
+        )),
+        vex_sdk::FRESULT::FR_EXIST => Err(io::const_error!(
+            io::ErrorKind::AlreadyExists,
+            "an object with the same name already exists in the directory",
+        )),
+        vex_sdk::FRESULT::FR_INVALID_OBJECT => Err(io::const_error!(
+            io::ErrorKind::Uncategorized,
+            "invalid or null file/directory object",
+        )),
+        vex_sdk::FRESULT::FR_WRITE_PROTECTED => Err(io::const_error!(
+            io::ErrorKind::PermissionDenied,
+            "a write operation was performed on write-protected media",
+        )),
+        vex_sdk::FRESULT::FR_INVALID_DRIVE => Err(io::const_error!(
+            io::ErrorKind::InvalidInput,
+            "an invalid drive number was specified in the path name",
+        )),
+        vex_sdk::FRESULT::FR_NOT_ENABLED => Err(io::const_error!(
+            io::ErrorKind::Uncategorized,
+            "work area for the logical drive has not been registered",
+        )),
+        vex_sdk::FRESULT::FR_NO_FILESYSTEM => Err(io::const_error!(
+            io::ErrorKind::Uncategorized,
+            "valid FAT volume could not be found on the drive",
+        )),
+        vex_sdk::FRESULT::FR_MKFS_ABORTED => Err(io::const_error!(
+            io::ErrorKind::Uncategorized,
+            "failed to create filesystem volume"
+        )),
+        vex_sdk::FRESULT::FR_TIMEOUT => Err(io::const_error!(
+            io::ErrorKind::TimedOut,
+            "the function was canceled due to a timeout of thread-safe control",
+        )),
+        vex_sdk::FRESULT::FR_LOCKED => Err(io::const_error!(
+            io::ErrorKind::Uncategorized,
+            "the operation to the object was rejected by file sharing control",
+        )),
+        vex_sdk::FRESULT::FR_NOT_ENOUGH_CORE => {
+            Err(io::const_error!(io::ErrorKind::OutOfMemory, "not enough memory for the operation"))
+        }
+        vex_sdk::FRESULT::FR_TOO_MANY_OPEN_FILES => Err(io::const_error!(
+            io::ErrorKind::Uncategorized,
+            "maximum number of open files has been reached",
+        )),
+        vex_sdk::FRESULT::FR_INVALID_PARAMETER => {
+            Err(io::const_error!(io::ErrorKind::InvalidInput, "a given parameter was invalid"))
+        }
+        _ => unreachable!(), // C-style enum
+    }
+}
diff --git a/library/std/src/sys/pal/mod.rs b/library/std/src/sys/pal/mod.rs
index 513121c6d30..dd5e83ee570 100644
--- a/library/std/src/sys/pal/mod.rs
+++ b/library/std/src/sys/pal/mod.rs
@@ -45,6 +45,10 @@ cfg_select! {
         mod trusty;
         pub use self::trusty::*;
     }
+    target_os = "vexos" => {
+        mod vexos;
+        pub use self::vexos::*;
+    }
     all(target_os = "wasi", target_env = "p2") => {
         mod wasip2;
         pub use self::wasip2::*;
diff --git a/library/std/src/sys/pal/vexos/mod.rs b/library/std/src/sys/pal/vexos/mod.rs
new file mode 100644
index 00000000000..61a34b0f68a
--- /dev/null
+++ b/library/std/src/sys/pal/vexos/mod.rs
@@ -0,0 +1,80 @@
+#[path = "../unsupported/os.rs"]
+pub mod os;
+#[path = "../unsupported/pipe.rs"]
+pub mod pipe;
+pub mod time;
+
+#[expect(dead_code)]
+#[path = "../unsupported/common.rs"]
+mod unsupported_common;
+
+pub use unsupported_common::{
+    decode_error_kind, init, is_interrupted, unsupported, unsupported_err,
+};
+
+use crate::arch::global_asm;
+use crate::ptr;
+use crate::sys::stdio;
+use crate::time::{Duration, Instant};
+
+global_asm!(
+    r#"
+    .section .boot, "ax"
+    .global _boot
+
+    _boot:
+        ldr sp, =__stack_top @ Set up the user stack.
+        b _start             @ Jump to the Rust entrypoint.
+    "#
+);
+
+#[cfg(not(test))]
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn _start() -> ! {
+    unsafe extern "C" {
+        static mut __bss_start: u8;
+        static mut __bss_end: u8;
+
+        fn main() -> i32;
+    }
+
+    // Clear the .bss (uninitialized statics) section by filling it with zeroes.
+    // This is required, since the compiler assumes it will be zeroed on first access.
+    ptr::write_bytes(
+        &raw mut __bss_start,
+        0,
+        (&raw mut __bss_end).offset_from_unsigned(&raw mut __bss_start),
+    );
+
+    main();
+
+    cleanup();
+    abort_internal()
+}
+
+// SAFETY: must be called only once during runtime cleanup.
+// NOTE: this is not guaranteed to run, for example when the program aborts.
+pub unsafe fn cleanup() {
+    let exit_time = Instant::now();
+    const FLUSH_TIMEOUT: Duration = Duration::from_millis(15);
+
+    // Force the serial buffer to flush
+    while exit_time.elapsed() < FLUSH_TIMEOUT {
+        vex_sdk::vexTasksRun();
+
+        // If the buffer has been fully flushed, exit the loop
+        if vex_sdk::vexSerialWriteFree(stdio::STDIO_CHANNEL) == (stdio::STDOUT_BUF_SIZE as i32) {
+            break;
+        }
+    }
+}
+
+pub fn abort_internal() -> ! {
+    unsafe {
+        vex_sdk::vexSystemExitRequest();
+
+        loop {
+            vex_sdk::vexTasksRun();
+        }
+    }
+}
diff --git a/library/std/src/sys/pal/vexos/time.rs b/library/std/src/sys/pal/vexos/time.rs
new file mode 100644
index 00000000000..f95d96cd27a
--- /dev/null
+++ b/library/std/src/sys/pal/vexos/time.rs
@@ -0,0 +1,28 @@
+use crate::time::Duration;
+
+#[expect(dead_code)]
+#[path = "../unsupported/time.rs"]
+mod unsupported_time;
+pub use unsupported_time::{SystemTime, UNIX_EPOCH};
+
+#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
+pub struct Instant(Duration);
+
+impl Instant {
+    pub fn now() -> Instant {
+        let micros = unsafe { vex_sdk::vexSystemHighResTimeGet() };
+        Self(Duration::from_micros(micros))
+    }
+
+    pub fn checked_sub_instant(&self, other: &Instant) -> Option<Duration> {
+        self.0.checked_sub(other.0)
+    }
+
+    pub fn checked_add_duration(&self, other: &Duration) -> Option<Instant> {
+        Some(Instant(self.0.checked_add(*other)?))
+    }
+
+    pub fn checked_sub_duration(&self, other: &Duration) -> Option<Instant> {
+        Some(Instant(self.0.checked_sub(*other)?))
+    }
+}
diff --git a/library/std/src/sys/random/mod.rs b/library/std/src/sys/random/mod.rs
index 1e0eec07b50..3c5a4c82a9f 100644
--- a/library/std/src/sys/random/mod.rs
+++ b/library/std/src/sys/random/mod.rs
@@ -101,6 +101,7 @@ cfg_select! {
     any(
         all(target_family = "wasm", target_os = "unknown"),
         target_os = "xous",
+        target_os = "vexos",
     ) => {
         // FIXME: finally remove std support for wasm32-unknown-unknown
         // FIXME: add random data generation to xous
@@ -116,6 +117,7 @@ cfg_select! {
     all(target_family = "wasm", target_os = "unknown"),
     all(target_os = "wasi", target_env = "p2"),
     target_os = "xous",
+    target_os = "vexos",
 )))]
 pub fn hashmap_random_keys() -> (u64, u64) {
     let mut buf = [0; 16];
diff --git a/library/std/src/sys/stdio/mod.rs b/library/std/src/sys/stdio/mod.rs
index 7436e4d9de4..404ac877926 100644
--- a/library/std/src/sys/stdio/mod.rs
+++ b/library/std/src/sys/stdio/mod.rs
@@ -29,6 +29,10 @@ cfg_select! {
         mod uefi;
         pub use uefi::*;
     }
+    target_os = "vexos" => {
+        mod vexos;
+        pub use vexos::*;
+    }
     all(target_os = "wasi", target_env = "p1") => {
         mod wasip1;
         pub use wasip1::*;
diff --git a/library/std/src/sys/stdio/vexos.rs b/library/std/src/sys/stdio/vexos.rs
new file mode 100644
index 00000000000..1f2251c6421
--- /dev/null
+++ b/library/std/src/sys/stdio/vexos.rs
@@ -0,0 +1,100 @@
+use crate::io;
+
+pub struct Stdin;
+pub struct Stdout;
+pub type Stderr = Stdout;
+
+pub const STDIO_CHANNEL: u32 = 1;
+
+impl Stdin {
+    pub const fn new() -> Stdin {
+        Stdin
+    }
+}
+
+impl io::Read for Stdin {
+    fn read(&mut self, mut buf: &mut [u8]) -> io::Result<usize> {
+        let mut count = 0;
+
+        for out_byte in buf.iter_mut() {
+            let byte = unsafe { vex_sdk::vexSerialReadChar(STDIO_CHANNEL) };
+            if byte < 0 {
+                break;
+            }
+
+            *out_byte = byte as u8;
+            count += 1;
+        }
+
+        Ok(count)
+    }
+}
+
+impl Stdout {
+    pub const fn new() -> Stdout {
+        Stdout
+    }
+}
+
+impl io::Write for Stdout {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        let mut written = 0;
+
+        // HACK: VEXos holds an internal ringbuffer for serial writes that is flushed to USB1
+        // roughly every millisecond by `vexTasksRun`. For writes larger than 2048 bytes, we
+        // must block until that buffer is flushed to USB1 before writing the rest of `buf`.
+        //
+        // This is fairly nonstandard for a `write` implementation, but it avoids a guaranteed
+        // recursive panic when using macros such as `print!` to write large amounts of data
+        // (buf.len() > 2048) to stdout at once.
+        for chunk in buf.chunks(STDOUT_BUF_SIZE) {
+            if unsafe { vex_sdk::vexSerialWriteFree(STDIO_CHANNEL) as usize } < chunk.len() {
+                self.flush().unwrap();
+            }
+
+            let count: usize = unsafe {
+                vex_sdk::vexSerialWriteBuffer(STDIO_CHANNEL, chunk.as_ptr(), chunk.len() as u32)
+            }
+            .try_into()
+            .map_err(|_| {
+                io::const_error!(io::ErrorKind::Uncategorized, "internal write error occurred")
+            })?;
+
+            written += count;
+
+            // This is a sanity check to ensure that we don't end up with non-contiguous
+            // buffer writes. e.g. a chunk gets only partially written, but we continue
+            // attempting to write the remaining chunks.
+            //
+            // In practice, this should never really occur since the previous flush ensures
+            // enough space in FIFO to write the entire chunk to vexSerialWriteBuffer.
+            if count != chunk.len() {
+                break;
+            }
+        }
+
+        Ok(written)
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        // This may block for up to a millisecond.
+        unsafe {
+            while (vex_sdk::vexSerialWriteFree(STDIO_CHANNEL) as usize) != STDOUT_BUF_SIZE {
+                vex_sdk::vexTasksRun();
+            }
+        }
+
+        Ok(())
+    }
+}
+
+pub const STDIN_BUF_SIZE: usize = 4096;
+pub const STDOUT_BUF_SIZE: usize = 2048;
+
+pub fn is_ebadf(_err: &io::Error) -> bool {
+    false
+}
+
+pub fn panic_output() -> Option<impl io::Write> {
+    Some(Stdout::new())
+}
diff --git a/library/std/src/sys/thread/mod.rs b/library/std/src/sys/thread/mod.rs
index 6bb7fc1a20e..3bd83dd760a 100644
--- a/library/std/src/sys/thread/mod.rs
+++ b/library/std/src/sys/thread/mod.rs
@@ -81,6 +81,13 @@ cfg_select! {
         ))]
         pub use unsupported::set_name;
     }
+    target_os = "vexos" => {
+        mod vexos;
+        pub use vexos::{sleep, yield_now};
+        #[expect(dead_code)]
+        mod unsupported;
+        pub use unsupported::{Thread, available_parallelism, current_os_id, set_name, DEFAULT_MIN_STACK_SIZE};
+    }
     all(target_os = "wasi", target_env = "p1") => {
         mod wasip1;
         pub use wasip1::{DEFAULT_MIN_STACK_SIZE, sleep, yield_now};
diff --git a/library/std/src/sys/thread/vexos.rs b/library/std/src/sys/thread/vexos.rs
new file mode 100644
index 00000000000..d917dde4d0b
--- /dev/null
+++ b/library/std/src/sys/thread/vexos.rs
@@ -0,0 +1,17 @@
+use crate::time::{Duration, Instant};
+
+pub fn yield_now() {
+    unsafe {
+        vex_sdk::vexTasksRun();
+    }
+}
+
+pub fn sleep(dur: Duration) {
+    let start = Instant::now();
+
+    while start.elapsed() < dur {
+        unsafe {
+            vex_sdk::vexTasksRun();
+        }
+    }
+}
diff --git a/library/std/src/sys/thread_local/mod.rs b/library/std/src/sys/thread_local/mod.rs
index cff74857c47..d5c795093cf 100644
--- a/library/std/src/sys/thread_local/mod.rs
+++ b/library/std/src/sys/thread_local/mod.rs
@@ -29,6 +29,7 @@ cfg_select! {
         target_os = "uefi",
         target_os = "zkvm",
         target_os = "trusty",
+        target_os = "vexos",
     ) => {
         mod no_threads;
         pub use no_threads::{EagerStorage, LazyStorage, thread_local_inner};
@@ -98,6 +99,7 @@ pub(crate) mod guard {
             target_os = "uefi",
             target_os = "zkvm",
             target_os = "trusty",
+            target_os = "vexos",
         ) => {
             pub(crate) fn enable() {
                 // FIXME: Right now there is no concept of "thread exit" on