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.mut_raw_ptr;
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.mut_raw_ptr.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(&mut self, name_op: &OpTy<'tcx>) -> InterpResult<'tcx, Pointer> {
130        let this = self.eval_context_mut();
131        this.assert_target_os_is_unix("getenv");
132
133        let name_ptr = this.read_pointer(name_op)?;
134        let name = this.read_os_str_from_c_str(name_ptr)?;
135
136        let var_ptr = this.machine.env_vars.unix().get_ptr(this, 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 this = self.eval_context_mut();
146        this.assert_target_os_is_unix("setenv");
147
148        let name_ptr = this.read_pointer(name_op)?;
149        let value_ptr = this.read_pointer(value_op)?;
150
151        let mut new = None;
152        if !this.ptr_is_null(name_ptr)? {
153            let name = this.read_os_str_from_c_str(name_ptr)?;
154            if !name.is_empty() && !name.to_string_lossy().contains('=') {
155                let value = this.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(this, &name, &value)?;
161            if let Some(var) = this.machine.env_vars.unix_mut().map.insert(name, var_ptr) {
162                this.deallocate_ptr(var, None, MiriMemoryKind::Machine.into())?;
163            }
164            this.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            this.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 this = self.eval_context_mut();
174        this.assert_target_os_is_unix("unsetenv");
175
176        let name_ptr = this.read_pointer(name_op)?;
177        let mut success = None;
178        if !this.ptr_is_null(name_ptr)? {
179            let name = this.read_os_str_from_c_str(name_ptr)?.to_owned();
180            if !name.is_empty() && !name.to_string_lossy().contains('=') {
181                success = Some(this.machine.env_vars.unix_mut().map.remove(&name));
182            }
183        }
184        if let Some(old) = success {
185            if let Some(var) = old {
186                this.deallocate_ptr(var, None, MiriMemoryKind::Machine.into())?;
187            }
188            this.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            this.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 this = self.eval_context_mut();
198        this.assert_target_os_is_unix("getcwd");
199
200        let buf = this.read_pointer(buf_op)?;
201        let size = this.read_target_usize(size_op)?;
202
203        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
204            this.reject_in_isolation("`getcwd`", reject_with)?;
205            this.set_last_error(ErrorKind::PermissionDenied)?;
206            return interp_ok(Pointer::null());
207        }
208
209        // If we cannot get the current directory, we return null
210        match env::current_dir() {
211            Ok(cwd) => {
212                if this.write_path_to_c_str(&cwd, buf, size)?.0 {
213                    return interp_ok(buf);
214                }
215                this.set_last_error(LibcError("ERANGE"))?;
216            }
217            Err(e) => this.set_last_error(e)?,
218        }
219
220        interp_ok(Pointer::null())
221    }
222
223    fn gethostname(
224        &mut self,
225        name_op: &OpTy<'tcx>,
226        len_op: &OpTy<'tcx>,
227    ) -> InterpResult<'tcx, Scalar> {
228        let this = self.eval_context_mut();
229        this.assert_target_os_is_unix("gethostname");
230
231        let name = this.read_pointer(name_op)?;
232        let len = this.read_target_usize(len_op)?;
233
234        // macOS makes a length of zero UB by writing to `name[namelen - 1]`:
235        // <https://github.com/apple-oss-distributions/Libc/blob/main/gen/FreeBSD/gethostname.c#L48-L60>
236        //
237        // Since POSIX does not clearly specify whether a zero length is valid, require the length
238        // argument to be non-zero on every platform. This cannot be folded into the access check
239        // below: `check_ptr_access` delegates to `check_and_deref_ptr`, which treats every
240        // zero-sized access as valid and returns before checking `name`. In particular, it would
241        // accept a null pointer here.
242        if len == 0 {
243            throw_ub_format!("`gethostname` called with a length of zero");
244        }
245        // The caller promises that `name` points to an array of `len` bytes, so validate that
246        // entire range up front. Since `len` is non-zero, this also rejects null and dangling
247        // pointers.
248        this.check_ptr_access(name, Size::from_bytes(len), CheckInAllocMsg::MemoryAccess)?;
249
250        if len > u64::try_from(HOSTNAME.len()).unwrap() {
251            let (written, _) = this.write_c_str(HOSTNAME, name, len)?;
252            assert!(written); // Value should fit.
253            return interp_ok(Scalar::from_i32(0));
254        }
255
256        // The exact behavior on a too short buffer differs by platform and even libc.
257        // POSIX says:
258        // > The returned name shall be null-terminated, except that if namelen is
259        // > an insufficient length to hold the host name, then the returned name
260        // > shall be truncated and it is unspecified whether the returned name is
261        // > null-terminated.
262        match (&this.tcx.sess.target.os, &this.tcx.sess.target.env) {
263            (Os::Android, _) => {
264                // Android violates POSIX and does not write anything:
265                // <https://raw.githubusercontent.com/aosp-mirror/platform_bionic/master/libc/bionic/gethostname.cpp>
266                this.set_errno_and_return_neg1_i32(LibcError("ENAMETOOLONG"))
267            }
268            (Os::Linux, Env::Gnu) | (Os::FreeBsd, _) => {
269                // Write what we can *without* trailing null.
270                let len: usize = len.try_into().unwrap();
271                this.write_bytes_ptr(name, HOSTNAME[..len].iter().copied())?;
272                this.set_errno_and_return_neg1_i32(LibcError("ENAMETOOLONG"))
273            }
274            (Os::Linux, _) | (Os::MacOs, _) | (Os::Solaris, _) | (Os::Illumos, _) => {
275                // Write what we can *with* trailing null.
276                let len: usize = len.try_into().unwrap();
277                let copy_len = len.strict_sub(1);
278                this.write_bytes_ptr(
279                    name,
280                    HOSTNAME[..copy_len].iter().copied().chain(std::iter::once(0)),
281                )?;
282
283                // These implementations report truncation as successful. POSIX requires
284                // truncation when `namelen` is insufficient, but does not require it
285                // to be reported as an error.
286                // musl: <https://git.musl-libc.org/cgit/musl/tree/src/unistd/gethostname.c>
287                // macOS: <https://github.com/apple-oss-distributions/Libc/blob/main/gen/FreeBSD/gethostname.c>
288                // Illumos: <https://github.com/illumos/illumos-gate/blob/master/usr/src/lib/libc/port/gen/gethostname.c>
289                // Solaris: <https://docs.oracle.com/cd/E88353_01/html/E37843/gethostname-3c.html>
290                interp_ok(Scalar::from_i32(0))
291            }
292            _ => {
293                throw_unsup_format!("`gethostname` is not supported on {}", this.tcx.sess.target.os)
294            }
295        }
296    }
297
298    fn chdir(&mut self, path_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
299        let this = self.eval_context_mut();
300        this.assert_target_os_is_unix("chdir");
301
302        let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
303
304        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
305            this.reject_in_isolation("`chdir`", reject_with)?;
306            return this.set_errno_and_return_neg1_i32(ErrorKind::PermissionDenied);
307        }
308
309        let result = env::set_current_dir(path).map(|()| 0);
310        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
311    }
312
313    /// Updates the `environ` static.
314    fn update_environ(&mut self) -> InterpResult<'tcx> {
315        let this = self.eval_context_mut();
316        // Deallocate the old environ list.
317        let environ = this.machine.env_vars.unix().environ.clone();
318        let old_vars_ptr = this.read_pointer(&environ)?;
319        this.deallocate_ptr(old_vars_ptr, None, MiriMemoryKind::Machine.into())?;
320
321        // Write the new list.
322        let vals = this.machine.env_vars.unix().map.values().copied().collect();
323        let environ_block = alloc_environ_block(this, vals)?;
324        this.write_pointer(environ_block, &environ)?;
325
326        interp_ok(())
327    }
328
329    fn getpid(&mut self) -> InterpResult<'tcx, Scalar> {
330        let this = self.eval_context_mut();
331        this.assert_target_os_is_unix("getpid");
332
333        // The reason we need to do this wacky of a conversion is because
334        // `libc::getpid` returns an i32, however, `std::process::id()` return an u32.
335        // So we un-do the conversion that stdlib does and turn it back into an i32.
336        // In `Scalar` representation, these are the same, so we don't need to anything else.
337        interp_ok(Scalar::from_u32(this.get_pid()))
338    }
339
340    /// The `gettid`-like function for Unix platforms that take no parameters and return a 32-bit
341    /// integer. It is not always named "gettid".
342    fn unix_gettid(&mut self, link_name: &str) -> InterpResult<'tcx, Scalar> {
343        let this = self.eval_context_ref();
344        this.assert_target_os_is_unix(link_name);
345
346        // For most platforms the return type is an `i32`, but some are unsigned. The TID
347        // will always be positive so we don't need to differentiate.
348        interp_ok(Scalar::from_u32(this.get_tid(this.active_thread())))
349    }
350
351    /// `fields_size`, if present, says how large each field of the struct is.
352    fn uname(
353        &mut self,
354        uname: &OpTy<'tcx>,
355        fields_size: Option<&OpTy<'tcx>>,
356    ) -> InterpResult<'tcx, Scalar> {
357        let this = self.eval_context_mut();
358        this.assert_target_os_is_unix("uname");
359
360        let uname_ptr = this.read_pointer(uname)?;
361        let fields_size = match fields_size {
362            None => None,
363            Some(size) => Some(this.read_scalar(size)?.to_i32()?),
364        };
365
366        if this.ptr_is_null(uname_ptr)? {
367            return this.set_errno_and_return_neg1_i32(LibcError("EFAULT"));
368        }
369
370        let uname = this.deref_pointer_as(uname, this.libc_ty_layout("utsname"))?;
371        let arch = this.machine.tcx.sess.target.arch.desc_symbol();
372        // Values required by POSIX.
373        let mut values = vec![
374            ("sysname", "Miri"),
375            ("nodename", "Miri"),
376            ("release", env!("CARGO_PKG_VERSION")),
377            ("version", concat!("Miri ", env!("CARGO_PKG_VERSION"))),
378            ("machine", arch.as_str()),
379        ];
380        if matches!(this.machine.tcx.sess.target.os, Os::Linux | Os::Android) {
381            values.push(("domainname", "(none)"));
382        }
383
384        for (name, value) in values {
385            let field = this.project_field_named(&uname, name)?;
386            let size = field.layout().layout.size().bytes();
387            if fields_size.is_some_and(|fields_size| u64::try_from(fields_size) != Ok(size)) {
388                throw_unsup_format!(
389                    "the fields size passed to `uname` does not match the type in the libc crate"
390                );
391            }
392            let (written, _) = this.write_c_str(value.as_bytes(), field.ptr(), size)?;
393            assert!(written); // All values should fit.
394        }
395        interp_ok(Scalar::from_i32(0))
396    }
397
398    /// The Apple-specific `int pthread_threadid_np(pthread_t thread, uint64_t *thread_id)`, which
399    /// allows querying the ID for arbitrary threads, identified by their pthread_t.
400    ///
401    /// API documentation: <https://www.manpagez.com/man/3/pthread_threadid_np/>.
402    fn apple_pthread_threadid_np(
403        &mut self,
404        thread_op: &OpTy<'tcx>,
405        tid_op: &OpTy<'tcx>,
406    ) -> InterpResult<'tcx, Scalar> {
407        let this = self.eval_context_mut();
408        this.assert_target_os(Os::MacOs, "pthread_threadip_np");
409
410        let tid_dest = this.read_pointer(tid_op)?;
411        if this.ptr_is_null(tid_dest)? {
412            // If NULL is passed, an error is immediately returned
413            return interp_ok(this.eval_libc("EINVAL"));
414        }
415
416        let thread = this.read_scalar(thread_op)?.to_int(this.libc_ty_layout("pthread_t").size)?;
417        let thread = if thread == 0 {
418            // Null thread ID indicates that we are querying the active thread.
419            this.machine.threads.active_thread()
420        } else {
421            // Our pthread_t is just the raw ThreadId.
422            let Ok(thread) = this.thread_id_try_from(thread) else {
423                return interp_ok(this.eval_libc("ESRCH"));
424            };
425            thread
426        };
427
428        // This returns an `int`, not a `pthread_t`, so we treat it like we treat `gettid` on Linux.
429        let tid = this.get_tid(thread);
430        let tid_dest = this.deref_pointer_as(tid_op, this.machine.layouts.u64)?;
431        this.write_int(tid, &tid_dest)?;
432
433        // Possible errors have been handled, return success.
434        interp_ok(Scalar::from_u32(0))
435    }
436}