diff --git a/redox-rt/src/lib.rs b/redox-rt/src/lib.rs
index 9aa0d90b360766655fb5ff32106203c36f87aad1..106fadc264738dc804a7cc1276f9b8d9408e8f9a 100644
--- a/redox-rt/src/lib.rs
+++ b/redox-rt/src/lib.rs
@@ -143,6 +143,7 @@ pub fn initialize_freestanding() {
     page.tcb_ptr = page;
     page.tcb_len = syscall::PAGE_SIZE;
     page.tls_end = (page as *mut Tcb).cast();
+    *page.os_specific.thr_fd.get_mut() = None;
 
     #[cfg(not(target_arch = "aarch64"))]
     unsafe {
diff --git a/redox-rt/src/proc.rs b/redox-rt/src/proc.rs
index edd90eb1c6f3b30bc9ec1dae279001e33481f4ad..360c18a14b73e6f33550a859a2e21ab1fba20ef7 100644
--- a/redox-rt/src/proc.rs
+++ b/redox-rt/src/proc.rs
@@ -45,6 +45,8 @@ pub struct InterpOverride {
 
 pub struct ExtraInfo<'a> {
     pub cwd: Option<&'a [u8]>,
+    // Default scheme for the process
+    pub default_scheme: Option<&'a [u8]>,
     // POSIX states that while sigactions are reset, ignored sigactions will remain ignored.
     pub sigignmask: u64,
     // POSIX also states that the sigprocmask must be preserved across execs.
@@ -351,8 +353,9 @@ where
     )?;
     push(AT_PHENT)?;
 
-    let total_args_envs_auxvpointee_size =
-        total_args_envs_size + extrainfo.cwd.map_or(0, |s| s.len() + 1);
+    let total_args_envs_auxvpointee_size = total_args_envs_size
+        + extrainfo.cwd.map_or(0, |s| s.len() + 1)
+        + extrainfo.default_scheme.map_or(0, |s| s.len() + 1);
     let args_envs_size_aligned = total_args_envs_auxvpointee_size.next_multiple_of(PAGE_SIZE);
     let target_args_env_address =
         find_free_target_addr(&tree, args_envs_size_aligned).ok_or(Error::new(ENOMEM))?;
@@ -392,10 +395,18 @@ where
 
         if let Some(cwd) = extrainfo.cwd {
             push(append(cwd)?)?;
-            push(AT_REDOX_INITIALCWD_PTR)?;
+            push(AT_REDOX_INITIAL_CWD_PTR)?;
             push(cwd.len())?;
-            push(AT_REDOX_INITIALCWD_LEN)?;
+            push(AT_REDOX_INITIAL_CWD_LEN)?;
         }
+
+        if let Some(default_scheme) = extrainfo.default_scheme {
+            push(append(default_scheme)?)?;
+            push(AT_REDOX_INITIAL_DEFAULT_SCHEME_PTR)?;
+            push(default_scheme.len())?;
+            push(AT_REDOX_INITIAL_DEFAULT_SCHEME_LEN)?;
+        }
+
         #[cfg(target_pointer_width = "32")]
         {
             push((extrainfo.sigignmask >> 32) as usize)?;
diff --git a/src/header/unistd/mod.rs b/src/header/unistd/mod.rs
index 0abbcdad18fe989ee52fe7e54472c5805c5db380..981fea243aa2d79cdba5b2c4665d2ef6a1d0c003 100644
--- a/src/header/unistd/mod.rs
+++ b/src/header/unistd/mod.rs
@@ -102,13 +102,21 @@ pub unsafe extern "C" fn chdir(path: *const c_char) -> c_int {
 }
 
 #[no_mangle]
-pub extern "C" fn chroot(path: *const c_char) -> c_int {
+pub unsafe extern "C" fn chroot(path: *const c_char) -> c_int {
     // TODO: Implement
     platform::ERRNO.set(crate::header::errno::EPERM);
 
     -1
 }
 
+#[no_mangle]
+pub unsafe extern "C" fn set_default_scheme(scheme: *const c_char) -> c_int {
+    let scheme = CStr::from_ptr(scheme);
+    Sys::set_default_scheme(scheme)
+        .map(|_| 0)
+        .or_minus_one_errno()
+}
+
 #[no_mangle]
 pub unsafe extern "C" fn chown(path: *const c_char, owner: uid_t, group: gid_t) -> c_int {
     let path = CStr::from_ptr(path);
diff --git a/src/platform/auxv_defs.rs b/src/platform/auxv_defs.rs
index d26d3e55f66fe19899fd274a049083d4108e593c..814eab24994935e3fb0e928eb54d670c3143c870 100644
--- a/src/platform/auxv_defs.rs
+++ b/src/platform/auxv_defs.rs
@@ -28,9 +28,9 @@ pub const AT_EXECFN: usize = 31; /* Filename of executable.  */
 
 #[cfg(target_os = "redox")]
 // XXX: The name AT_CWD is already used in openat... for a completely different purpose.
-pub const AT_REDOX_INITIALCWD_PTR: usize = 32;
+pub const AT_REDOX_INITIAL_CWD_PTR: usize = 32;
 #[cfg(target_os = "redox")]
-pub const AT_REDOX_INITIALCWD_LEN: usize = 33;
+pub const AT_REDOX_INITIAL_CWD_LEN: usize = 33;
 
 #[cfg(target_os = "redox")]
 pub const AT_REDOX_INHERITED_SIGIGNMASK: usize = 34;
@@ -40,3 +40,8 @@ pub const AT_REDOX_INHERITED_SIGIGNMASK_HI: usize = 35;
 pub const AT_REDOX_INHERITED_SIGPROCMASK: usize = 36;
 #[cfg(all(target_os = "redox", target_pointer_width = "32"))]
 pub const AT_REDOX_INHERITED_SIGPROCMASK_HI: usize = 37;
+
+#[cfg(target_os = "redox")]
+pub const AT_REDOX_INITIAL_DEFAULT_SCHEME_PTR: usize = 38;
+#[cfg(target_os = "redox")]
+pub const AT_REDOX_INITIAL_DEFAULT_SCHEME_LEN: usize = 39;
diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs
index c2f1d4b8afc820e07c6c18d6839c1042995a49e6..0646f1b78c434719313ee00336de7b091c3fb41f 100644
--- a/src/platform/linux/mod.rs
+++ b/src/platform/linux/mod.rs
@@ -1,5 +1,5 @@
-use crate::io::Write;
-use core::{arch::asm, mem::offset_of, ptr};
+use crate::{header::errno::EOPNOTSUPP, io::Write};
+use core::{arch::asm, ptr};
 
 use super::{types::*, Pal, ERRNO};
 use crate::{
@@ -100,6 +100,10 @@ impl Pal for Sys {
         e(unsafe { syscall!(CHDIR, path.as_ptr()) }) as c_int
     }
 
+    fn set_default_scheme(scheme: CStr) -> Result<(), Errno> {
+        Err(Errno(EOPNOTSUPP))
+    }
+
     fn chmod(path: CStr, mode: mode_t) -> c_int {
         e(unsafe { syscall!(FCHMODAT, AT_FDCWD, path.as_ptr(), mode, 0) }) as c_int
     }
diff --git a/src/platform/mod.rs b/src/platform/mod.rs
index 0ce9a4c07454aed335c821e7dc9702f24126cd6e..2b4071dd84dad5af1c241849e24428aa87aa9aae 100644
--- a/src/platform/mod.rs
+++ b/src/platform/mod.rs
@@ -287,13 +287,24 @@ pub fn init(auxvs: Box<[[usize; 2]]>) {
     use self::auxv_defs::*;
 
     if let (Some(cwd_ptr), Some(cwd_len)) = (
-        get_auxv(&auxvs, AT_REDOX_INITIALCWD_PTR),
-        get_auxv(&auxvs, AT_REDOX_INITIALCWD_LEN),
+        get_auxv(&auxvs, AT_REDOX_INITIAL_CWD_PTR),
+        get_auxv(&auxvs, AT_REDOX_INITIAL_CWD_LEN),
     ) {
         let cwd_bytes: &'static [u8] =
             unsafe { core::slice::from_raw_parts(cwd_ptr as *const u8, cwd_len) };
         if let Ok(cwd) = core::str::from_utf8(cwd_bytes) {
-            self::sys::path::setcwd_manual(cwd.into());
+            self::sys::path::set_cwd_manual(cwd.into());
+        }
+    }
+
+    if let (Some(scheme_ptr), Some(scheme_len)) = (
+        get_auxv(&auxvs, AT_REDOX_INITIAL_DEFAULT_SCHEME_PTR),
+        get_auxv(&auxvs, AT_REDOX_INITIAL_DEFAULT_SCHEME_LEN),
+    ) {
+        let scheme_bytes: &'static [u8] =
+            unsafe { core::slice::from_raw_parts(scheme_ptr as *const u8, scheme_len) };
+        if let Ok(scheme) = core::str::from_utf8(scheme_bytes) {
+            self::sys::path::set_default_scheme_manual(scheme.into());
         }
     }
 
diff --git a/src/platform/pal/mod.rs b/src/platform/pal/mod.rs
index 500a0f5de90df25e0d1f21a7761ddbbbb020ca33..3a234dfec37848f242580f44584bd386fc56cd08 100644
--- a/src/platform/pal/mod.rs
+++ b/src/platform/pal/mod.rs
@@ -3,7 +3,6 @@ use crate::{
     c_str::CStr,
     error::Errno,
     header::{
-        dirent::dirent,
         sys_resource::{rlimit, rusage},
         sys_stat::stat,
         sys_statvfs::statvfs,
@@ -33,6 +32,8 @@ pub trait Pal {
 
     fn chdir(path: CStr) -> c_int;
 
+    fn set_default_scheme(scheme: CStr) -> Result<(), Errno>;
+
     fn chmod(path: CStr, mode: mode_t) -> c_int;
 
     fn chown(path: CStr, owner: uid_t, group: gid_t) -> c_int;
@@ -191,10 +192,10 @@ pub trait Pal {
 
     fn pipe2(fildes: &mut [c_int], flags: c_int) -> c_int;
 
-    unsafe fn rlct_clone(stack: *mut usize) -> Result<crate::pthread::OsTid, Errno>;
-    unsafe fn rlct_kill(os_tid: crate::pthread::OsTid, signal: usize) -> Result<(), Errno>;
+    unsafe fn rlct_clone(stack: *mut usize) -> Result<pthread::OsTid, Errno>;
+    unsafe fn rlct_kill(os_tid: pthread::OsTid, signal: usize) -> Result<(), Errno>;
 
-    fn current_os_tid() -> crate::pthread::OsTid;
+    fn current_os_tid() -> pthread::OsTid;
 
     fn read(fildes: c_int, buf: &mut [u8]) -> Result<ssize_t, Errno>;
     fn pread(fildes: c_int, buf: &mut [u8], offset: off_t) -> Result<ssize_t, Errno>;
diff --git a/src/platform/redox/exec.rs b/src/platform/redox/exec.rs
index edc7304d37045e1eb0119f624cfaaf5d8bedb465..6b8c3856c72f5b6d5da8f72f34ecc3b448022638 100644
--- a/src/platform/redox/exec.rs
+++ b/src/platform/redox/exec.rs
@@ -131,6 +131,9 @@ pub fn execve(
     let wants_setugid = stat.st_mode & ((S_ISUID | S_ISGID) as u16) != 0;
 
     let cwd: Box<[u8]> = super::path::clone_cwd().unwrap_or_default().into();
+    let default_scheme: Box<[u8]> = super::path::clone_default_scheme()
+        .unwrap_or_else(|| Box::from("file"))
+        .into();
 
     // Count arguments
     let mut len = 0;
@@ -295,6 +298,7 @@ pub fn execve(
         let _ = syscall::write(*escalate_fd, &flatten_with_nul(args))?;
         let _ = syscall::write(*escalate_fd, &flatten_with_nul(envs))?;
         let _ = syscall::write(*escalate_fd, &cwd)?;
+        let _ = syscall::write(*escalate_fd, &default_scheme)?;
 
         // Closing will notify the scheme, and from that point we will no longer have control
         // over this process (unless it fails). We do this manually since drop cannot handle
@@ -310,6 +314,7 @@ pub fn execve(
 
         let extrainfo = ExtraInfo {
             cwd: Some(&cwd),
+            default_scheme: Some(&default_scheme),
             sigignmask: 0,
             sigprocmask,
         };
diff --git a/src/platform/redox/mod.rs b/src/platform/redox/mod.rs
index 534bb98272438b490fae473b441a0898a705bd5b..95c7b89847d340da8413ac7fe3fe11001aed79e7 100644
--- a/src/platform/redox/mod.rs
+++ b/src/platform/redox/mod.rs
@@ -175,6 +175,11 @@ impl Pal for Sys {
         e(path::chdir(path).map(|()| 0)) as c_int
     }
 
+    fn set_default_scheme(path: CStr) -> Result<(), Errno> {
+        let path = path.to_str().map_err(|_| Errno(EINVAL))?;
+        Ok(path::set_default_scheme(path)?)
+    }
+
     fn chmod(path: CStr, mode: mode_t) -> c_int {
         match File::open(path, fcntl::O_PATH | fcntl::O_CLOEXEC) {
             Ok(file) => Self::fchmod(*file, mode),
diff --git a/src/platform/redox/path.rs b/src/platform/redox/path.rs
index 01c075748bad8c5e92fb1a51cb2aaa04e892a2e0..414056a63c3be99758ee56ebf35f4ce58d055bfc 100644
--- a/src/platform/redox/path.rs
+++ b/src/platform/redox/path.rs
@@ -23,28 +23,21 @@ pub fn chdir(path: &str) -> Result<()> {
     let _siglock = tmp_disable_signals();
     let mut cwd_guard = CWD.lock();
 
-    let canonicalized =
-        canonicalize_using_cwd(cwd_guard.as_deref(), path).ok_or(Error::new(ENOENT))?;
+    let canon = canonicalize_using_cwd(cwd_guard.as_deref(), path).ok_or(Error::new(ENOENT))?;
+    let canon_with_scheme = canonicalize_with_cwd_internal(cwd_guard.as_deref(), path)?;
 
-    let fd = syscall::open(&canonicalized, O_STAT | O_CLOEXEC)?;
+    let fd = syscall::open(&canon_with_scheme, O_STAT | O_CLOEXEC)?;
     let mut stat = Stat::default();
     if syscall::fstat(fd, &mut stat).is_err() || (stat.st_mode & MODE_TYPE) != MODE_DIR {
         return Err(Error::new(ENOTDIR));
     }
     let _ = syscall::close(fd);
 
-    *cwd_guard = Some(canonicalized.into_boxed_str());
-
-    // TODO: Check that the dir exists and is a directory.
+    *cwd_guard = Some(canon.into_boxed_str());
 
     Ok(())
 }
 
-pub fn clone_cwd() -> Option<Box<str>> {
-    let _siglock = tmp_disable_signals();
-    CWD.lock().clone()
-}
-
 // TODO: MaybeUninit
 pub fn getcwd(buf: &mut [u8]) -> Option<usize> {
     let _siglock = tmp_disable_signals();
@@ -62,20 +55,81 @@ pub fn getcwd(buf: &mut [u8]) -> Option<usize> {
     Some(cwd.len())
 }
 
+/// Sets the default scheme
+///
+/// By default absolute paths resolve to /scheme/file, calling this function
+/// allows a different scheme to be used as the root. This property is inherited
+/// by child processes.
+///
+/// Resets CWD to /.
+pub fn set_default_scheme(scheme: &str) -> Result<()> {
+    let _siglock = tmp_disable_signals();
+    let mut cwd_guard = CWD.lock();
+    let mut default_scheme_guard = DEFAULT_SCHEME.lock();
+
+    *cwd_guard = None;
+    *default_scheme_guard = Some(scheme.into());
+
+    Ok(())
+}
+
+// TODO: How much of this logic should be in redox-path?
+fn canonicalize_with_cwd_internal(cwd: Option<&str>, path: &str) -> Result<String> {
+    let path = canonicalize_using_cwd(cwd, path).ok_or(Error::new(ENOENT))?;
+
+    let standard_scheme = path == "/scheme" || path.starts_with("/scheme/");
+    let legacy_scheme = path
+        .split("/")
+        .next()
+        .map(|c| c.contains(":"))
+        .unwrap_or(false);
+
+    Ok(if standard_scheme || legacy_scheme {
+        path
+    } else {
+        let mut default_scheme_guard = DEFAULT_SCHEME.lock();
+        let default_scheme = default_scheme_guard.get_or_insert_with(|| Box::from("file"));
+        let mut result = format!("/scheme/{}{}", default_scheme, path);
+
+        // Trim trailing / to keep path canonical.
+        if result.as_bytes().last() == Some(&b'/') {
+            result.pop();
+        }
+
+        result
+    })
+}
+
 pub fn canonicalize(path: &str) -> Result<String> {
     let _siglock = tmp_disable_signals();
-    let cwd = CWD.lock();
-    canonicalize_using_cwd(cwd.as_deref(), path).ok_or(Error::new(ENOENT))
+    let cwd_guard = CWD.lock();
+    canonicalize_with_cwd_internal(cwd_guard.as_deref(), path)
 }
 
 // TODO: arraystring?
 static CWD: Mutex<Option<Box<str>>> = Mutex::new(None);
+static DEFAULT_SCHEME: Mutex<Option<Box<str>>> = Mutex::new(None);
 
-pub fn setcwd_manual(cwd: Box<str>) {
+pub fn set_cwd_manual(cwd: Box<str>) {
     let _siglock = tmp_disable_signals();
     *CWD.lock() = Some(cwd);
 }
 
+pub fn set_default_scheme_manual(scheme: Box<str>) {
+    let _siglock = tmp_disable_signals();
+    *DEFAULT_SCHEME.lock() = Some(scheme)
+}
+
+pub fn clone_cwd() -> Option<Box<str>> {
+    let _siglock = tmp_disable_signals();
+    CWD.lock().clone()
+}
+
+pub fn clone_default_scheme() -> Option<Box<str>> {
+    let _siglock = tmp_disable_signals();
+    DEFAULT_SCHEME.lock().clone()
+}
+
 pub fn open(path: &str, flags: usize) -> Result<usize> {
     // TODO: SYMLOOP_MAX
     const MAX_LEVEL: usize = 64;
@@ -84,7 +138,7 @@ pub fn open(path: &str, flags: usize) -> Result<usize> {
     let mut path = path;
 
     for _ in 0..MAX_LEVEL {
-        let canon = canonicalize(path)?;
+        let canon = canonicalize_with_cwd_internal(CWD.lock().as_deref(), path)?;
 
         let open_res = if canon.starts_with(libcscheme::LIBC_SCHEME) {
             libcscheme::open(&canon, flags)