about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/tools/miri/src/machine.rs11
-rw-r--r--src/tools/miri/src/shims/env.rs549
-rw-r--r--src/tools/miri/src/shims/extern_static.rs14
-rw-r--r--src/tools/miri/src/shims/unix/env.rs276
-rw-r--r--src/tools/miri/src/shims/unix/macos/foreign_items.rs11
-rw-r--r--src/tools/miri/src/shims/unix/mod.rs5
-rw-r--r--src/tools/miri/src/shims/windows/env.rs257
-rw-r--r--src/tools/miri/src/shims/windows/foreign_items.rs5
-rw-r--r--src/tools/miri/src/shims/windows/mod.rs8
-rw-r--r--src/tools/miri/tests/pass/shims/env/var.rs8
10 files changed, 614 insertions, 530 deletions
diff --git a/src/tools/miri/src/machine.rs b/src/tools/miri/src/machine.rs
index cbe70cbffee..8b286871774 100644
--- a/src/tools/miri/src/machine.rs
+++ b/src/tools/miri/src/machine.rs
@@ -31,7 +31,7 @@ use rustc_target::spec::abi::Abi;
 
 use crate::{
     concurrency::{data_race, weak_memory},
-    shims::unix::FdTable,
+    shims::unix,
     *,
 };
 
@@ -439,8 +439,7 @@ pub struct MiriMachine<'mir, 'tcx> {
     /// Ptr-int-cast module global data.
     pub alloc_addresses: alloc_addresses::GlobalState,
 
-    /// Environment variables set by `setenv`.
-    /// Miri does not expose env vars from the host to the emulated program.
+    /// Environment variables.
     pub(crate) env_vars: EnvVars<'tcx>,
 
     /// Return place of the main function.
@@ -465,9 +464,9 @@ pub struct MiriMachine<'mir, 'tcx> {
     pub(crate) validate: bool,
 
     /// The table of file descriptors.
-    pub(crate) fds: shims::unix::FdTable,
+    pub(crate) fds: unix::FdTable,
     /// The table of directory descriptors.
-    pub(crate) dirs: shims::unix::DirTable,
+    pub(crate) dirs: unix::DirTable,
 
     /// This machine's monotone clock.
     pub(crate) clock: Clock,
@@ -642,7 +641,7 @@ impl<'mir, 'tcx> MiriMachine<'mir, 'tcx> {
             tls: TlsData::default(),
             isolated_op: config.isolated_op,
             validate: config.validate,
-            fds: FdTable::new(config.mute_stdout_stderr),
+            fds: unix::FdTable::new(config.mute_stdout_stderr),
             dirs: Default::default(),
             layouts,
             threads: ThreadManager::default(),
diff --git a/src/tools/miri/src/shims/env.rs b/src/tools/miri/src/shims/env.rs
index 298fefdb0f3..fc0160fdf21 100644
--- a/src/tools/miri/src/shims/env.rs
+++ b/src/tools/miri/src/shims/env.rs
@@ -1,33 +1,24 @@
-use std::env;
-use std::ffi::{OsStr, OsString};
-use std::io::ErrorKind;
-use std::mem;
+use std::ffi::OsString;
 
 use rustc_data_structures::fx::FxHashMap;
-use rustc_middle::ty::layout::LayoutOf;
-use rustc_middle::ty::Ty;
-use rustc_target::abi::Size;
 
 use crate::*;
-use helpers::windows_check_buffer_size;
+use shims::{unix::UnixEnvVars, windows::WindowsEnvVars};
 
 #[derive(Default)]
-pub struct EnvVars<'tcx> {
-    /// Stores pointers to the environment variables. These variables must be stored as
-    /// null-terminated target strings (c_str or wide_str) with the `"{name}={value}"` format.
-    map: FxHashMap<OsString, Pointer<Option<Provenance>>>,
-
-    /// Place where the `environ` static is stored. Lazily initialized, but then never changes.
-    pub(crate) environ: Option<MPlaceTy<'tcx, Provenance>>,
+pub enum EnvVars<'tcx> {
+    #[default]
+    Uninit,
+    Unix(UnixEnvVars<'tcx>),
+    Windows(WindowsEnvVars),
 }
 
 impl VisitProvenance for EnvVars<'_> {
     fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
-        let EnvVars { map, environ } = self;
-
-        environ.visit_provenance(visit);
-        for ptr in map.values() {
-            ptr.visit_provenance(visit);
+        match self {
+            EnvVars::Uninit => {}
+            EnvVars::Unix(env) => env.visit_provenance(visit),
+            EnvVars::Windows(env) => env.visit_provenance(visit),
         }
     }
 }
@@ -39,517 +30,73 @@ impl<'tcx> EnvVars<'tcx> {
     ) -> InterpResult<'tcx> {
         // Initialize the `env_vars` map.
         // Skip the loop entirely if we don't want to forward anything.
+        let mut env_vars = FxHashMap::default();
         if ecx.machine.communicate() || !config.forwarded_env_vars.is_empty() {
             for (name, value) in &config.env {
                 let forward = ecx.machine.communicate()
                     || config.forwarded_env_vars.iter().any(|v| **v == *name);
                 if forward {
-                    add_env_var(ecx, name, value)?;
+                    env_vars.insert(OsString::from(name), OsString::from(value));
                 }
             }
         }
 
         for (name, value) in &config.set_env_vars {
-            add_env_var(ecx, OsStr::new(name), OsStr::new(value))?;
+            env_vars.insert(OsString::from(name), OsString::from(value));
         }
 
-        // Initialize the `environ` pointer when needed.
-        if ecx.target_os_is_unix() {
-            // This is memory backing an extern static, hence `ExternStatic`, not `Env`.
-            let layout = ecx.machine.layouts.mut_raw_ptr;
-            let place = ecx.allocate(layout, MiriMemoryKind::ExternStatic.into())?;
-            ecx.write_null(&place)?;
-            ecx.machine.env_vars.environ = Some(place);
-            ecx.update_environ()?;
-        }
+        let env_vars = if ecx.target_os_is_unix() {
+            EnvVars::Unix(UnixEnvVars::new(ecx, env_vars)?)
+        } else if ecx.tcx.sess.target.os == "windows" {
+            EnvVars::Windows(WindowsEnvVars::new(ecx, env_vars)?)
+        } else {
+            // Used e.g. for wasi
+            EnvVars::Uninit
+        };
+        ecx.machine.env_vars = env_vars;
+
         Ok(())
     }
 
     pub(crate) fn cleanup<'mir>(
         ecx: &mut InterpCx<'mir, 'tcx, MiriMachine<'mir, 'tcx>>,
     ) -> InterpResult<'tcx> {
-        // Deallocate individual env vars.
-        let env_vars = mem::take(&mut ecx.machine.env_vars.map);
-        for (_name, ptr) in env_vars {
-            ecx.deallocate_ptr(ptr, None, MiriMemoryKind::Runtime.into())?;
-        }
-        // Deallocate environ var list.
-        if ecx.target_os_is_unix() {
-            let environ = ecx.machine.env_vars.environ.as_ref().unwrap();
-            let old_vars_ptr = ecx.read_pointer(environ)?;
-            ecx.deallocate_ptr(old_vars_ptr, None, MiriMemoryKind::Runtime.into())?;
-        }
-        Ok(())
-    }
-}
-
-fn add_env_var<'mir, 'tcx>(
-    ecx: &mut InterpCx<'mir, 'tcx, MiriMachine<'mir, 'tcx>>,
-    name: &OsStr,
-    value: &OsStr,
-) -> InterpResult<'tcx, ()> {
-    let var_ptr = match ecx.tcx.sess.target.os.as_ref() {
-        _ if ecx.target_os_is_unix() => alloc_env_var_as_c_str(name, value, ecx)?,
-        "windows" => alloc_env_var_as_wide_str(name, value, ecx)?,
-        unsupported =>
-            throw_unsup_format!(
-                "environment support for target OS `{}` not yet available",
-                unsupported
-            ),
-    };
-    ecx.machine.env_vars.map.insert(name.to_os_string(), var_ptr);
-    Ok(())
-}
-
-fn alloc_env_var_as_c_str<'mir, 'tcx>(
-    name: &OsStr,
-    value: &OsStr,
-    ecx: &mut InterpCx<'mir, 'tcx, MiriMachine<'mir, 'tcx>>,
-) -> InterpResult<'tcx, Pointer<Option<Provenance>>> {
-    let mut name_osstring = name.to_os_string();
-    name_osstring.push("=");
-    name_osstring.push(value);
-    ecx.alloc_os_str_as_c_str(name_osstring.as_os_str(), MiriMemoryKind::Runtime.into())
-}
-
-fn alloc_env_var_as_wide_str<'mir, 'tcx>(
-    name: &OsStr,
-    value: &OsStr,
-    ecx: &mut InterpCx<'mir, 'tcx, MiriMachine<'mir, 'tcx>>,
-) -> InterpResult<'tcx, Pointer<Option<Provenance>>> {
-    let mut name_osstring = name.to_os_string();
-    name_osstring.push("=");
-    name_osstring.push(value);
-    ecx.alloc_os_str_as_wide_str(name_osstring.as_os_str(), MiriMemoryKind::Runtime.into())
-}
-
-impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
-pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
-    fn getenv(
-        &mut self,
-        name_op: &OpTy<'tcx, Provenance>,
-    ) -> InterpResult<'tcx, Pointer<Option<Provenance>>> {
-        let this = self.eval_context_mut();
-        this.assert_target_os_is_unix("getenv");
-
-        let name_ptr = this.read_pointer(name_op)?;
-        let name = this.read_os_str_from_c_str(name_ptr)?;
-        this.read_environ()?;
-        Ok(match this.machine.env_vars.map.get(name) {
-            Some(var_ptr) => {
-                // The offset is used to strip the "{name}=" part of the string.
-                var_ptr.offset(
-                    Size::from_bytes(u64::try_from(name.len()).unwrap().checked_add(1).unwrap()),
-                    this,
-                )?
-            }
-            None => Pointer::null(),
-        })
-    }
-
-    #[allow(non_snake_case)]
-    fn GetEnvironmentVariableW(
-        &mut self,
-        name_op: &OpTy<'tcx, Provenance>, // LPCWSTR
-        buf_op: &OpTy<'tcx, Provenance>,  // LPWSTR
-        size_op: &OpTy<'tcx, Provenance>, // DWORD
-    ) -> InterpResult<'tcx, Scalar<Provenance>> {
-        // ^ Returns DWORD (u32 on Windows)
-
-        let this = self.eval_context_mut();
-        this.assert_target_os("windows", "GetEnvironmentVariableW");
-
-        let name_ptr = this.read_pointer(name_op)?;
-        let buf_ptr = this.read_pointer(buf_op)?;
-        let buf_size = this.read_scalar(size_op)?.to_u32()?; // in characters
-
-        let name = this.read_os_str_from_wide_str(name_ptr)?;
-        Ok(match this.machine.env_vars.map.get(&name) {
-            Some(&var_ptr) => {
-                // The offset is used to strip the "{name}=" part of the string.
-                #[rustfmt::skip]
-                let name_offset_bytes = u64::try_from(name.len()).unwrap()
-                    .checked_add(1).unwrap()
-                    .checked_mul(2).unwrap();
-                let var_ptr = var_ptr.offset(Size::from_bytes(name_offset_bytes), this)?;
-                let var = this.read_os_str_from_wide_str(var_ptr)?;
-
-                Scalar::from_u32(windows_check_buffer_size(this.write_os_str_to_wide_str(
-                    &var,
-                    buf_ptr,
-                    buf_size.into(),
-                )?))
-                // This can in fact return 0. It is up to the caller to set last_error to 0
-                // beforehand and check it afterwards to exclude that case.
-            }
-            None => {
-                let envvar_not_found = this.eval_windows("c", "ERROR_ENVVAR_NOT_FOUND");
-                this.set_last_error(envvar_not_found)?;
-                Scalar::from_u32(0) // return zero upon failure
-            }
-        })
-    }
-
-    #[allow(non_snake_case)]
-    fn GetEnvironmentStringsW(&mut self) -> InterpResult<'tcx, Pointer<Option<Provenance>>> {
-        let this = self.eval_context_mut();
-        this.assert_target_os("windows", "GetEnvironmentStringsW");
-
-        // Info on layout of environment blocks in Windows:
-        // https://docs.microsoft.com/en-us/windows/win32/procthread/environment-variables
-        let mut env_vars = std::ffi::OsString::new();
-        for &item in this.machine.env_vars.map.values() {
-            let env_var = this.read_os_str_from_wide_str(item)?;
-            env_vars.push(env_var);
-            env_vars.push("\0");
-        }
-        // Allocate environment block & Store environment variables to environment block.
-        // Final null terminator(block terminator) is added by `alloc_os_str_to_wide_str`.
-        let envblock_ptr =
-            this.alloc_os_str_as_wide_str(&env_vars, MiriMemoryKind::Runtime.into())?;
-        // If the function succeeds, the return value is a pointer to the environment block of the current process.
-        Ok(envblock_ptr)
-    }
-
-    #[allow(non_snake_case)]
-    fn FreeEnvironmentStringsW(
-        &mut self,
-        env_block_op: &OpTy<'tcx, Provenance>,
-    ) -> InterpResult<'tcx, Scalar<Provenance>> {
-        let this = self.eval_context_mut();
-        this.assert_target_os("windows", "FreeEnvironmentStringsW");
-
-        let env_block_ptr = this.read_pointer(env_block_op)?;
-        let result = this.deallocate_ptr(env_block_ptr, None, MiriMemoryKind::Runtime.into());
-        // If the function succeeds, the return value is nonzero.
-        Ok(Scalar::from_i32(i32::from(result.is_ok())))
-    }
-
-    fn setenv(
-        &mut self,
-        name_op: &OpTy<'tcx, Provenance>,
-        value_op: &OpTy<'tcx, Provenance>,
-    ) -> InterpResult<'tcx, i32> {
-        let this = self.eval_context_mut();
-        this.assert_target_os_is_unix("setenv");
-
-        let name_ptr = this.read_pointer(name_op)?;
-        let value_ptr = this.read_pointer(value_op)?;
-
-        let mut new = None;
-        if !this.ptr_is_null(name_ptr)? {
-            let name = this.read_os_str_from_c_str(name_ptr)?;
-            if !name.is_empty() && !name.to_string_lossy().contains('=') {
-                let value = this.read_os_str_from_c_str(value_ptr)?;
-                new = Some((name.to_owned(), value.to_owned()));
-            }
-        }
-        if let Some((name, value)) = new {
-            let var_ptr = alloc_env_var_as_c_str(&name, &value, this)?;
-            if let Some(var) = this.machine.env_vars.map.insert(name, var_ptr) {
-                this.deallocate_ptr(var, None, MiriMemoryKind::Runtime.into())?;
-            }
-            this.update_environ()?;
-            Ok(0) // return zero on success
-        } else {
-            // name argument is a null pointer, points to an empty string, or points to a string containing an '=' character.
-            let einval = this.eval_libc("EINVAL");
-            this.set_last_error(einval)?;
-            Ok(-1)
-        }
-    }
-
-    #[allow(non_snake_case)]
-    fn SetEnvironmentVariableW(
-        &mut self,
-        name_op: &OpTy<'tcx, Provenance>,  // LPCWSTR
-        value_op: &OpTy<'tcx, Provenance>, // LPCWSTR
-    ) -> InterpResult<'tcx, Scalar<Provenance>> {
-        let this = self.eval_context_mut();
-        this.assert_target_os("windows", "SetEnvironmentVariableW");
-
-        let name_ptr = this.read_pointer(name_op)?;
-        let value_ptr = this.read_pointer(value_op)?;
-
-        if this.ptr_is_null(name_ptr)? {
-            // ERROR CODE is not clearly explained in docs.. For now, throw UB instead.
-            throw_ub_format!("pointer to environment variable name is NULL");
-        }
-
-        let name = this.read_os_str_from_wide_str(name_ptr)?;
-        if name.is_empty() {
-            throw_unsup_format!("environment variable name is an empty string");
-        } else if name.to_string_lossy().contains('=') {
-            throw_unsup_format!("environment variable name contains '='");
-        } else if this.ptr_is_null(value_ptr)? {
-            // Delete environment variable `{name}`
-            if let Some(var) = this.machine.env_vars.map.remove(&name) {
-                this.deallocate_ptr(var, None, MiriMemoryKind::Runtime.into())?;
-            }
-            Ok(this.eval_windows("c", "TRUE"))
-        } else {
-            let value = this.read_os_str_from_wide_str(value_ptr)?;
-            let var_ptr = alloc_env_var_as_wide_str(&name, &value, this)?;
-            if let Some(var) = this.machine.env_vars.map.insert(name, var_ptr) {
-                this.deallocate_ptr(var, None, MiriMemoryKind::Runtime.into())?;
-            }
-            Ok(this.eval_windows("c", "TRUE"))
-        }
-    }
-
-    fn unsetenv(&mut self, name_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, i32> {
-        let this = self.eval_context_mut();
-        this.assert_target_os_is_unix("unsetenv");
-
-        let name_ptr = this.read_pointer(name_op)?;
-        let mut success = None;
-        if !this.ptr_is_null(name_ptr)? {
-            let name = this.read_os_str_from_c_str(name_ptr)?.to_owned();
-            if !name.is_empty() && !name.to_string_lossy().contains('=') {
-                success = Some(this.machine.env_vars.map.remove(&name));
-            }
-        }
-        if let Some(old) = success {
-            if let Some(var) = old {
-                this.deallocate_ptr(var, None, MiriMemoryKind::Runtime.into())?;
-            }
-            this.update_environ()?;
-            Ok(0)
-        } else {
-            // name argument is a null pointer, points to an empty string, or points to a string containing an '=' character.
-            let einval = this.eval_libc("EINVAL");
-            this.set_last_error(einval)?;
-            Ok(-1)
+        let this = ecx.eval_context_mut();
+        match this.machine.env_vars {
+            EnvVars::Unix(_) => UnixEnvVars::cleanup(this),
+            EnvVars::Windows(_) => Ok(()), // no cleanup needed
+            EnvVars::Uninit => Ok(()),
         }
     }
 
-    fn getcwd(
-        &mut self,
-        buf_op: &OpTy<'tcx, Provenance>,
-        size_op: &OpTy<'tcx, Provenance>,
-    ) -> InterpResult<'tcx, Pointer<Option<Provenance>>> {
-        let this = self.eval_context_mut();
-        this.assert_target_os_is_unix("getcwd");
-
-        let buf = this.read_pointer(buf_op)?;
-        let size = this.read_target_usize(size_op)?;
-
-        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
-            this.reject_in_isolation("`getcwd`", reject_with)?;
-            this.set_last_error_from_io_error(ErrorKind::PermissionDenied)?;
-            return Ok(Pointer::null());
+    pub(crate) fn unix(&self) -> &UnixEnvVars<'tcx> {
+        match self {
+            EnvVars::Unix(env) => env,
+            _ => unreachable!(),
         }
-
-        // If we cannot get the current directory, we return null
-        match env::current_dir() {
-            Ok(cwd) => {
-                if this.write_path_to_c_str(&cwd, buf, size)?.0 {
-                    return Ok(buf);
-                }
-                let erange = this.eval_libc("ERANGE");
-                this.set_last_error(erange)?;
-            }
-            Err(e) => this.set_last_error_from_io_error(e.kind())?,
-        }
-
-        Ok(Pointer::null())
     }
 
-    #[allow(non_snake_case)]
-    fn GetCurrentDirectoryW(
-        &mut self,
-        size_op: &OpTy<'tcx, Provenance>, // DWORD
-        buf_op: &OpTy<'tcx, Provenance>,  // LPTSTR
-    ) -> InterpResult<'tcx, Scalar<Provenance>> {
-        let this = self.eval_context_mut();
-        this.assert_target_os("windows", "GetCurrentDirectoryW");
-
-        let size = u64::from(this.read_scalar(size_op)?.to_u32()?);
-        let buf = this.read_pointer(buf_op)?;
-
-        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
-            this.reject_in_isolation("`GetCurrentDirectoryW`", reject_with)?;
-            this.set_last_error_from_io_error(ErrorKind::PermissionDenied)?;
-            return Ok(Scalar::from_u32(0));
+    pub(crate) fn unix_mut(&mut self) -> &mut UnixEnvVars<'tcx> {
+        match self {
+            EnvVars::Unix(env) => env,
+            _ => unreachable!(),
         }
-
-        // If we cannot get the current directory, we return 0
-        match env::current_dir() {
-            Ok(cwd) => {
-                // This can in fact return 0. It is up to the caller to set last_error to 0
-                // beforehand and check it afterwards to exclude that case.
-                return Ok(Scalar::from_u32(windows_check_buffer_size(
-                    this.write_path_to_wide_str(&cwd, buf, size)?,
-                )));
-            }
-            Err(e) => this.set_last_error_from_io_error(e.kind())?,
-        }
-        Ok(Scalar::from_u32(0))
     }
 
-    fn chdir(&mut self, path_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, i32> {
-        let this = self.eval_context_mut();
-        this.assert_target_os_is_unix("chdir");
-
-        let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
-
-        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
-            this.reject_in_isolation("`chdir`", reject_with)?;
-            this.set_last_error_from_io_error(ErrorKind::PermissionDenied)?;
-
-            return Ok(-1);
-        }
-
-        match env::set_current_dir(path) {
-            Ok(()) => Ok(0),
-            Err(e) => {
-                this.set_last_error_from_io_error(e.kind())?;
-                Ok(-1)
-            }
+    pub(crate) fn windows(&self) -> &WindowsEnvVars {
+        match self {
+            EnvVars::Windows(env) => env,
+            _ => unreachable!(),
         }
     }
 
-    #[allow(non_snake_case)]
-    fn SetCurrentDirectoryW(
-        &mut self,
-        path_op: &OpTy<'tcx, Provenance>, // LPCTSTR
-    ) -> InterpResult<'tcx, Scalar<Provenance>> {
-        // ^ Returns BOOL (i32 on Windows)
-
-        let this = self.eval_context_mut();
-        this.assert_target_os("windows", "SetCurrentDirectoryW");
-
-        let path = this.read_path_from_wide_str(this.read_pointer(path_op)?)?;
-
-        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
-            this.reject_in_isolation("`SetCurrentDirectoryW`", reject_with)?;
-            this.set_last_error_from_io_error(ErrorKind::PermissionDenied)?;
-
-            return Ok(this.eval_windows("c", "FALSE"));
-        }
-
-        match env::set_current_dir(path) {
-            Ok(()) => Ok(this.eval_windows("c", "TRUE")),
-            Err(e) => {
-                this.set_last_error_from_io_error(e.kind())?;
-                Ok(this.eval_windows("c", "FALSE"))
-            }
+    pub(crate) fn windows_mut(&mut self) -> &mut WindowsEnvVars {
+        match self {
+            EnvVars::Windows(env) => env,
+            _ => unreachable!(),
         }
     }
-
-    /// Updates the `environ` static.
-    /// The first time it gets called, also initializes `extra.environ`.
-    fn update_environ(&mut self) -> InterpResult<'tcx> {
-        let this = self.eval_context_mut();
-        // Deallocate the old environ list, if any.
-        let environ = this.machine.env_vars.environ.as_ref().unwrap().clone();
-        let old_vars_ptr = this.read_pointer(&environ)?;
-        if !this.ptr_is_null(old_vars_ptr)? {
-            this.deallocate_ptr(old_vars_ptr, None, MiriMemoryKind::Runtime.into())?;
-        }
-
-        // Collect all the pointers to each variable in a vector.
-        let mut vars: Vec<Pointer<Option<Provenance>>> =
-            this.machine.env_vars.map.values().copied().collect();
-        // Add the trailing null pointer.
-        vars.push(Pointer::null());
-        // Make an array with all these pointers inside Miri.
-        let tcx = this.tcx;
-        let vars_layout = this.layout_of(Ty::new_array(
-            tcx.tcx,
-            this.machine.layouts.mut_raw_ptr.ty,
-            u64::try_from(vars.len()).unwrap(),
-        ))?;
-        let vars_place = this.allocate(vars_layout, MiriMemoryKind::Runtime.into())?;
-        for (idx, var) in vars.into_iter().enumerate() {
-            let place = this.project_field(&vars_place, idx)?;
-            this.write_pointer(var, &place)?;
-        }
-        this.write_pointer(vars_place.ptr(), &environ)?;
-
-        Ok(())
-    }
-
-    /// Reads from the `environ` static.
-    /// We don't actually care about the result, but we care about this potentially causing a data race.
-    fn read_environ(&self) -> InterpResult<'tcx> {
-        let this = self.eval_context_ref();
-        let environ = this.machine.env_vars.environ.as_ref().unwrap();
-        let _vars_ptr = this.read_pointer(environ)?;
-        Ok(())
-    }
-
-    fn getpid(&mut self) -> InterpResult<'tcx, i32> {
-        let this = self.eval_context_mut();
-        this.assert_target_os_is_unix("getpid");
-
-        this.check_no_isolation("`getpid`")?;
-
-        // The reason we need to do this wacky of a conversion is because
-        // `libc::getpid` returns an i32, however, `std::process::id()` return an u32.
-        // So we un-do the conversion that stdlib does and turn it back into an i32.
-        #[allow(clippy::cast_possible_wrap)]
-        Ok(std::process::id() as i32)
-    }
-
-    #[allow(non_snake_case)]
-    fn GetCurrentProcessId(&mut self) -> InterpResult<'tcx, u32> {
-        let this = self.eval_context_mut();
-        this.assert_target_os("windows", "GetCurrentProcessId");
-        this.check_no_isolation("`GetCurrentProcessId`")?;
-
-        Ok(std::process::id())
-    }
-
-    #[allow(non_snake_case)]
-    fn GetUserProfileDirectoryW(
-        &mut self,
-        token: &OpTy<'tcx, Provenance>, // HANDLE
-        buf: &OpTy<'tcx, Provenance>,   // LPWSTR
-        size: &OpTy<'tcx, Provenance>,  // LPDWORD
-    ) -> InterpResult<'tcx, Scalar<Provenance>> // returns BOOL
-    {
-        let this = self.eval_context_mut();
-        this.assert_target_os("windows", "GetUserProfileDirectoryW");
-        this.check_no_isolation("`GetUserProfileDirectoryW`")?;
-
-        let token = this.read_target_isize(token)?;
-        let buf = this.read_pointer(buf)?;
-        let size = this.deref_pointer(size)?;
-
-        if token != -4 {
-            throw_unsup_format!(
-                "GetUserProfileDirectoryW: only CURRENT_PROCESS_TOKEN is supported"
-            );
-        }
-
-        // See <https://learn.microsoft.com/en-us/windows/win32/api/userenv/nf-userenv-getuserprofiledirectoryw> for docs.
-        Ok(match directories::UserDirs::new() {
-            Some(dirs) => {
-                let home = dirs.home_dir();
-                let size_avail = if this.ptr_is_null(size.ptr())? {
-                    0 // if the buf pointer is null, we can't write to it; `size` will be updated to the required length
-                } else {
-                    this.read_scalar(&size)?.to_u32()?
-                };
-                // Of course we cannot use `windows_check_buffer_size` here since this uses
-                // a different method for dealing with a too-small buffer than the other functions...
-                let (success, len) = this.write_path_to_wide_str(home, buf, size_avail.into())?;
-                // The Windows docs just say that this is written on failure. But std
-                // seems to rely on it always being written.
-                this.write_scalar(Scalar::from_u32(len.try_into().unwrap()), &size)?;
-                if success {
-                    Scalar::from_i32(1) // return TRUE
-                } else {
-                    this.set_last_error(this.eval_windows("c", "ERROR_INSUFFICIENT_BUFFER"))?;
-                    Scalar::from_i32(0) // return FALSE
-                }
-            }
-            None => {
-                // We have to pick some error code.
-                this.set_last_error(this.eval_windows("c", "ERROR_BAD_USER_PROFILE"))?;
-                Scalar::from_i32(0) // return FALSE
-            }
-        })
-    }
 }
+
+impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
+pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {}
diff --git a/src/tools/miri/src/shims/extern_static.rs b/src/tools/miri/src/shims/extern_static.rs
index 7c4a54fb461..442338a6117 100644
--- a/src/tools/miri/src/shims/extern_static.rs
+++ b/src/tools/miri/src/shims/extern_static.rs
@@ -47,20 +47,14 @@ impl<'mir, 'tcx> MiriMachine<'mir, 'tcx> {
                     &["__cxa_thread_atexit_impl", "getrandom", "statx", "__clock_gettime64"],
                 )?;
                 // "environ"
-                Self::add_extern_static(
-                    this,
-                    "environ",
-                    this.machine.env_vars.environ.as_ref().unwrap().ptr(),
-                );
+                let environ = this.machine.env_vars.unix().environ();
+                Self::add_extern_static(this, "environ", environ);
             }
             "freebsd" => {
                 Self::null_ptr_extern_statics(this, &["__cxa_thread_atexit_impl"])?;
                 // "environ"
-                Self::add_extern_static(
-                    this,
-                    "environ",
-                    this.machine.env_vars.environ.as_ref().unwrap().ptr(),
-                );
+                let environ = this.machine.env_vars.unix().environ();
+                Self::add_extern_static(this, "environ", environ);
             }
             "android" => {
                 Self::null_ptr_extern_statics(this, &["bsd_signal"])?;
diff --git a/src/tools/miri/src/shims/unix/env.rs b/src/tools/miri/src/shims/unix/env.rs
new file mode 100644
index 00000000000..128f0dcafa9
--- /dev/null
+++ b/src/tools/miri/src/shims/unix/env.rs
@@ -0,0 +1,276 @@
+use std::env;
+use std::ffi::{OsStr, OsString};
+use std::io::ErrorKind;
+use std::mem;
+
+use rustc_data_structures::fx::FxHashMap;
+use rustc_middle::ty::layout::LayoutOf;
+use rustc_middle::ty::Ty;
+use rustc_target::abi::Size;
+
+use crate::*;
+
+pub struct UnixEnvVars<'tcx> {
+    /// Stores pointers to the environment variables. These variables must be stored as
+    /// null-terminated target strings (c_str or wide_str) with the `"{name}={value}"` format.
+    map: FxHashMap<OsString, Pointer<Option<Provenance>>>,
+
+    /// Place where the `environ` static is stored. Lazily initialized, but then never changes.
+    environ: MPlaceTy<'tcx, Provenance>,
+}
+
+impl VisitProvenance for UnixEnvVars<'_> {
+    fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
+        let UnixEnvVars { map, environ } = self;
+
+        environ.visit_provenance(visit);
+        for ptr in map.values() {
+            ptr.visit_provenance(visit);
+        }
+    }
+}
+
+impl<'tcx> UnixEnvVars<'tcx> {
+    pub(crate) fn new<'mir>(
+        ecx: &mut InterpCx<'mir, 'tcx, MiriMachine<'mir, 'tcx>>,
+        env_vars: FxHashMap<OsString, OsString>,
+    ) -> InterpResult<'tcx, Self> {
+        // Allocate memory for all these env vars.
+        let mut env_vars_machine = FxHashMap::default();
+        for (name, val) in env_vars.into_iter() {
+            let ptr = alloc_env_var(ecx, &name, &val)?;
+            env_vars_machine.insert(name, ptr);
+        }
+
+        // This is memory backing an extern static, hence `ExternStatic`, not `Env`.
+        let layout = ecx.machine.layouts.mut_raw_ptr;
+        let environ = ecx.allocate(layout, MiriMemoryKind::ExternStatic.into())?;
+        let environ_block = alloc_environ_block(ecx, env_vars_machine.values().copied().collect())?;
+        ecx.write_pointer(environ_block, &environ)?;
+
+        Ok(UnixEnvVars { map: env_vars_machine, environ })
+    }
+
+    pub(crate) fn cleanup<'mir>(
+        ecx: &mut InterpCx<'mir, 'tcx, MiriMachine<'mir, 'tcx>>,
+    ) -> InterpResult<'tcx> {
+        // Deallocate individual env vars.
+        let env_vars = mem::take(&mut ecx.machine.env_vars.unix_mut().map);
+        for (_name, ptr) in env_vars {
+            ecx.deallocate_ptr(ptr, None, MiriMemoryKind::Runtime.into())?;
+        }
+        // Deallocate environ var list.
+        let environ = &ecx.machine.env_vars.unix().environ;
+        let old_vars_ptr = ecx.read_pointer(environ)?;
+        ecx.deallocate_ptr(old_vars_ptr, None, MiriMemoryKind::Runtime.into())?;
+
+        Ok(())
+    }
+
+    pub(crate) fn environ(&self) -> Pointer<Option<Provenance>> {
+        self.environ.ptr()
+    }
+}
+
+fn alloc_env_var<'mir, 'tcx>(
+    ecx: &mut InterpCx<'mir, 'tcx, MiriMachine<'mir, 'tcx>>,
+    name: &OsStr,
+    value: &OsStr,
+) -> InterpResult<'tcx, Pointer<Option<Provenance>>> {
+    let mut name_osstring = name.to_os_string();
+    name_osstring.push("=");
+    name_osstring.push(value);
+    ecx.alloc_os_str_as_c_str(name_osstring.as_os_str(), MiriMemoryKind::Runtime.into())
+}
+
+/// Allocates an `environ` block with the given list of pointers.
+fn alloc_environ_block<'mir, 'tcx>(
+    ecx: &mut InterpCx<'mir, 'tcx, MiriMachine<'mir, 'tcx>>,
+    mut vars: Vec<Pointer<Option<Provenance>>>,
+) -> InterpResult<'tcx, Pointer<Option<Provenance>>> {
+    // Add trailing null.
+    vars.push(Pointer::null());
+    // Make an array with all these pointers inside Miri.
+    let vars_layout = ecx.layout_of(Ty::new_array(
+        *ecx.tcx,
+        ecx.machine.layouts.mut_raw_ptr.ty,
+        u64::try_from(vars.len()).unwrap(),
+    ))?;
+    let vars_place = ecx.allocate(vars_layout, MiriMemoryKind::Runtime.into())?;
+    for (idx, var) in vars.into_iter().enumerate() {
+        let place = ecx.project_field(&vars_place, idx)?;
+        ecx.write_pointer(var, &place)?;
+    }
+    Ok(vars_place.ptr())
+}
+
+impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
+pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
+    fn getenv(
+        &mut self,
+        name_op: &OpTy<'tcx, Provenance>,
+    ) -> InterpResult<'tcx, Pointer<Option<Provenance>>> {
+        let this = self.eval_context_mut();
+        this.assert_target_os_is_unix("getenv");
+
+        let name_ptr = this.read_pointer(name_op)?;
+        let name = this.read_os_str_from_c_str(name_ptr)?;
+
+        // We don't care about the value as we have the `map` to keep track of everything,
+        // but we do want to do this read so it shows up as a data race.
+        let _vars_ptr = this.read_pointer(&this.machine.env_vars.unix().environ)?;
+        Ok(match this.machine.env_vars.unix().map.get(name) {
+            Some(var_ptr) => {
+                // The offset is used to strip the "{name}=" part of the string.
+                var_ptr.offset(
+                    Size::from_bytes(u64::try_from(name.len()).unwrap().checked_add(1).unwrap()),
+                    this,
+                )?
+            }
+            None => Pointer::null(),
+        })
+    }
+
+    fn setenv(
+        &mut self,
+        name_op: &OpTy<'tcx, Provenance>,
+        value_op: &OpTy<'tcx, Provenance>,
+    ) -> InterpResult<'tcx, i32> {
+        let this = self.eval_context_mut();
+        this.assert_target_os_is_unix("setenv");
+
+        let name_ptr = this.read_pointer(name_op)?;
+        let value_ptr = this.read_pointer(value_op)?;
+
+        let mut new = None;
+        if !this.ptr_is_null(name_ptr)? {
+            let name = this.read_os_str_from_c_str(name_ptr)?;
+            if !name.is_empty() && !name.to_string_lossy().contains('=') {
+                let value = this.read_os_str_from_c_str(value_ptr)?;
+                new = Some((name.to_owned(), value.to_owned()));
+            }
+        }
+        if let Some((name, value)) = new {
+            let var_ptr = alloc_env_var(this, &name, &value)?;
+            if let Some(var) = this.machine.env_vars.unix_mut().map.insert(name, var_ptr) {
+                this.deallocate_ptr(var, None, MiriMemoryKind::Runtime.into())?;
+            }
+            this.update_environ()?;
+            Ok(0) // return zero on success
+        } else {
+            // name argument is a null pointer, points to an empty string, or points to a string containing an '=' character.
+            let einval = this.eval_libc("EINVAL");
+            this.set_last_error(einval)?;
+            Ok(-1)
+        }
+    }
+
+    fn unsetenv(&mut self, name_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, i32> {
+        let this = self.eval_context_mut();
+        this.assert_target_os_is_unix("unsetenv");
+
+        let name_ptr = this.read_pointer(name_op)?;
+        let mut success = None;
+        if !this.ptr_is_null(name_ptr)? {
+            let name = this.read_os_str_from_c_str(name_ptr)?.to_owned();
+            if !name.is_empty() && !name.to_string_lossy().contains('=') {
+                success = Some(this.machine.env_vars.unix_mut().map.remove(&name));
+            }
+        }
+        if let Some(old) = success {
+            if let Some(var) = old {
+                this.deallocate_ptr(var, None, MiriMemoryKind::Runtime.into())?;
+            }
+            this.update_environ()?;
+            Ok(0)
+        } else {
+            // name argument is a null pointer, points to an empty string, or points to a string containing an '=' character.
+            let einval = this.eval_libc("EINVAL");
+            this.set_last_error(einval)?;
+            Ok(-1)
+        }
+    }
+
+    fn getcwd(
+        &mut self,
+        buf_op: &OpTy<'tcx, Provenance>,
+        size_op: &OpTy<'tcx, Provenance>,
+    ) -> InterpResult<'tcx, Pointer<Option<Provenance>>> {
+        let this = self.eval_context_mut();
+        this.assert_target_os_is_unix("getcwd");
+
+        let buf = this.read_pointer(buf_op)?;
+        let size = this.read_target_usize(size_op)?;
+
+        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
+            this.reject_in_isolation("`getcwd`", reject_with)?;
+            this.set_last_error_from_io_error(ErrorKind::PermissionDenied)?;
+            return Ok(Pointer::null());
+        }
+
+        // If we cannot get the current directory, we return null
+        match env::current_dir() {
+            Ok(cwd) => {
+                if this.write_path_to_c_str(&cwd, buf, size)?.0 {
+                    return Ok(buf);
+                }
+                let erange = this.eval_libc("ERANGE");
+                this.set_last_error(erange)?;
+            }
+            Err(e) => this.set_last_error_from_io_error(e.kind())?,
+        }
+
+        Ok(Pointer::null())
+    }
+
+    fn chdir(&mut self, path_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, i32> {
+        let this = self.eval_context_mut();
+        this.assert_target_os_is_unix("chdir");
+
+        let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
+
+        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
+            this.reject_in_isolation("`chdir`", reject_with)?;
+            this.set_last_error_from_io_error(ErrorKind::PermissionDenied)?;
+
+            return Ok(-1);
+        }
+
+        match env::set_current_dir(path) {
+            Ok(()) => Ok(0),
+            Err(e) => {
+                this.set_last_error_from_io_error(e.kind())?;
+                Ok(-1)
+            }
+        }
+    }
+
+    /// Updates the `environ` static.
+    fn update_environ(&mut self) -> InterpResult<'tcx> {
+        let this = self.eval_context_mut();
+        // Deallocate the old environ list.
+        let environ = this.machine.env_vars.unix().environ.clone();
+        let old_vars_ptr = this.read_pointer(&environ)?;
+        this.deallocate_ptr(old_vars_ptr, None, MiriMemoryKind::Runtime.into())?;
+
+        // Write the new list.
+        let vals = this.machine.env_vars.unix().map.values().copied().collect();
+        let environ_block = alloc_environ_block(this, vals)?;
+        this.write_pointer(environ_block, &environ)?;
+
+        Ok(())
+    }
+
+    fn getpid(&mut self) -> InterpResult<'tcx, i32> {
+        let this = self.eval_context_mut();
+        this.assert_target_os_is_unix("getpid");
+
+        this.check_no_isolation("`getpid`")?;
+
+        // The reason we need to do this wacky of a conversion is because
+        // `libc::getpid` returns an i32, however, `std::process::id()` return an u32.
+        // So we un-do the conversion that stdlib does and turn it back into an i32.
+        #[allow(clippy::cast_possible_wrap)]
+        Ok(std::process::id() as i32)
+    }
+}
diff --git a/src/tools/miri/src/shims/unix/macos/foreign_items.rs b/src/tools/miri/src/shims/unix/macos/foreign_items.rs
index 53a02bf5e0b..66a8dce753f 100644
--- a/src/tools/miri/src/shims/unix/macos/foreign_items.rs
+++ b/src/tools/miri/src/shims/unix/macos/foreign_items.rs
@@ -74,15 +74,8 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
             // Environment related shims
             "_NSGetEnviron" => {
                 let [] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
-                this.write_pointer(
-                    this.machine
-                        .env_vars
-                        .environ
-                        .as_ref()
-                        .expect("machine must be initialized")
-                        .ptr(),
-                    dest,
-                )?;
+                let environ = this.machine.env_vars.unix().environ();
+                this.write_pointer(environ, dest)?;
             }
 
             // Time related shims
diff --git a/src/tools/miri/src/shims/unix/mod.rs b/src/tools/miri/src/shims/unix/mod.rs
index 2bc41e1a62d..144593aa2fc 100644
--- a/src/tools/miri/src/shims/unix/mod.rs
+++ b/src/tools/miri/src/shims/unix/mod.rs
@@ -1,5 +1,6 @@
 pub mod foreign_items;
 
+mod env;
 mod fd;
 mod fs;
 mod mem;
@@ -11,9 +12,11 @@ mod freebsd;
 mod linux;
 mod macos;
 
+pub use env::UnixEnvVars;
 pub use fd::{FdTable, FileDescriptor};
 pub use fs::DirTable;
-// All the unix-specific extension traits
+// All the Unix-specific extension traits
+pub use env::EvalContextExt as _;
 pub use fd::EvalContextExt as _;
 pub use fs::EvalContextExt as _;
 pub use mem::EvalContextExt as _;
diff --git a/src/tools/miri/src/shims/windows/env.rs b/src/tools/miri/src/shims/windows/env.rs
new file mode 100644
index 00000000000..e91623ac871
--- /dev/null
+++ b/src/tools/miri/src/shims/windows/env.rs
@@ -0,0 +1,257 @@
+use std::env;
+use std::ffi::OsString;
+use std::io::ErrorKind;
+
+use rustc_data_structures::fx::FxHashMap;
+
+use crate::*;
+use helpers::windows_check_buffer_size;
+
+#[derive(Default)]
+pub struct WindowsEnvVars {
+    /// Stores the environment varialbles.
+    map: FxHashMap<OsString, OsString>,
+}
+
+impl VisitProvenance for WindowsEnvVars {
+    fn visit_provenance(&self, _visit: &mut VisitWith<'_>) {
+        let WindowsEnvVars { map: _ } = self;
+    }
+}
+
+impl WindowsEnvVars {
+    pub(crate) fn new<'mir, 'tcx>(
+        _ecx: &mut InterpCx<'mir, 'tcx, MiriMachine<'mir, 'tcx>>,
+        env_vars: FxHashMap<OsString, OsString>,
+    ) -> InterpResult<'tcx, Self> {
+        Ok(Self { map: env_vars })
+    }
+}
+
+impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
+pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
+    #[allow(non_snake_case)]
+    fn GetEnvironmentVariableW(
+        &mut self,
+        name_op: &OpTy<'tcx, Provenance>, // LPCWSTR
+        buf_op: &OpTy<'tcx, Provenance>,  // LPWSTR
+        size_op: &OpTy<'tcx, Provenance>, // DWORD
+    ) -> InterpResult<'tcx, Scalar<Provenance>> {
+        // ^ Returns DWORD (u32 on Windows)
+
+        let this = self.eval_context_mut();
+        this.assert_target_os("windows", "GetEnvironmentVariableW");
+
+        let name_ptr = this.read_pointer(name_op)?;
+        let buf_ptr = this.read_pointer(buf_op)?;
+        let buf_size = this.read_scalar(size_op)?.to_u32()?; // in characters
+
+        let name = this.read_os_str_from_wide_str(name_ptr)?;
+        Ok(match this.machine.env_vars.windows().map.get(&name).cloned() {
+            Some(val) => {
+                Scalar::from_u32(windows_check_buffer_size(this.write_os_str_to_wide_str(
+                    &val,
+                    buf_ptr,
+                    buf_size.into(),
+                )?))
+                // This can in fact return 0. It is up to the caller to set last_error to 0
+                // beforehand and check it afterwards to exclude that case.
+            }
+            None => {
+                let envvar_not_found = this.eval_windows("c", "ERROR_ENVVAR_NOT_FOUND");
+                this.set_last_error(envvar_not_found)?;
+                Scalar::from_u32(0) // return zero upon failure
+            }
+        })
+    }
+
+    #[allow(non_snake_case)]
+    fn GetEnvironmentStringsW(&mut self) -> InterpResult<'tcx, Pointer<Option<Provenance>>> {
+        let this = self.eval_context_mut();
+        this.assert_target_os("windows", "GetEnvironmentStringsW");
+
+        // Info on layout of environment blocks in Windows:
+        // https://docs.microsoft.com/en-us/windows/win32/procthread/environment-variables
+        let mut env_vars = std::ffi::OsString::new();
+        for (name, value) in this.machine.env_vars.windows().map.iter() {
+            env_vars.push(name);
+            env_vars.push("=");
+            env_vars.push(value);
+            env_vars.push("\0");
+        }
+        // Allocate environment block & Store environment variables to environment block.
+        // Final null terminator(block terminator) is added by `alloc_os_str_to_wide_str`.
+        let envblock_ptr =
+            this.alloc_os_str_as_wide_str(&env_vars, MiriMemoryKind::Runtime.into())?;
+        // If the function succeeds, the return value is a pointer to the environment block of the current process.
+        Ok(envblock_ptr)
+    }
+
+    #[allow(non_snake_case)]
+    fn FreeEnvironmentStringsW(
+        &mut self,
+        env_block_op: &OpTy<'tcx, Provenance>,
+    ) -> InterpResult<'tcx, Scalar<Provenance>> {
+        let this = self.eval_context_mut();
+        this.assert_target_os("windows", "FreeEnvironmentStringsW");
+
+        let env_block_ptr = this.read_pointer(env_block_op)?;
+        this.deallocate_ptr(env_block_ptr, None, MiriMemoryKind::Runtime.into())?;
+        // If the function succeeds, the return value is nonzero.
+        Ok(Scalar::from_i32(1))
+    }
+
+    #[allow(non_snake_case)]
+    fn SetEnvironmentVariableW(
+        &mut self,
+        name_op: &OpTy<'tcx, Provenance>,  // LPCWSTR
+        value_op: &OpTy<'tcx, Provenance>, // LPCWSTR
+    ) -> InterpResult<'tcx, Scalar<Provenance>> {
+        let this = self.eval_context_mut();
+        this.assert_target_os("windows", "SetEnvironmentVariableW");
+
+        let name_ptr = this.read_pointer(name_op)?;
+        let value_ptr = this.read_pointer(value_op)?;
+
+        if this.ptr_is_null(name_ptr)? {
+            // ERROR CODE is not clearly explained in docs.. For now, throw UB instead.
+            throw_ub_format!("pointer to environment variable name is NULL");
+        }
+
+        let name = this.read_os_str_from_wide_str(name_ptr)?;
+        if name.is_empty() {
+            throw_unsup_format!("environment variable name is an empty string");
+        } else if name.to_string_lossy().contains('=') {
+            throw_unsup_format!("environment variable name contains '='");
+        } else if this.ptr_is_null(value_ptr)? {
+            // Delete environment variable `{name}` if it exists.
+            this.machine.env_vars.windows_mut().map.remove(&name);
+            Ok(this.eval_windows("c", "TRUE"))
+        } else {
+            let value = this.read_os_str_from_wide_str(value_ptr)?;
+            this.machine.env_vars.windows_mut().map.insert(name, value);
+            Ok(this.eval_windows("c", "TRUE"))
+        }
+    }
+
+    #[allow(non_snake_case)]
+    fn GetCurrentDirectoryW(
+        &mut self,
+        size_op: &OpTy<'tcx, Provenance>, // DWORD
+        buf_op: &OpTy<'tcx, Provenance>,  // LPTSTR
+    ) -> InterpResult<'tcx, Scalar<Provenance>> {
+        let this = self.eval_context_mut();
+        this.assert_target_os("windows", "GetCurrentDirectoryW");
+
+        let size = u64::from(this.read_scalar(size_op)?.to_u32()?);
+        let buf = this.read_pointer(buf_op)?;
+
+        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
+            this.reject_in_isolation("`GetCurrentDirectoryW`", reject_with)?;
+            this.set_last_error_from_io_error(ErrorKind::PermissionDenied)?;
+            return Ok(Scalar::from_u32(0));
+        }
+
+        // If we cannot get the current directory, we return 0
+        match env::current_dir() {
+            Ok(cwd) => {
+                // This can in fact return 0. It is up to the caller to set last_error to 0
+                // beforehand and check it afterwards to exclude that case.
+                return Ok(Scalar::from_u32(windows_check_buffer_size(
+                    this.write_path_to_wide_str(&cwd, buf, size)?,
+                )));
+            }
+            Err(e) => this.set_last_error_from_io_error(e.kind())?,
+        }
+        Ok(Scalar::from_u32(0))
+    }
+
+    #[allow(non_snake_case)]
+    fn SetCurrentDirectoryW(
+        &mut self,
+        path_op: &OpTy<'tcx, Provenance>, // LPCTSTR
+    ) -> InterpResult<'tcx, Scalar<Provenance>> {
+        // ^ Returns BOOL (i32 on Windows)
+
+        let this = self.eval_context_mut();
+        this.assert_target_os("windows", "SetCurrentDirectoryW");
+
+        let path = this.read_path_from_wide_str(this.read_pointer(path_op)?)?;
+
+        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
+            this.reject_in_isolation("`SetCurrentDirectoryW`", reject_with)?;
+            this.set_last_error_from_io_error(ErrorKind::PermissionDenied)?;
+
+            return Ok(this.eval_windows("c", "FALSE"));
+        }
+
+        match env::set_current_dir(path) {
+            Ok(()) => Ok(this.eval_windows("c", "TRUE")),
+            Err(e) => {
+                this.set_last_error_from_io_error(e.kind())?;
+                Ok(this.eval_windows("c", "FALSE"))
+            }
+        }
+    }
+
+    #[allow(non_snake_case)]
+    fn GetCurrentProcessId(&mut self) -> InterpResult<'tcx, u32> {
+        let this = self.eval_context_mut();
+        this.assert_target_os("windows", "GetCurrentProcessId");
+        this.check_no_isolation("`GetCurrentProcessId`")?;
+
+        Ok(std::process::id())
+    }
+
+    #[allow(non_snake_case)]
+    fn GetUserProfileDirectoryW(
+        &mut self,
+        token: &OpTy<'tcx, Provenance>, // HANDLE
+        buf: &OpTy<'tcx, Provenance>,   // LPWSTR
+        size: &OpTy<'tcx, Provenance>,  // LPDWORD
+    ) -> InterpResult<'tcx, Scalar<Provenance>> // returns BOOL
+    {
+        let this = self.eval_context_mut();
+        this.assert_target_os("windows", "GetUserProfileDirectoryW");
+        this.check_no_isolation("`GetUserProfileDirectoryW`")?;
+
+        let token = this.read_target_isize(token)?;
+        let buf = this.read_pointer(buf)?;
+        let size = this.deref_pointer(size)?;
+
+        if token != -4 {
+            throw_unsup_format!(
+                "GetUserProfileDirectoryW: only CURRENT_PROCESS_TOKEN is supported"
+            );
+        }
+
+        // See <https://learn.microsoft.com/en-us/windows/win32/api/userenv/nf-userenv-getuserprofiledirectoryw> for docs.
+        Ok(match directories::UserDirs::new() {
+            Some(dirs) => {
+                let home = dirs.home_dir();
+                let size_avail = if this.ptr_is_null(size.ptr())? {
+                    0 // if the buf pointer is null, we can't write to it; `size` will be updated to the required length
+                } else {
+                    this.read_scalar(&size)?.to_u32()?
+                };
+                // Of course we cannot use `windows_check_buffer_size` here since this uses
+                // a different method for dealing with a too-small buffer than the other functions...
+                let (success, len) = this.write_path_to_wide_str(home, buf, size_avail.into())?;
+                // The Windows docs just say that this is written on failure. But std
+                // seems to rely on it always being written.
+                this.write_scalar(Scalar::from_u32(len.try_into().unwrap()), &size)?;
+                if success {
+                    Scalar::from_i32(1) // return TRUE
+                } else {
+                    this.set_last_error(this.eval_windows("c", "ERROR_INSUFFICIENT_BUFFER"))?;
+                    Scalar::from_i32(0) // return FALSE
+                }
+            }
+            None => {
+                // We have to pick some error code.
+                this.set_last_error(this.eval_windows("c", "ERROR_BAD_USER_PROFILE"))?;
+                Scalar::from_i32(0) // return FALSE
+            }
+        })
+    }
+}
diff --git a/src/tools/miri/src/shims/windows/foreign_items.rs b/src/tools/miri/src/shims/windows/foreign_items.rs
index 24f7cd18e7a..e8ae80261c6 100644
--- a/src/tools/miri/src/shims/windows/foreign_items.rs
+++ b/src/tools/miri/src/shims/windows/foreign_items.rs
@@ -10,11 +10,10 @@ use rustc_target::spec::abi::Abi;
 
 use crate::shims::alloc::EvalContextExt as _;
 use crate::shims::os_str::bytes_to_os_str;
+use crate::shims::windows::*;
 use crate::*;
 use shims::foreign_items::EmulateForeignItemResult;
-use shims::windows::handle::{EvalContextExt as _, Handle, PseudoHandle};
-use shims::windows::sync::EvalContextExt as _;
-use shims::windows::thread::EvalContextExt as _;
+use shims::windows::handle::{Handle, PseudoHandle};
 
 fn is_dyn_sym(name: &str) -> bool {
     // std does dynamic detection for these symbols
diff --git a/src/tools/miri/src/shims/windows/mod.rs b/src/tools/miri/src/shims/windows/mod.rs
index 7688abe412b..65f682b9dad 100644
--- a/src/tools/miri/src/shims/windows/mod.rs
+++ b/src/tools/miri/src/shims/windows/mod.rs
@@ -1,5 +1,13 @@
 pub mod foreign_items;
 
+mod env;
 mod handle;
 mod sync;
 mod thread;
+
+pub use env::WindowsEnvVars;
+// All the Windows-specific extension traits
+pub use env::EvalContextExt as _;
+pub use handle::EvalContextExt as _;
+pub use sync::EvalContextExt as _;
+pub use thread::EvalContextExt as _;
diff --git a/src/tools/miri/tests/pass/shims/env/var.rs b/src/tools/miri/tests/pass/shims/env/var.rs
index 23a3724ff7f..babaf00578a 100644
--- a/src/tools/miri/tests/pass/shims/env/var.rs
+++ b/src/tools/miri/tests/pass/shims/env/var.rs
@@ -1,4 +1,5 @@
 use std::env;
+use std::thread;
 
 fn main() {
     // Test that miri environment is isolated when communication is disabled.
@@ -23,4 +24,11 @@ fn main() {
     env::remove_var("MIRI_TEST");
     assert_eq!(env::var("MIRI_TEST"), Err(env::VarError::NotPresent));
     println!("{:#?}", env::vars().collect::<Vec<_>>());
+
+    // Do things concurrently, to make sure there's no data race.
+    let t = thread::spawn(|| {
+        env::set_var("MIRI_TEST", "42");
+    });
+    env::set_var("MIRI_TEST", "42");
+    t.join().unwrap();
 }