Skip to main content

nodejs/stdlib/
os.rs

1//! Node `os` module. Values that Node derives from the host (platform, arch,
2//! hostname, home/tmp dirs, endianness, EOL) are returned faithfully; the
3//! machine-specific numeric readings (`cpus`, `totalmem`, `freemem`, `loadavg`,
4//! `uptime`) return best-effort placeholders (not fuzzed — they vary per host on
5//! reference Node too).
6
7use crate::host::with_host;
8use fusevm::Value;
9use indexmap::IndexMap;
10
11pub const METHODS: &[&str] = &[
12    "platform",
13    "arch",
14    "type",
15    "release",
16    "hostname",
17    "homedir",
18    "tmpdir",
19    "endianness",
20    "cpus",
21    "totalmem",
22    "freemem",
23    "uptime",
24    "loadavg",
25    "userInfo",
26    "networkInterfaces",
27    "version",
28    "machine",
29    "availableParallelism",
30    "getPriority",
31    "setPriority",
32];
33
34/// `os.EOL` constant.
35pub fn constant(name: &str) -> Option<Value> {
36    match name {
37        "EOL" => Some(with_host(|h| h.new_str("\n"))),
38        "devNull" => Some(with_host(|h| h.new_str("/dev/null"))),
39        // os.constants.signals (POSIX signal numbers) + priority levels.
40        // Every number comes from `libc`, so it is the one THIS platform uses:
41        // the signal table was a hardcoded list of macOS values, which made
42        // `SIGUSR1` 30 (Linux uses 10) and `SIGSTOP` 17 (Linux uses 19) on
43        // every Linux host. `errno` and `dlopen` were missing entirely.
44        "constants" => {
45            // Each sub-table allocates, so it is built BEFORE the borrow below
46            // rather than inside it.
47            let sig = super::constants::object(&super::constants::signals());
48            let prio = super::constants::object(&super::constants::priority());
49            let errno = super::constants::object(&super::constants::errno());
50            let dlopen = super::constants::object(&super::constants::dlopen());
51            Some(with_host(|h| {
52                let mut m = indexmap::IndexMap::new();
53                m.insert("UV_UDP_REUSEADDR".to_string(), Value::Float(4.0));
54                m.insert("dlopen".to_string(), dlopen);
55                m.insert("errno".to_string(), errno);
56                m.insert("signals".to_string(), sig);
57                m.insert("priority".to_string(), prio);
58                h.new_object(m)
59            }))
60        }
61        _ => None,
62    }
63}
64
65/// Node's `process.platform`/`os.platform()` string for the build target.
66pub fn platform() -> &'static str {
67    match std::env::consts::OS {
68        "macos" => "darwin",
69        "windows" => "win32",
70        other => other,
71    }
72}
73
74/// Node's `os.arch()`/`process.arch` string for the build target.
75pub fn arch() -> &'static str {
76    match std::env::consts::ARCH {
77        "aarch64" => "arm64",
78        "x86_64" => "x64",
79        other => other,
80    }
81}
82
83pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
84    let s = |v: &str| Ok(with_host(|h| h.new_str(v)));
85    Some(match method {
86        "platform" => s(platform()),
87        "arch" => s(arch()),
88        "machine" => s(std::env::consts::ARCH),
89        "type" => s(match std::env::consts::OS {
90            "macos" => "Darwin",
91            "linux" => "Linux",
92            "windows" => "Windows_NT",
93            other => other,
94        }),
95        "release" => s(""),
96        "version" => s(""),
97        "hostname" => s(&hostname()),
98        "homedir" => s(&dirs::home_dir()
99            .map(|p| p.to_string_lossy().into_owned())
100            .unwrap_or_default()),
101        "tmpdir" => s(std::env::temp_dir().to_string_lossy().trim_end_matches('/')),
102        "endianness" => s(if cfg!(target_endian = "big") {
103            "BE"
104        } else {
105            "LE"
106        }),
107        // Physical memory, from `sysconf`. Answering 0 is not a neutral
108        // placeholder: `os.totalmem()` is read to size caches and worker pools,
109        // and zero bytes of RAM is a value no machine reports.
110        "totalmem" => Ok(Value::Float(phys_bytes(libc::_SC_PHYS_PAGES))),
111        "freemem" => Ok(Value::Float(free_bytes())),
112        "uptime" => Ok(Value::Float(uptime_secs())),
113        "cpus" => Ok(cpus()),
114        // Real 1/5/15-minute load averages via `getloadavg(3)`.
115        "loadavg" => {
116            let mut avg = [0f64; 3];
117            // SAFETY: writes at most 3 doubles into a 3-element buffer.
118            let n = unsafe { libc::getloadavg(avg.as_mut_ptr(), 3) };
119            let items: Vec<Value> = if n == 3 {
120                avg.iter().map(|v| Value::Float(*v)).collect()
121            } else {
122                vec![Value::Float(0.0); 3]
123            };
124            Ok(with_host(|h| h.new_array(items)))
125        }
126        "networkInterfaces" => Ok(network_interfaces()),
127        "userInfo" => Ok(user_info()),
128        // Logical CPU count (Node uses libuv's available parallelism).
129        "availableParallelism" => {
130            let n = std::thread::available_parallelism()
131                .map(|n| n.get())
132                .unwrap_or(1);
133            Ok(Value::Float(n as f64))
134        }
135        // `os.getPriority([pid])` — the nice value of `pid` (0 = current process).
136        "getPriority" => {
137            let pid = if args.is_empty() {
138                0
139            } else {
140                super::arg_num(args, 0) as i32
141            };
142            // SAFETY: pure query; PRIO_PROCESS with a pid.
143            let prio = unsafe { libc::getpriority(libc::PRIO_PROCESS as _, pid as _) };
144            Ok(Value::Float(prio as f64))
145        }
146        // `os.setPriority([pid, ]priority)` — best-effort (needs privilege to lower
147        // the nice value); returns undefined.
148        "setPriority" => {
149            let (pid, prio) = if args.len() >= 2 {
150                (
151                    super::arg_num(args, 0) as i32,
152                    super::arg_num(args, 1) as i32,
153                )
154            } else {
155                (0, super::arg_num(args, 0) as i32)
156            };
157            // SAFETY: PRIO_PROCESS with a pid and nice value; failure returns -1.
158            unsafe {
159                libc::setpriority(libc::PRIO_PROCESS as _, pid as _, prio as _);
160            }
161            Ok(Value::Undef)
162        }
163        _ => return None,
164    })
165}
166
167fn hostname() -> String {
168    std::process::Command::new("hostname")
169        .output()
170        .ok()
171        .and_then(|o| String::from_utf8(o.stdout).ok())
172        .map(|s| s.trim().to_string())
173        .unwrap_or_default()
174}
175
176fn user_info() -> Value {
177    with_host(|h| {
178        let mut m = IndexMap::new();
179        let user = std::env::var("USER")
180            .or_else(|_| std::env::var("USERNAME"))
181            .unwrap_or_default();
182        let home = dirs::home_dir()
183            .map(|p| p.to_string_lossy().into_owned())
184            .unwrap_or_default();
185        let shell = std::env::var("SHELL").unwrap_or_default();
186        m.insert("username".into(), h.new_str(user));
187        m.insert("homedir".into(), h.new_str(home));
188        m.insert("shell".into(), h.new_str(shell));
189        m.insert("uid".into(), Value::Float(-1.0));
190        m.insert("gid".into(), Value::Float(-1.0));
191        h.new_object(m)
192    })
193}
194
195/// Bytes of physical memory behind a `sysconf` page count. Both platforms this
196/// targets expose `_SC_PHYS_PAGES`; anything that does not answers 0, which is
197/// what the whole family used to do unconditionally.
198fn phys_bytes(name: libc::c_int) -> f64 {
199    // SAFETY: `sysconf` reads a constant and returns a long; -1 signals absent.
200    let pages = unsafe { libc::sysconf(name) };
201    let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
202    if pages <= 0 || page <= 0 {
203        return 0.0;
204    }
205    pages as f64 * page as f64
206}
207
208/// Free physical memory. Linux has a `sysconf` for it; macOS does not, so the
209/// page counts come from the Mach VM statistics the same way libuv reads them.
210#[cfg(target_os = "linux")]
211fn free_bytes() -> f64 {
212    phys_bytes(libc::_SC_AVPHYS_PAGES)
213}
214
215#[cfg(target_os = "macos")]
216fn free_bytes() -> f64 {
217    // `vm.page_free_count` rather than Mach's `host_statistics64`:
218    // `libc::mach_host_self` is deprecated in favour of a separate crate, and
219    // this reads the same counter through the `sysctl` path already used here.
220    let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
221    if page <= 0 {
222        return 0.0;
223    }
224    // libuv counts the free list PLUS the speculative pages, which the kernel
225    // hands back on demand — free alone reports roughly a third of what node
226    // does on the same machine.
227    let free = sysctl_u32(c"vm.page_free_count").unwrap_or(0) as f64;
228    let spec = sysctl_u32(c"vm.page_speculative_count").unwrap_or(0) as f64;
229    (free + spec) * page as f64
230}
231
232#[cfg(not(any(target_os = "linux", target_os = "macos")))]
233fn free_bytes() -> f64 {
234    0.0
235}
236
237/// Seconds since boot.
238#[cfg(target_os = "linux")]
239fn uptime_secs() -> f64 {
240    std::fs::read_to_string("/proc/uptime")
241        .ok()
242        .and_then(|s| s.split_whitespace().next()?.parse::<f64>().ok())
243        .unwrap_or(0.0)
244}
245
246#[cfg(target_os = "macos")]
247fn uptime_secs() -> f64 {
248    let mut mib = [libc::CTL_KERN, libc::KERN_BOOTTIME];
249    let mut tv: libc::timeval = unsafe { std::mem::zeroed() };
250    let mut len = std::mem::size_of::<libc::timeval>();
251    // SAFETY: `sysctl` writes at most `len` bytes into `tv`.
252    let rc = unsafe {
253        libc::sysctl(
254            mib.as_mut_ptr(),
255            mib.len() as u32,
256            &mut tv as *mut _ as *mut libc::c_void,
257            &mut len,
258            std::ptr::null_mut(),
259            0,
260        )
261    };
262    if rc != 0 || tv.tv_sec == 0 {
263        return 0.0;
264    }
265    let now = std::time::SystemTime::now()
266        .duration_since(std::time::UNIX_EPOCH)
267        .map(|d| d.as_secs_f64())
268        .unwrap_or(0.0);
269    (now - tv.tv_sec as f64).max(0.0).floor()
270}
271
272#[cfg(not(any(target_os = "linux", target_os = "macos")))]
273fn uptime_secs() -> f64 {
274    0.0
275}
276
277/// `os.cpus()` — one entry per logical core, each with the CPU's model string,
278/// its nominal speed in MHz, and the scheduler's time counters.
279///
280/// It answered an EMPTY array, which is the shape `os.cpus().length` is read
281/// for: sizing a worker pool off zero cores, or `|| 1` masking it.
282fn cpus() -> Value {
283    let n = std::thread::available_parallelism()
284        .map(|n| n.get())
285        .unwrap_or(1);
286    let model = cpu_model();
287    let speed = cpu_speed_mhz();
288    with_host(|h| {
289        let items: Vec<Value> = (0..n)
290            .map(|_| {
291                let mut times = IndexMap::new();
292                // The per-core counters need Mach's `host_processor_info` on
293                // macOS and `/proc/stat` on Linux; libuv reads them per core and
294                // this does not, so they are reported as zero rather than
295                // invented. The COUNT, model and speed are real.
296                for k in ["user", "nice", "sys", "idle", "irq"] {
297                    times.insert(k.to_string(), Value::Float(0.0));
298                }
299                let times = h.new_object(times);
300                let mut m = IndexMap::new();
301                m.insert("model".into(), h.new_str(model.clone()));
302                m.insert("speed".into(), Value::Float(speed));
303                m.insert("times".into(), times);
304                h.new_object(m)
305            })
306            .collect();
307        h.new_array(items)
308    })
309}
310
311#[cfg(target_os = "macos")]
312fn cpu_model() -> String {
313    sysctl_string(c"machdep.cpu.brand_string").unwrap_or_else(|| "unknown".into())
314}
315
316#[cfg(target_os = "linux")]
317fn cpu_model() -> String {
318    std::fs::read_to_string("/proc/cpuinfo")
319        .ok()
320        .and_then(|s| {
321            s.lines()
322                .find(|l| l.starts_with("model name") || l.starts_with("Model"))
323                .and_then(|l| l.split_once(':'))
324                .map(|(_, v)| v.trim().to_string())
325        })
326        .unwrap_or_else(|| "unknown".into())
327}
328
329#[cfg(not(any(target_os = "linux", target_os = "macos")))]
330fn cpu_model() -> String {
331    "unknown".into()
332}
333
334#[cfg(target_os = "macos")]
335fn cpu_speed_mhz() -> f64 {
336    // Apple Silicon does not expose `hw.cpufrequency`; libuv reports 0 there
337    // too rather than guessing.
338    sysctl_u64(c"hw.cpufrequency").map_or(0.0, |hz| (hz / 1_000_000) as f64)
339}
340
341#[cfg(target_os = "linux")]
342fn cpu_speed_mhz() -> f64 {
343    std::fs::read_to_string("/proc/cpuinfo")
344        .ok()
345        .and_then(|s| {
346            s.lines()
347                .find(|l| l.starts_with("cpu MHz"))
348                .and_then(|l| l.split_once(':'))
349                .and_then(|(_, v)| v.trim().parse::<f64>().ok())
350        })
351        .map(|f| f.round())
352        .unwrap_or(0.0)
353}
354
355#[cfg(not(any(target_os = "linux", target_os = "macos")))]
356fn cpu_speed_mhz() -> f64 {
357    0.0
358}
359
360#[cfg(target_os = "macos")]
361fn sysctl_string(name: &std::ffi::CStr) -> Option<String> {
362    let mut len: usize = 0;
363    // SAFETY: a null buffer asks only for the length.
364    if unsafe {
365        libc::sysctlbyname(
366            name.as_ptr(),
367            std::ptr::null_mut(),
368            &mut len,
369            std::ptr::null_mut(),
370            0,
371        )
372    } != 0
373        || len == 0
374    {
375        return None;
376    }
377    let mut buf = vec![0u8; len];
378    // SAFETY: writes at most `len` bytes into a buffer of that size.
379    if unsafe {
380        libc::sysctlbyname(
381            name.as_ptr(),
382            buf.as_mut_ptr() as *mut libc::c_void,
383            &mut len,
384            std::ptr::null_mut(),
385            0,
386        )
387    } != 0
388    {
389        return None;
390    }
391    buf.pop();
392    String::from_utf8(buf).ok()
393}
394
395#[cfg(target_os = "macos")]
396fn sysctl_u32(name: &std::ffi::CStr) -> Option<u32> {
397    let mut out: u32 = 0;
398    let mut len = std::mem::size_of::<u32>();
399    // SAFETY: writes at most 4 bytes into `out`.
400    let rc = unsafe {
401        libc::sysctlbyname(
402            name.as_ptr(),
403            &mut out as *mut _ as *mut libc::c_void,
404            &mut len,
405            std::ptr::null_mut(),
406            0,
407        )
408    };
409    (rc == 0).then_some(out)
410}
411
412#[cfg(target_os = "macos")]
413fn sysctl_u64(name: &std::ffi::CStr) -> Option<u64> {
414    let mut out: u64 = 0;
415    let mut len = std::mem::size_of::<u64>();
416    // SAFETY: writes at most 8 bytes into `out`.
417    let rc = unsafe {
418        libc::sysctlbyname(
419            name.as_ptr(),
420            &mut out as *mut _ as *mut libc::c_void,
421            &mut len,
422            std::ptr::null_mut(),
423            0,
424        )
425    };
426    (rc == 0).then_some(out)
427}
428
429/// `os.networkInterfaces()` — the addresses `getifaddrs(3)` reports, grouped by
430/// interface name, in node's shape. It answered an EMPTY object, which reads as
431/// a machine with no network at all.
432///
433/// Only IPv4 and IPv6 entries become addresses; a link-layer entry supplies the
434/// interface's MAC, which node attaches to every address of that interface.
435fn network_interfaces() -> Value {
436    let mut head: *mut libc::ifaddrs = std::ptr::null_mut();
437    // SAFETY: `getifaddrs` allocates the list; `freeifaddrs` releases it below.
438    if unsafe { libc::getifaddrs(&mut head) } != 0 {
439        return with_host(|h| h.new_object(IndexMap::new()));
440    }
441    let mut macs: std::collections::HashMap<String, String> = std::collections::HashMap::new();
442    let mut addrs: Vec<(String, IfAddr)> = Vec::new();
443    let mut cur = head;
444    while !cur.is_null() {
445        // SAFETY: the list is well-formed until `freeifaddrs`.
446        let ifa = unsafe { &*cur };
447        cur = ifa.ifa_next;
448        if ifa.ifa_name.is_null() {
449            continue;
450        }
451        // SAFETY: `ifa_name` is a NUL-terminated interface name.
452        let name = unsafe { std::ffi::CStr::from_ptr(ifa.ifa_name) }
453            .to_string_lossy()
454            .into_owned();
455        if let Some(mac) = link_mac(ifa) {
456            macs.insert(name.clone(), mac);
457            continue;
458        }
459        if let Some(a) = ip_addr(ifa) {
460            addrs.push((name, a));
461        }
462    }
463    // SAFETY: `head` came from `getifaddrs` and is freed exactly once.
464    unsafe { libc::freeifaddrs(head) };
465    with_host(|h| {
466        let mut grouped: IndexMap<String, Vec<Value>> = IndexMap::new();
467        for (name, a) in addrs {
468            let mac = macs
469                .get(&name)
470                .cloned()
471                .unwrap_or_else(|| "00:00:00:00:00:00".into());
472            let mut m = IndexMap::new();
473            m.insert("address".into(), h.new_str(a.address.clone()));
474            m.insert("netmask".into(), h.new_str(a.netmask.clone()));
475            m.insert("family".into(), h.new_str(a.family.to_string()));
476            m.insert("mac".into(), h.new_str(mac));
477            m.insert("internal".into(), Value::Bool(a.internal));
478            m.insert(
479                "cidr".into(),
480                h.new_str(format!("{}/{}", a.address, a.prefix)),
481            );
482            grouped.entry(name).or_default().push(h.new_object(m));
483        }
484        let mut out = IndexMap::new();
485        for (name, list) in grouped {
486            let arr = h.new_array(list);
487            out.insert(name, arr);
488        }
489        h.new_object(out)
490    })
491}
492
493struct IfAddr {
494    address: String,
495    netmask: String,
496    family: &'static str,
497    internal: bool,
498    prefix: u32,
499}
500
501/// The MAC of a link-layer entry, or `None` for an address entry.
502#[cfg(target_os = "macos")]
503fn link_mac(ifa: &libc::ifaddrs) -> Option<String> {
504    if ifa.ifa_addr.is_null() {
505        return None;
506    }
507    // SAFETY: `sa_family` is the first field of every `sockaddr`.
508    if unsafe { (*ifa.ifa_addr).sa_family } as i32 != libc::AF_LINK {
509        return None;
510    }
511    // SAFETY: an `AF_LINK` address is a `sockaddr_dl`.
512    let dl = unsafe { &*(ifa.ifa_addr as *const libc::sockaddr_dl) };
513    let start = dl.sdl_nlen as usize;
514    let len = dl.sdl_alen as usize;
515    if len != 6 || start + len > dl.sdl_data.len() {
516        return None;
517    }
518    let b: Vec<String> = dl.sdl_data[start..start + len]
519        .iter()
520        .map(|c| format!("{:02x}", *c as u8))
521        .collect();
522    Some(b.join(":"))
523}
524
525#[cfg(target_os = "linux")]
526fn link_mac(ifa: &libc::ifaddrs) -> Option<String> {
527    if ifa.ifa_addr.is_null() {
528        return None;
529    }
530    // SAFETY: `sa_family` is the first field of every `sockaddr`.
531    if unsafe { (*ifa.ifa_addr).sa_family } as i32 != libc::AF_PACKET {
532        return None;
533    }
534    // SAFETY: an `AF_PACKET` address is a `sockaddr_ll`.
535    let ll = unsafe { &*(ifa.ifa_addr as *const libc::sockaddr_ll) };
536    let len = ll.sll_halen as usize;
537    if len != 6 {
538        return None;
539    }
540    let b: Vec<String> = ll.sll_addr[..len]
541        .iter()
542        .map(|c| format!("{c:02x}"))
543        .collect();
544    Some(b.join(":"))
545}
546
547#[cfg(not(any(target_os = "linux", target_os = "macos")))]
548fn link_mac(_ifa: &libc::ifaddrs) -> Option<String> {
549    None
550}
551
552/// The IPv4/IPv6 address of an entry, with its netmask and prefix length.
553fn ip_addr(ifa: &libc::ifaddrs) -> Option<IfAddr> {
554    if ifa.ifa_addr.is_null() {
555        return None;
556    }
557    // SAFETY: `sa_family` is the first field of every `sockaddr`.
558    let fam = unsafe { (*ifa.ifa_addr).sa_family } as i32;
559    let internal = ifa.ifa_flags & libc::IFF_LOOPBACK as u32 != 0;
560    if fam == libc::AF_INET {
561        // SAFETY: an `AF_INET` address is a `sockaddr_in`.
562        let sin = unsafe { &*(ifa.ifa_addr as *const libc::sockaddr_in) };
563        let ip = std::net::Ipv4Addr::from(u32::from_be(sin.sin_addr.s_addr));
564        let mask = if ifa.ifa_netmask.is_null() {
565            std::net::Ipv4Addr::UNSPECIFIED
566        } else {
567            // SAFETY: the netmask of an `AF_INET` entry is a `sockaddr_in`.
568            let m = unsafe { &*(ifa.ifa_netmask as *const libc::sockaddr_in) };
569            std::net::Ipv4Addr::from(u32::from_be(m.sin_addr.s_addr))
570        };
571        return Some(IfAddr {
572            address: ip.to_string(),
573            netmask: mask.to_string(),
574            family: "IPv4",
575            internal,
576            prefix: u32::from(mask).count_ones(),
577        });
578    }
579    if fam == libc::AF_INET6 {
580        // SAFETY: an `AF_INET6` address is a `sockaddr_in6`.
581        let sin = unsafe { &*(ifa.ifa_addr as *const libc::sockaddr_in6) };
582        let ip = std::net::Ipv6Addr::from(sin.sin6_addr.s6_addr);
583        let mask = if ifa.ifa_netmask.is_null() {
584            std::net::Ipv6Addr::UNSPECIFIED
585        } else {
586            // SAFETY: the netmask of an `AF_INET6` entry is a `sockaddr_in6`.
587            let m = unsafe { &*(ifa.ifa_netmask as *const libc::sockaddr_in6) };
588            std::net::Ipv6Addr::from(m.sin6_addr.s6_addr)
589        };
590        let prefix: u32 = mask.octets().iter().map(|b| b.count_ones()).sum();
591        return Some(IfAddr {
592            address: ip.to_string(),
593            netmask: mask.to_string(),
594            family: "IPv6",
595            internal,
596            prefix,
597        });
598    }
599    None
600}