Skip to main content

miri/shims/unix/
env.rs

1use std::env;
2use std::ffi::{OsStr, OsString};
3use std::io::ErrorKind;
4
5use rustc_abi::{FieldIdx, Size};
6use rustc_data_structures::fx::FxHashMap;
7use rustc_index::IndexVec;
8use rustc_middle::ty::Ty;
9use rustc_target::spec::{Env, Os};
10
11use super::HOSTNAME;
12use crate::*;
13
14pub struct UnixEnvVars<'tcx> {
15    /// Stores pointers to the environment variables. These variables must be stored as
16    /// null-terminated target strings (c_str or wide_str) with the `"{name}={value}"` format.
17    map: FxHashMap<OsString, Pointer>,
18
19    /// Place where the `environ` static is stored. Lazily initialized, but then never changes.
20    environ: MPlaceTy<'tcx>,
21}
22
23impl VisitProvenance for UnixEnvVars<'_> {
24    fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
25        let UnixEnvVars { map, environ } = self;
26
27        environ.visit_provenance(visit);
28        for ptr in map.values() {
29            ptr.visit_provenance(visit);
30        }
31    }
32}
33
34impl<'tcx> UnixEnvVars<'tcx> {
35    pub(crate) fn new(
36        ecx: &mut InterpCx<'tcx, MiriMachine<'tcx>>,
37        env_vars: FxHashMap<OsString, OsString>,
38    ) -> InterpResult<'tcx, Self> {
39        // Allocate memory for all these env vars.
40        let mut env_vars_machine = FxHashMap::default();
41        for (name, val) in env_vars.into_iter() {
42            let ptr = alloc_env_var(ecx, &name, &val)?;
43            env_vars_machine.insert(name, ptr);
44        }
45
46        // This is memory backing an extern static, hence `ExternStatic`, not `Env`.
47        let layout = ecx.machine.layouts.unit_ptr_mut;
48        let environ = ecx.allocate(layout, MiriMemoryKind::ExternStatic.into())?;
49        let environ_block = alloc_environ_block(ecx, env_vars_machine.values().copied().collect())?;
50        ecx.write_pointer(environ_block, &environ)?;
51
52        interp_ok(UnixEnvVars { map: env_vars_machine, environ })
53    }
54
55    pub(crate) fn environ(&self) -> Pointer {
56        self.environ.ptr()
57    }
58
59    fn get_ptr(
60        &self,
61        ecx: &InterpCx<'tcx, MiriMachine<'tcx>>,
62        name: &OsStr,
63    ) -> InterpResult<'tcx, Option<Pointer>> {
64        // We don't care about the value as we have the `map` to keep track of everything,
65        // but we do want to do this read so it shows up as a data race.
66        let _vars_ptr = ecx.read_pointer(&self.environ)?;
67        let Some(var_ptr) = self.map.get(name) else {
68            return interp_ok(None);
69        };
70        // The offset is used to strip the "{name}=" part of the string.
71        let var_ptr = var_ptr.wrapping_offset(
72            Size::from_bytes(u64::try_from(name.len()).unwrap().strict_add(1)),
73            ecx,
74        );
75        interp_ok(Some(var_ptr))
76    }
77
78    /// Implementation detail for [`InterpCx::get_env_var`]. This basically does `getenv`, complete
79    /// with the reads of the environment, but returns an [`OsString`] instead of a pointer.
80    pub(crate) fn get(
81        &self,
82        ecx: &InterpCx<'tcx, MiriMachine<'tcx>>,
83        name: &OsStr,
84    ) -> InterpResult<'tcx, Option<OsString>> {
85        let var_ptr = self.get_ptr(ecx, name)?;
86        if let Some(ptr) = var_ptr {
87            let var = ecx.read_os_str_from_c_str(ptr)?;
88            interp_ok(Some(var.to_owned()))
89        } else {
90            interp_ok(None)
91        }
92    }
93}
94
95fn alloc_env_var<'tcx>(
96    ecx: &mut InterpCx<'tcx, MiriMachine<'tcx>>,
97    name: &OsStr,
98    value: &OsStr,
99) -> InterpResult<'tcx, Pointer> {
100    let mut name_osstring = name.to_os_string();
101    name_osstring.push("=");
102    name_osstring.push(value);
103    ecx.alloc_os_str_as_c_str(name_osstring.as_os_str(), MiriMemoryKind::Machine.into())
104}
105
106/// Allocates an `environ` block with the given list of pointers.
107fn alloc_environ_block<'tcx>(
108    ecx: &mut InterpCx<'tcx, MiriMachine<'tcx>>,
109    mut vars: IndexVec<FieldIdx, Pointer>,
110) -> InterpResult<'tcx, Pointer> {
111    // Add trailing null.
112    vars.push(Pointer::null());
113    // Make an array with all these pointers inside Miri.
114    let vars_layout = ecx.layout_of(Ty::new_array(
115        *ecx.tcx,
116        ecx.machine.layouts.unit_ptr_mut.ty,
117        u64::try_from(vars.len()).unwrap(),
118    ))?;
119    let vars_place = ecx.allocate(vars_layout, MiriMemoryKind::Machine.into())?;
120    for (idx, var) in vars.into_iter_enumerated() {
121        let place = ecx.project_field(&vars_place, idx)?;
122        ecx.write_pointer(var, &place)?;
123    }
124    interp_ok(vars_place.ptr())
125}
126
127impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
128pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
129    fn getenv(&self, name_op: &OpTy<'tcx>) -> InterpResult<'tcx, Pointer> {
130        let ecx = self.eval_context_ref();
131        ecx.assert_target_os_is_unix("getenv");
132
133        let name_ptr = ecx.read_pointer(name_op)?;
134        let name = ecx.read_os_str_from_c_str(name_ptr)?;
135
136        let var_ptr = ecx.machine.env_vars.unix().get_ptr(ecx, name)?;
137        interp_ok(var_ptr.unwrap_or_else(Pointer::null))
138    }
139
140    fn setenv(
141        &mut self,
142        name_op: &OpTy<'tcx>,
143        value_op: &OpTy<'tcx>,
144    ) -> InterpResult<'tcx, Scalar> {
145        let ecx = self.eval_context_mut();
146        ecx.assert_target_os_is_unix("setenv");
147
148        let name_ptr = ecx.read_pointer(name_op)?;
149        let value_ptr = ecx.read_pointer(value_op)?;
150
151        let mut new = None;
152        if !ecx.ptr_is_null(name_ptr)? {
153            let name = ecx.read_os_str_from_c_str(name_ptr)?;
154            if !name.is_empty() && !name.to_string_lossy().contains('=') {
155                let value = ecx.read_os_str_from_c_str(value_ptr)?;
156                new = Some((name.to_owned(), value.to_owned()));
157            }
158        }
159        if let Some((name, value)) = new {
160            let var_ptr = alloc_env_var(ecx, &name, &value)?;
161            if let Some(var) = ecx.machine.env_vars.unix_mut().map.insert(name, var_ptr) {
162                ecx.deallocate_ptr(var, None, MiriMemoryKind::Machine.into())?;
163            }
164            ecx.update_environ()?;
165            interp_ok(Scalar::from_i32(0)) // return zero on success
166        } else {
167            // name argument is a null pointer, points to an empty string, or points to a string containing an '=' character.
168            ecx.set_errno_and_return_neg1_i32(LibcError("EINVAL"))
169        }
170    }
171
172    fn unsetenv(&mut self, name_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
173        let ecx = self.eval_context_mut();
174        ecx.assert_target_os_is_unix("unsetenv");
175
176        let name_ptr = ecx.read_pointer(name_op)?;
177        let mut success = None;
178        if !ecx.ptr_is_null(name_ptr)? {
179            let name = ecx.read_os_str_from_c_str(name_ptr)?.to_owned();
180            if !name.is_empty() && !name.to_string_lossy().contains('=') {
181                success = Some(ecx.machine.env_vars.unix_mut().map.remove(&name));
182            }
183        }
184        if let Some(old) = success {
185            if let Some(var) = old {
186                ecx.deallocate_ptr(var, None, MiriMemoryKind::Machine.into())?;
187            }
188            ecx.update_environ()?;
189            interp_ok(Scalar::from_i32(0))
190        } else {
191            // name argument is a null pointer, points to an empty string, or points to a string containing an '=' character.
192            ecx.set_errno_and_return_neg1_i32(LibcError("EINVAL"))
193        }
194    }
195
196    fn getcwd(&mut self, buf_op: &OpTy<'tcx>, size_op: &OpTy<'tcx>) -> InterpResult<'tcx, Pointer> {
197        let ecx = self.eval_context_mut();
198        ecx.assert_target_os_is_unix("getcwd");
199
200        let buf = ecx.read_pointer(buf_op)?;
201        let size = ecx.read_target_usize(size_op)?;
202
203        if ecx.ptr_is_null(buf)? {
204            // glibc has special behavior here so avoid confusing UB reports.
205            throw_unsup_format!("getcwd with a null pointer for `buf` is not supported");
206        }
207
208        if size == 0 {
209            ecx.set_last_error(LibcError("EINVAL"))?;
210            return interp_ok(Pointer::null());
211        }
212
213        if let IsolatedOp::Reject(reject_with) = ecx.machine.isolated_op {
214            ecx.reject_in_isolation("`getcwd`", reject_with)?;
215            ecx.set_last_error(ErrorKind::PermissionDenied)?;
216            return interp_ok(Pointer::null());
217        }
218
219        // If we cannot get the current directory, we return null
220        match env::current_dir() {
221            Ok(cwd) => {
222                if ecx.write_path_to_c_str(&cwd, buf, size)?.0 {
223                    return interp_ok(buf);
224                }
225                ecx.set_last_error(LibcError("ERANGE"))?;
226            }
227            Err(e) => ecx.set_last_error(e)?,
228        }
229
230        interp_ok(Pointer::null())
231    }
232
233    fn gethostname(
234        &mut self,
235        name_op: &OpTy<'tcx>,
236        len_op: &OpTy<'tcx>,
237    ) -> InterpResult<'tcx, Scalar> {
238        let ecx = self.eval_context_mut();
239        ecx.assert_target_os_is_unix("gethostname");
240
241        let name = ecx.read_pointer(name_op)?;
242        let len = ecx.read_target_usize(len_op)?;
243
244        // macOS makes a length of zero UB by writing to `name[namelen - 1]`:
245        // <https://github.com/apple-oss-distributions/Libc/blob/main/gen/FreeBSD/gethostname.c#L48-L60>
246        //
247        // Since POSIX does not clearly specify whether a zero length is valid, require the length
248        // argument to be non-zero on every platform. This cannot be folded into the access check
249        // below: `check_ptr_access` delegates to `check_and_deref_ptr`, which treats every
250        // zero-sized access as valid and returns before checking `name`. In particular, it would
251        // accept a null pointer here.
252        if len == 0 {
253            throw_ub_format!("`gethostname` called with a length of zero");
254        }
255        // The caller promises that `name` points to an array of `len` bytes, so validate that
256        // entire range up front. Since `len` is non-zero, this also rejects null and dangling
257        // pointers.
258        ecx.check_ptr_access(name, Size::from_bytes(len), CheckInAllocMsg::MemoryAccess)?;
259
260        if len > u64::try_from(HOSTNAME.len()).unwrap() {
261            let (written, _) = ecx.write_c_str(HOSTNAME, name, len)?;
262            assert!(written); // Value should fit.
263            return interp_ok(Scalar::from_i32(0));
264        }
265
266        // The exact behavior on a too short buffer differs by platform and even libc.
267        // POSIX says:
268        // > The returned name shall be null-terminated, except that if namelen is
269        // > an insufficient length to hold the host name, then the returned name
270        // > shall be truncated and it is unspecified whether the returned name is
271        // > null-terminated.
272        match (&ecx.tcx.sess.target.os, &ecx.tcx.sess.target.env) {
273            (Os::Android, _) => {
274                // Android violates POSIX and does not write anything:
275                // <https://raw.githubusercontent.com/aosp-mirror/platform_bionic/master/libc/bionic/gethostname.cpp>
276                ecx.set_errno_and_return_neg1_i32(LibcError("ENAMETOOLONG"))
277            }
278            (Os::Linux, Env::Gnu) | (Os::FreeBsd, _) => {
279                // Write what we can *without* trailing null.
280                let len: usize = len.try_into().unwrap();
281                ecx.write_bytes_ptr(name, HOSTNAME[..len].iter().copied())?;
282                ecx.set_errno_and_return_neg1_i32(LibcError("ENAMETOOLONG"))
283            }
284            (Os::Linux, _) | (Os::MacOs, _) | (Os::Solaris, _) | (Os::Illumos, _) => {
285                // Write what we can *with* trailing null.
286                let len: usize = len.try_into().unwrap();
287                let copy_len = len.strict_sub(1);
288                ecx.write_bytes_ptr(
289                    name,
290                    HOSTNAME[..copy_len].iter().copied().chain(std::iter::once(0)),
291                )?;
292
293                // These implementations report truncation as successful. POSIX requires
294                // truncation when `namelen` is insufficient, but does not require it
295                // to be reported as an error.
296                // musl: <https://git.musl-libc.org/cgit/musl/tree/src/unistd/gethostname.c>
297                // macOS: <https://github.com/apple-oss-distributions/Libc/blob/main/gen/FreeBSD/gethostname.c>
298                // Illumos: <https://github.com/illumos/illumos-gate/blob/master/usr/src/lib/libc/port/gen/gethostname.c>
299                // Solaris: <https://docs.oracle.com/cd/E88353_01/html/E37843/gethostname-3c.html>
300                interp_ok(Scalar::from_i32(0))
301            }
302            _ => {
303                throw_unsup_format!("`gethostname` is not supported on {}", ecx.tcx.sess.target.os)
304            }
305        }
306    }
307
308    fn chdir(&mut self, path_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
309        let ecx = self.eval_context_mut();
310        ecx.assert_target_os_is_unix("chdir");
311
312        let path = ecx.read_path_from_c_str(ecx.read_pointer(path_op)?)?;
313
314        if let IsolatedOp::Reject(reject_with) = ecx.machine.isolated_op {
315            ecx.reject_in_isolation("`chdir`", reject_with)?;
316            return ecx.set_errno_and_return_neg1_i32(ErrorKind::PermissionDenied);
317        }
318
319        let result = env::set_current_dir(path).map(|()| 0);
320        interp_ok(Scalar::from_i32(ecx.try_unwrap_io_result(result)?))
321    }
322
323    /// Updates the `environ` static.
324    fn update_environ(&mut self) -> InterpResult<'tcx> {
325        let ecx = self.eval_context_mut();
326        // Deallocate the old environ list.
327        let environ = ecx.machine.env_vars.unix().environ.clone();
328        let old_vars_ptr = ecx.read_pointer(&environ)?;
329        ecx.deallocate_ptr(old_vars_ptr, None, MiriMemoryKind::Machine.into())?;
330
331        // Write the new list.
332        let vals = ecx.machine.env_vars.unix().map.values().copied().collect();
333        let environ_block = alloc_environ_block(ecx, vals)?;
334        ecx.write_pointer(environ_block, &environ)?;
335
336        interp_ok(())
337    }
338
339    fn getpid(&self) -> InterpResult<'tcx, Scalar> {
340        let ecx = self.eval_context_ref();
341        ecx.assert_target_os_is_unix("getpid");
342
343        // The reason we need to do this wacky of a conversion is because
344        // `libc::getpid` returns an i32, however, `std::process::id()` return an u32.
345        // So we un-do the conversion that stdlib does and turn it back into an i32.
346        // In `Scalar` representation, these are the same, so we don't need to anything else.
347        interp_ok(Scalar::from_u32(ecx.get_pid()))
348    }
349
350    /// The `gettid`-like function for Unix platforms that take no parameters and return a 32-bit
351    /// integer. It is not always named "gettid".
352    fn unix_gettid(&mut self, link_name: &str) -> InterpResult<'tcx, Scalar> {
353        let ecx = self.eval_context_ref();
354        ecx.assert_target_os_is_unix(link_name);
355
356        // For most platforms the return type is an `i32`, but some are unsigned. The TID
357        // will always be positive so we don't need to differentiate.
358        interp_ok(Scalar::from_u32(ecx.get_tid(ecx.active_thread())))
359    }
360
361    /// `fields_size`, if present, says how large each field of the struct is.
362    fn uname(
363        &mut self,
364        uname: &OpTy<'tcx>,
365        fields_size: Option<&OpTy<'tcx>>,
366    ) -> InterpResult<'tcx, Scalar> {
367        let ecx = self.eval_context_mut();
368        ecx.assert_target_os_is_unix("uname");
369
370        let uname_ptr = ecx.read_pointer(uname)?;
371        let fields_size = match fields_size {
372            None => None,
373            Some(size) => Some(ecx.read_scalar(size)?.to_i32()?),
374        };
375
376        if ecx.ptr_is_null(uname_ptr)? {
377            return ecx.set_errno_and_return_neg1_i32(LibcError("EFAULT"));
378        }
379
380        let uname = ecx.deref_pointer(uname)?;
381        let arch = ecx.machine.tcx.sess.target.arch.desc_symbol();
382        // Values required by POSIX.
383        let mut values = vec![
384            ("sysname", "Miri"),
385            ("nodename", "Miri"),
386            ("release", env!("CARGO_PKG_VERSION")),
387            ("version", concat!("Miri ", env!("CARGO_PKG_VERSION"))),
388            ("machine", arch.as_str()),
389        ];
390        if matches!(ecx.machine.tcx.sess.target.os, Os::Linux | Os::Android) {
391            values.push(("domainname", "(none)"));
392        }
393
394        for (name, value) in values {
395            let field = ecx.project_field_named(&uname, name)?;
396            let size = field.layout().layout.size().bytes();
397            if fields_size.is_some_and(|fields_size| u64::try_from(fields_size) != Ok(size)) {
398                throw_unsup_format!(
399                    "the fields size passed to `uname` does not match the type in the libc crate"
400                );
401            }
402            let (written, _) = ecx.write_c_str(value.as_bytes(), field.ptr(), size)?;
403            assert!(written); // All values should fit.
404        }
405        interp_ok(Scalar::from_i32(0))
406    }
407
408    /// The Apple-specific `int pthread_threadid_np(pthread_t thread, uint64_t *thread_id)`, which
409    /// allows querying the ID for arbitrary threads, identified by their pthread_t.
410    ///
411    /// API documentation: <https://www.manpagez.com/man/3/pthread_threadid_np/>.
412    fn apple_pthread_threadid_np(
413        &mut self,
414        thread_op: &OpTy<'tcx>,
415        tid_op: &OpTy<'tcx>,
416    ) -> InterpResult<'tcx, Scalar> {
417        let ecx = self.eval_context_mut();
418        ecx.assert_target_os(Os::MacOs, "pthread_threadip_np");
419
420        let tid_dest = ecx.read_pointer(tid_op)?;
421        if ecx.ptr_is_null(tid_dest)? {
422            // If NULL is passed, an error is immediately returned
423            return interp_ok(ecx.eval_libc("EINVAL"));
424        }
425
426        let thread = ecx.read_scalar(thread_op)?.to_int(ecx.libc_ty_layout("pthread_t").size)?;
427        let thread = if thread == 0 {
428            // Null thread ID indicates that we are querying the active thread.
429            ecx.machine.threads.active_thread()
430        } else {
431            // Our pthread_t is just the raw ThreadId.
432            // macOS documents this to return an error and indeed it does not seem to segfault.
433            let Ok(thread) = ecx.thread_id_try_from(thread) else {
434                return interp_ok(ecx.eval_libc("ESRCH"));
435            };
436            thread
437        };
438
439        // This returns an `int`, not a `pthread_t`, so we treat it like we treat `gettid` on Linux.
440        let tid = ecx.get_tid(thread);
441        let tid_dest = ecx.deref_pointer(tid_op)?;
442        ecx.write_int(tid, &tid_dest)?;
443
444        // Possible errors have been handled, return success.
445        interp_ok(Scalar::from_u32(0))
446    }
447}