shadow_rs/
shadow.rs

1//! The main entrypoint to Shadow.
2//!
3//! This is called from a small C wrapper for build complexity reasons.
4
5use std::borrow::Borrow;
6use std::ffi::{CStr, OsStr};
7use std::fmt::Write;
8use std::os::unix::ffi::OsStrExt;
9use std::thread;
10
11use anyhow::Context;
12use clap::Parser;
13use nix::sys::{personality, resource, signal};
14use signal_hook::{consts, iterator::Signals};
15
16use crate::core::configuration::{CliOptions, ConfigFileOptions, ConfigOptions};
17use crate::core::controller::Controller;
18use crate::core::logger::shadow_logger;
19use crate::core::sim_config::SimConfig;
20use crate::core::worker;
21use crate::cshadow as c;
22use crate::utility::shm_cleanup;
23
24use shadow_build_info::{BUILD_TIMESTAMP, GIT_BRANCH, GIT_COMMIT_INFO, GIT_DATE};
25
26const HELP_INFO_STR: &str =
27    "For more information, visit https://shadow.github.io or https://github.com/shadow";
28
29/// Main entry point for the simulator.
30pub fn run_shadow(args: Vec<&OsStr>) -> anyhow::Result<()> {
31    // Install the shared memory allocator's clean up routine on exit. Once this guard is dropped,
32    // all shared memory allocations will become invalid.
33    let _guard = unsafe { crate::shadow_shmem::allocator::SharedMemAllocatorDropGuard::new() };
34
35    verify_glib_version().context("Unsupported GLib version")?;
36
37    let mut signals_list = Signals::new([consts::signal::SIGINT, consts::signal::SIGTERM])?;
38    thread::spawn(move || {
39        // `next()` should block until we've received a signal, or `signals_list` is closed and
40        // `None` is returned
41        if let Some(signal) = signals_list.forever().next() {
42            log::info!("Received signal {}. Flushing log and exiting", signal);
43            log::logger().flush();
44            std::process::exit(1);
45        }
46        log::debug!("Finished waiting for a signal");
47    });
48
49    // unblock all signals in shadow and child processes since cmake's ctest blocks
50    // SIGTERM (and maybe others)
51    signal::sigprocmask(
52        signal::SigmaskHow::SIG_SETMASK,
53        Some(&signal::SigSet::empty()),
54        None,
55    )?;
56
57    // parse the options from the command line
58    let options = match CliOptions::try_parse_from(args.clone()) {
59        Ok(x) => x,
60        Err(e) => {
61            // will print to either stdout or stderr with formatting
62            e.print().unwrap();
63            if e.use_stderr() {
64                // the `clap::Error` represents an error (ex: invalid flag)
65                std::process::exit(1);
66            } else {
67                // the `clap::Error` represents a non-error, but we'll want to exit anyways (ex:
68                // '--help')
69                std::process::exit(0);
70            }
71        }
72    };
73
74    if options.show_build_info {
75        write_build_info(std::io::stderr()).unwrap();
76        std::process::exit(0);
77    }
78
79    if options.shm_cleanup {
80        // clean up any orphaned shared memory
81        shm_cleanup::shm_cleanup(shm_cleanup::SHM_DIR_PATH)
82            .context("Cleaning shared memory files")?;
83        std::process::exit(0);
84    }
85
86    // read from stdin if the config filename is given as '-'
87    let config_filename: String = match options.config.as_ref().unwrap().as_str() {
88        "-" => "/dev/stdin",
89        x => x,
90    }
91    .into();
92
93    // load the configuration yaml
94    let config_file = load_config_file(&config_filename, true)
95        .with_context(|| format!("Failed to load configuration file {}", config_filename))?;
96
97    // generate the final shadow configuration from the config file and cli options
98    let shadow_config = ConfigOptions::new(config_file, options.clone());
99
100    if options.show_config {
101        eprintln!("{:#?}", shadow_config);
102        return Ok(());
103    }
104
105    // configure other global state
106    if shadow_config.experimental.use_object_counters.unwrap() {
107        worker::enable_object_counters();
108    }
109
110    // get the log level
111    let log_level = shadow_config.general.log_level.unwrap();
112    let log_level: log::Level = log_level.into();
113
114    // start up the logging subsystem to handle all future messages
115    shadow_logger::init(
116        log_level.to_level_filter(),
117        shadow_config.experimental.report_errors_to_stderr.unwrap(),
118    )
119    .unwrap();
120
121    // disable log buffering during startup so that we see every message immediately in the terminal
122    shadow_logger::set_buffering_enabled(false);
123
124    // check if some log levels have been compiled out
125    if log_level > log::STATIC_MAX_LEVEL {
126        log::warn!(
127            "Log level set to {}, but messages higher than {} have been compiled out",
128            log_level,
129            log::STATIC_MAX_LEVEL,
130        );
131    }
132
133    // warn if running with root privileges
134    if nix::unistd::getuid().is_root() {
135        // a real-world example is opentracker, which will attempt to drop privileges if it detects
136        // that the effective user is root, but this fails in shadow and opentracker exits with an
137        // error
138        log::warn!(
139            "Shadow is running as root. Shadow does not emulate Linux permissions, and some
140            applications may behave differently when running as root. It is recommended to run
141            Shadow as a non-root user."
142        );
143    } else if nix::unistd::geteuid().is_root() {
144        log::warn!(
145            "Shadow is running with root privileges. Shadow does not emulate Linux permissions,
146            and some applications may behave differently when running with root privileges. It
147            is recommended to run Shadow as a non-root user."
148        );
149    }
150
151    // before we run the simulation, clean up any orphaned shared memory
152    if let Err(e) = shm_cleanup::shm_cleanup(shm_cleanup::SHM_DIR_PATH) {
153        log::warn!("Unable to clean up shared memory files: {:?}", e);
154    }
155
156    // save the platform data required for CPU pinning
157    if shadow_config.experimental.use_cpu_pinning.unwrap() {
158        #[allow(clippy::collapsible_if)]
159        if unsafe { c::affinity_initPlatformInfo() } != 0 {
160            return Err(anyhow::anyhow!("Unable to initialize platform info"));
161        }
162    }
163
164    // raise fd soft limit to hard limit
165    raise_rlimit(resource::Resource::RLIMIT_NOFILE).context("Could not raise fd limit")?;
166
167    // raise number of processes/threads soft limit to hard limit
168    raise_rlimit(resource::Resource::RLIMIT_NPROC).context("Could not raise proc limit")?;
169
170    if shadow_config.experimental.use_sched_fifo.unwrap() {
171        set_sched_fifo().context("Could not set real-time scheduler mode to SCHED_FIFO")?;
172        log::debug!("Successfully set real-time scheduler mode to SCHED_FIFO");
173    }
174
175    // Disable address space layout randomization of processes forked from this
176    // one to improve determinism in cases when an executable under simulation
177    // branch on memory addresses.
178    match disable_aslr() {
179        Ok(()) => log::debug!("ASLR disabled for processes forked from this parent process"),
180        Err(e) => log::warn!(
181            "Could not disable address space layout randomization. This may affect determinism: {e:#}"
182        ),
183    };
184
185    // check sidechannel mitigations
186    if sidechannel_mitigations_enabled().context("Failed to get sidechannel mitigation status")? {
187        log::warn!(
188            "Speculative Store Bypass sidechannel mitigation is enabled (perhaps by seccomp?). \
189             This typically adds ~30% performance overhead."
190        );
191    }
192
193    // log some information
194    eprintln!("** Starting Shadow {}", env!("CARGO_PKG_VERSION"));
195    let mut build_info = Vec::new();
196    write_build_info(&mut build_info).unwrap();
197    for line in std::str::from_utf8(&build_info).unwrap().trim().split('\n') {
198        log::info!("{line}");
199    }
200    log::info!("Logging current startup arguments and environment");
201    log_environment(args.clone());
202
203    if let Err(e) = verify_supported_system() {
204        log::warn!("Couldn't verify supported system: {e:?}")
205    }
206
207    log::debug!("Startup checks passed, we are ready to start the simulation");
208
209    // allow gdb to attach before starting the simulation
210    if options.gdb {
211        pause_for_gdb_attach().context("Could not pause shadow to allow gdb to attach")?;
212    }
213
214    let sim_config = SimConfig::new(&shadow_config, &options.debug_hosts.unwrap_or_default())
215        .context("Failed to initialize the simulation")?;
216
217    // allocate and initialize our main simulation driver
218    let controller = Controller::new(sim_config, &shadow_config);
219
220    // enable log buffering if not at trace level
221    let buffer_log = !log::log_enabled!(log::Level::Trace);
222    shadow_logger::set_buffering_enabled(buffer_log);
223    if buffer_log {
224        log::info!("Log message buffering is enabled for efficiency");
225    }
226
227    // run the simulation
228    controller.run().context("Failed to run the simulation")?;
229
230    // disable log buffering
231    shadow_logger::set_buffering_enabled(false);
232    if buffer_log {
233        // only show if we disabled buffering above
234        log::info!("Log message buffering is disabled during cleanup");
235    }
236
237    Ok(())
238}
239
240pub fn version() -> String {
241    let mut s = env!("CARGO_PKG_VERSION").to_string();
242
243    if let (Some(commit), Some(date)) = (GIT_COMMIT_INFO, GIT_DATE) {
244        write!(s, " — {commit} {date}").unwrap();
245    }
246
247    s
248}
249
250fn write_build_info(mut w: impl std::io::Write) -> std::io::Result<()> {
251    writeln!(w, "Shadow {}", version())?;
252    writeln!(
253        w,
254        "GLib {}.{}.{}",
255        c::GLIB_MAJOR_VERSION,
256        c::GLIB_MINOR_VERSION,
257        c::GLIB_MICRO_VERSION,
258    )?;
259    writeln!(w, "Built on {}", BUILD_TIMESTAMP)?;
260    writeln!(
261        w,
262        "Built from git branch {}",
263        GIT_BRANCH.unwrap_or("<unknown>"),
264    )?;
265    writeln!(w, "{}", env!("SHADOW_BUILD_INFO"))?;
266    writeln!(w, "{HELP_INFO_STR}")?;
267
268    Ok(())
269}
270
271fn verify_supported_system() -> anyhow::Result<()> {
272    let uts_name = nix::sys::utsname::uname()?;
273    let sysname = uts_name
274        .sysname()
275        .to_str()
276        .with_context(|| "Decoding system name")?;
277    if sysname != "Linux" {
278        anyhow::bail!("Unsupported sysname: {sysname}");
279    }
280    let version = uts_name
281        .release()
282        .to_str()
283        .with_context(|| "Decoding system release")?;
284    let mut version_parts = version.split('.');
285    let Some(major) = version_parts.next() else {
286        anyhow::bail!("Couldn't find major version in : {version}");
287    };
288    let major: i32 = major
289        .parse()
290        .with_context(|| format!("Parsing major version number '{major}'"))?;
291    let Some(minor) = version_parts.next() else {
292        anyhow::bail!("Couldn't find minor version in : {version}");
293    };
294    let minor: i32 = minor
295        .parse()
296        .with_context(|| format!("Parsing minor version number '{minor}'"))?;
297
298    // Keep in sync with `supported_platforms.md`.
299    const MIN_KERNEL_VERSION: (i32, i32) = (5, 4);
300
301    if (major, minor) < MIN_KERNEL_VERSION {
302        anyhow::bail!(
303            "kernel version {major}.{minor} is older than minimum supported version {}.{}",
304            MIN_KERNEL_VERSION.0,
305            MIN_KERNEL_VERSION.1
306        );
307    }
308
309    Ok(())
310}
311
312fn verify_glib_version() -> anyhow::Result<()> {
313    // Technically redundant, since our minimum glib version enforced by cmake is already larger
314    // than this version. Still, doesn't hurt to keep this check for posterity in case we ever try
315    // to go back to supporting older versions.
316    if c::GLIB_MAJOR_VERSION == 2 && c::GLIB_MINOR_VERSION == 40 {
317        anyhow::bail!(
318            "You compiled against GLib version {}.{}.{}, which has bugs known to break \"
319            Shadow. Please update to a newer version of GLib.",
320            c::GLIB_MAJOR_VERSION,
321            c::GLIB_MINOR_VERSION,
322            c::GLIB_MICRO_VERSION,
323        );
324    }
325
326    // check the that run-time GLib matches the compiled version
327    let mismatch = unsafe {
328        c::glib_check_version(
329            c::GLIB_MAJOR_VERSION,
330            c::GLIB_MINOR_VERSION,
331            c::GLIB_MICRO_VERSION,
332        )
333    };
334
335    if !mismatch.is_null() {
336        let mismatch = unsafe { std::ffi::CStr::from_ptr(mismatch) };
337        anyhow::bail!(
338            "The version of the run-time GLib library ({}.{}.{}) is not compatible with \
339            the version against which Shadow was compiled ({}.{}.{}). GLib message: '{}'.",
340            unsafe { c::glib_major_version },
341            unsafe { c::glib_minor_version },
342            unsafe { c::glib_micro_version },
343            c::GLIB_MAJOR_VERSION,
344            c::GLIB_MINOR_VERSION,
345            c::GLIB_MICRO_VERSION,
346            mismatch.to_string_lossy(),
347        );
348    }
349
350    Ok(())
351}
352
353fn load_config_file(
354    filename: impl AsRef<std::path::Path>,
355    extended_yaml: bool,
356) -> anyhow::Result<ConfigFileOptions> {
357    let file = std::fs::File::open(filename).context("Could not open config file")?;
358
359    // serde's default behaviour is to silently ignore duplicate keys during deserialization so we
360    // would typically need to use serde_with's `maps_duplicate_key_is_error()` on our
361    // 'ConfigFileOptions' struct to prevent duplicate hostnames, but since we deserialize to
362    // serde_yaml's `Value` type initially we don't need to prevent duplicate keys as serde_yaml
363    // does this for us: https://github.com/dtolnay/serde-yaml/pull/301
364
365    let mut config_file: serde_yaml::Value =
366        serde_yaml::from_reader(file).context("Could not parse configuration file as yaml")?;
367
368    if extended_yaml {
369        // apply the merge before removing extension fields
370        config_file
371            .apply_merge()
372            .context("Could not merge '<<' keys")?;
373
374        // remove top-level extension fields
375        if let serde_yaml::Value::Mapping(mapping) = &mut config_file {
376            // remove entries having a key beginning with "x-" (follows docker's convention:
377            // https://docs.docker.com/compose/compose-file/#extension)
378            mapping.retain(|key, _value| {
379                if let serde_yaml::Value::String(key) = key {
380                    if key.starts_with("x-") {
381                        return false;
382                    }
383                }
384                true
385            });
386        }
387    }
388
389    serde_yaml::from_value(config_file).context("Could not parse configuration file")
390}
391
392fn pause_for_gdb_attach() -> anyhow::Result<()> {
393    let pid = nix::unistd::getpid();
394    log::info!(
395        "Pausing with SIGTSTP to enable debugger attachment (pid {})",
396        pid
397    );
398    eprintln!(
399        "** Pausing with SIGTSTP to enable debugger attachment (pid {})",
400        pid
401    );
402
403    signal::raise(signal::Signal::SIGTSTP)?;
404
405    log::info!("Resuming now");
406    Ok(())
407}
408
409fn set_sched_fifo() -> anyhow::Result<()> {
410    let mut param: libc::sched_param = unsafe { std::mem::zeroed() };
411    param.sched_priority = 1;
412
413    let rv = nix::errno::Errno::result(unsafe {
414        libc::sched_setscheduler(0, libc::SCHED_FIFO, std::ptr::from_ref(&param))
415    })
416    .context("Could not set kernel SCHED_FIFO")?;
417
418    assert_eq!(rv, 0);
419
420    Ok(())
421}
422
423fn raise_rlimit(resource: resource::Resource) -> anyhow::Result<()> {
424    let (_soft_limit, hard_limit) = resource::getrlimit(resource)?;
425    resource::setrlimit(resource, hard_limit, hard_limit)?;
426    Ok(())
427}
428
429fn disable_aslr() -> anyhow::Result<()> {
430    let pers = personality::get().context("Could not get personality")?;
431    personality::set(pers | personality::Persona::ADDR_NO_RANDOMIZE)
432        .context("Could not set personality")?;
433    Ok(())
434}
435
436fn sidechannel_mitigations_enabled() -> anyhow::Result<bool> {
437    let state = nix::errno::Errno::result(unsafe {
438        libc::prctl(
439            libc::PR_GET_SPECULATION_CTRL,
440            libc::PR_SPEC_STORE_BYPASS,
441            0,
442            0,
443            0,
444        )
445    })
446    .context("Failed prctl()")?;
447    let state = state as u32;
448    Ok((state & libc::PR_SPEC_DISABLE) != 0)
449}
450
451fn log_environment(args: Vec<&OsStr>) {
452    for arg in args {
453        log::info!("arg: {}", arg.to_string_lossy());
454    }
455
456    for (key, value) in std::env::vars_os() {
457        let level = match key.to_string_lossy().borrow() {
458            "LD_PRELOAD" | "LD_STATIC_TLS_EXTRA" | "G_DEBUG" | "G_SLICE" => log::Level::Info,
459            _ => log::Level::Trace,
460        };
461        log::log!(level, "env: {:?}={:?}", key, value);
462    }
463}
464
465mod export {
466    use super::*;
467
468    #[unsafe(no_mangle)]
469    pub extern "C-unwind" fn main_runShadow(
470        argc: libc::c_int,
471        argv: *const *const libc::c_char,
472    ) -> libc::c_int {
473        let args = (0..argc).map(|x| unsafe { CStr::from_ptr(*argv.add(x as usize)) });
474        let args = args.map(|x| OsStr::from_bytes(x.to_bytes()));
475
476        let result = run_shadow(args.collect());
477        log::logger().flush();
478
479        if let Err(e) = result {
480            // log the full error, its context, and its backtrace if enabled
481            if log::log_enabled!(log::Level::Error) {
482                for line in format!("{:?}", e).split('\n') {
483                    log::error!("{}", line);
484                }
485                log::logger().flush();
486
487                // print the short error
488                eprintln!("** Shadow did not complete successfully: {}", e);
489                eprintln!("**   {}", e.root_cause());
490                eprintln!("** See the log for details");
491            } else {
492                // logging may not be configured yet, so print to stderr
493                eprintln!("{:?}", e);
494            }
495
496            return 1;
497        }
498
499        eprintln!("** Shadow completed successfully");
500        0
501    }
502}