elf_runner/
config.rs

1// Copyright 2021 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::error::ProgramError;
6use ::routing::policy::ScopedPolicyChecker;
7use fidl_fuchsia_data as fdata;
8use runner::StartInfoProgramError;
9
10const CREATE_RAW_PROCESSES_KEY: &str = "job_policy_create_raw_processes";
11const DENY_BAD_HANDLES_KEY: &str = "deny_bad_handles";
12const SHARED_PROCESS_KEY: &str = "is_shared_process";
13const CRITICAL_KEY: &str = "main_process_critical";
14const FORWARD_STDOUT_KEY: &str = "forward_stdout_to";
15const FORWARD_STDERR_KEY: &str = "forward_stderr_to";
16const VMEX_KEY: &str = "job_policy_ambient_mark_vmo_exec";
17const STOP_EVENT_KEY: &str = "lifecycle.stop_event";
18const STOP_EVENT_VARIANTS: [&'static str; 2] = ["notify", "ignore"];
19const USE_NEXT_VDSO_KEY: &str = "use_next_vdso";
20const JOB_WITH_AVAILABLE_EXCEPTION_CHANNEL_KEY: &str = "job_with_available_exception_channel";
21const MEMORY_ATTRIBUTION: &str = "memory_attribution";
22
23/// Target sink for stdout and stderr output streams.
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum StreamSink {
26    /// The component omitted configuration or explicitly requested forwarding to the log.
27    Log,
28    /// The component requested to not forward to the log.
29    None,
30}
31
32impl Default for StreamSink {
33    fn default() -> Self {
34        StreamSink::Log
35    }
36}
37
38/// Parsed representation of the `ComponentStartInfo.program` dictionary.
39#[derive(Debug, Default, Eq, PartialEq, Clone)]
40pub struct ElfProgramConfig {
41    pub binary: String,
42    pub args: Vec<String>,
43    pub notify_lifecycle_stop: bool,
44    pub ambient_mark_vmo_exec: bool,
45    pub main_process_critical: bool,
46    pub create_raw_processes: bool,
47    pub deny_bad_handles: bool,
48    pub is_shared_process: bool,
49    pub use_next_vdso: bool,
50    pub job_with_available_exception_channel: bool,
51    pub memory_attribution: bool,
52    pub stdout_sink: StreamSink,
53    pub stderr_sink: StreamSink,
54    pub environ: Option<Vec<String>>,
55}
56
57impl ElfProgramConfig {
58    /// Parse the given dictionary into an ElfProgramConfig, checking it against security policy as
59    /// needed.
60    ///
61    /// Checking against security policy is intentionally combined with parsing here, so that policy
62    /// enforcement is as close to the point of parsing as possible and can't be inadvertently skipped.
63    pub fn parse_and_check(
64        program: &fdata::Dictionary,
65        checker: &ScopedPolicyChecker,
66    ) -> Result<Self, ProgramError> {
67        let config = Self::parse(program).map_err(ProgramError::Parse)?;
68
69        if config.ambient_mark_vmo_exec {
70            checker.ambient_mark_vmo_exec_allowed().map_err(ProgramError::Policy)?;
71        }
72
73        if config.main_process_critical {
74            checker.main_process_critical_allowed().map_err(ProgramError::Policy)?;
75        }
76
77        if config.create_raw_processes {
78            checker.create_raw_processes_allowed().map_err(ProgramError::Policy)?;
79        }
80
81        if config.is_shared_process && !config.create_raw_processes {
82            return Err(ProgramError::SharedProcessRequiresJobPolicy);
83        }
84
85        Ok(config)
86    }
87
88    // Parses a `program` dictionary but does not check policy or consistency.
89    fn parse(program: &fdata::Dictionary) -> Result<Self, StartInfoProgramError> {
90        let notify_lifecycle_stop =
91            match runner::get_enum(program, STOP_EVENT_KEY, &STOP_EVENT_VARIANTS)? {
92                Some("notify") => true,
93                _ => false,
94            };
95
96        Ok(ElfProgramConfig {
97            binary: runner::get_program_binary_from_dict(&program)?,
98            args: runner::get_program_args_from_dict(&program)?,
99            notify_lifecycle_stop,
100            ambient_mark_vmo_exec: runner::get_bool(program, VMEX_KEY)?,
101            main_process_critical: runner::get_bool(program, CRITICAL_KEY)?,
102            create_raw_processes: runner::get_bool(program, CREATE_RAW_PROCESSES_KEY)?,
103            deny_bad_handles: runner::get_bool(program, DENY_BAD_HANDLES_KEY)?,
104            is_shared_process: runner::get_bool(program, SHARED_PROCESS_KEY)?,
105            use_next_vdso: runner::get_bool(program, USE_NEXT_VDSO_KEY)?,
106            job_with_available_exception_channel: runner::get_bool(
107                program,
108                JOB_WITH_AVAILABLE_EXCEPTION_CHANNEL_KEY,
109            )?,
110            memory_attribution: runner::get_bool(program, MEMORY_ATTRIBUTION)?,
111            stdout_sink: get_stream_sink(&program, FORWARD_STDOUT_KEY)?,
112            stderr_sink: get_stream_sink(&program, FORWARD_STDERR_KEY)?,
113            environ: runner::get_environ(&program)?,
114        })
115    }
116
117    pub fn process_options(&self) -> zx::ProcessOptions {
118        if self.is_shared_process {
119            zx::ProcessOptions::SHARED
120        } else {
121            zx::ProcessOptions::empty()
122        }
123    }
124}
125
126fn get_stream_sink(
127    dict: &fdata::Dictionary,
128    key: &str,
129) -> Result<StreamSink, StartInfoProgramError> {
130    match runner::get_enum(dict, key, &["log", "none"])? {
131        Some("log") => Ok(StreamSink::Log),
132        Some("none") => Ok(StreamSink::None),
133        Some(_) => unreachable!("get_enum returns only values in variants"),
134        None => Ok(StreamSink::default()),
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use ::routing::policy::PolicyError;
142    use assert_matches::assert_matches;
143    use cm_config::{
144        AllowlistEntryBuilder, ChildPolicyAllowlists, JobPolicyAllowlists, SecurityPolicy,
145    };
146    use fidl_fuchsia_data as fdata;
147    use lazy_static::lazy_static;
148    use moniker::Moniker;
149    use std::collections::HashMap;
150    use std::sync::Arc;
151    use test_case::test_case;
152
153    const BINARY_KEY: &str = "binary";
154    const TEST_BINARY: &str = "test_binary";
155
156    lazy_static! {
157        static ref TEST_MONIKER: Moniker = Moniker::root();
158        static ref PERMISSIVE_SECURITY_POLICY: Arc<SecurityPolicy> = Arc::new(SecurityPolicy {
159            job_policy: JobPolicyAllowlists {
160                ambient_mark_vmo_exec: vec![AllowlistEntryBuilder::build_exact_from_moniker(
161                    &TEST_MONIKER
162                ),],
163                main_process_critical: vec![AllowlistEntryBuilder::build_exact_from_moniker(
164                    &TEST_MONIKER
165                ),],
166                create_raw_processes: vec![AllowlistEntryBuilder::build_exact_from_moniker(
167                    &TEST_MONIKER
168                ),],
169            },
170            capability_policy: HashMap::new(),
171            debug_capability_policy: HashMap::new(),
172            child_policy: ChildPolicyAllowlists { reboot_on_terminate: vec![] },
173        });
174        static ref RESTRICTIVE_SECURITY_POLICY: Arc<SecurityPolicy> =
175            Arc::new(SecurityPolicy::default());
176    }
177
178    #[test_case("forward_stdout_to", new_string("log"), ElfProgramConfig { stdout_sink: StreamSink::Log, ..default_valid_config()} ; "when_stdout_log")]
179    #[test_case("forward_stdout_to", new_string("none"), ElfProgramConfig { stdout_sink: StreamSink::None, ..default_valid_config()} ; "when_stdout_none")]
180    #[test_case("forward_stderr_to", new_string("log"), ElfProgramConfig { stderr_sink: StreamSink::Log, ..default_valid_config()} ; "when_stderr_log")]
181    #[test_case("forward_stderr_to", new_string("none"), ElfProgramConfig { stderr_sink: StreamSink::None, ..default_valid_config()} ; "when_stderr_none")]
182    #[test_case("environ", new_empty_vec(), ElfProgramConfig { environ: None, ..default_valid_config()} ; "when_environ_empty")]
183    #[test_case("environ", new_vec(vec!["FOO=BAR"]), ElfProgramConfig { environ: Some(vec!["FOO=BAR".into()]), ..default_valid_config()} ; "when_environ_has_values")]
184    #[test_case("lifecycle.stop_event", new_string("notify"), ElfProgramConfig { notify_lifecycle_stop: true, ..default_valid_config()} ; "when_stop_event_notify")]
185    #[test_case("lifecycle.stop_event", new_string("ignore"), ElfProgramConfig { notify_lifecycle_stop: false, ..default_valid_config()} ; "when_stop_event_ignore")]
186    #[test_case("main_process_critical", new_string("true"), ElfProgramConfig { main_process_critical: true, ..default_valid_config()} ; "when_main_process_critical_true")]
187    #[test_case("main_process_critical", new_string("false"), ElfProgramConfig { main_process_critical: false, ..default_valid_config()} ; "when_main_process_critical_false")]
188    #[test_case("job_policy_ambient_mark_vmo_exec", new_string("true"), ElfProgramConfig { ambient_mark_vmo_exec: true, ..default_valid_config()} ; "when_ambient_mark_vmo_exec_true")]
189    #[test_case("job_policy_ambient_mark_vmo_exec", new_string("false"), ElfProgramConfig { ambient_mark_vmo_exec: false, ..default_valid_config()} ; "when_ambient_mark_vmo_exec_false")]
190    #[test_case("job_policy_create_raw_processes", new_string("true"), ElfProgramConfig { create_raw_processes: true, ..default_valid_config()} ; "when_create_raw_processes_true")]
191    #[test_case("job_policy_create_raw_processes", new_string("false"), ElfProgramConfig { create_raw_processes: false, ..default_valid_config()} ; "when_create_raw_processes_false")]
192    #[test_case("use_next_vdso", new_string("true"), ElfProgramConfig { use_next_vdso: true, ..default_valid_config()} ; "use_next_vdso_true")]
193    #[test_case("use_next_vdso", new_string("false"), ElfProgramConfig { use_next_vdso: false, ..default_valid_config()} ; "use_next_vdso_false")]
194    fn test_parse_and_check_with_permissive_policy(
195        key: &str,
196        value: fdata::DictionaryValue,
197        expected: ElfProgramConfig,
198    ) {
199        let checker =
200            ScopedPolicyChecker::new(PERMISSIVE_SECURITY_POLICY.clone(), TEST_MONIKER.clone());
201        let program = new_program_stanza(key, value);
202
203        let actual = ElfProgramConfig::parse_and_check(&program, &checker).unwrap();
204
205        assert_eq!(actual, expected);
206    }
207
208    #[test_case("job_policy_ambient_mark_vmo_exec", new_string("true") , "ambient_mark_vmo_exec" ; "when_ambient_mark_vmo_exec_true")]
209    #[test_case("main_process_critical", new_string("true"), "main_process_critical" ; "when_main_process_critical_true")]
210    #[test_case("job_policy_create_raw_processes", new_string("true"), "create_raw_processes" ; "when_create_raw_processes_true")]
211    fn test_parse_and_check_with_restrictive_policy(
212        key: &str,
213        value: fdata::DictionaryValue,
214        policy: &str,
215    ) {
216        let checker =
217            ScopedPolicyChecker::new(RESTRICTIVE_SECURITY_POLICY.clone(), TEST_MONIKER.clone());
218        let program = new_program_stanza(key, value);
219
220        let actual = ElfProgramConfig::parse_and_check(&program, &checker);
221
222        assert_matches!(
223            actual,
224            Err(ProgramError::Policy(PolicyError::JobPolicyDisallowed {
225                policy: p,
226                ..
227            }))
228            if p == policy
229        );
230    }
231
232    #[test_case("lifecycle.stop_event", new_string("invalid") ; "for_stop_event")]
233    #[test_case("environ", new_empty_string() ; "for_environ")]
234    fn test_parse_and_check_with_invalid_value(key: &str, value: fdata::DictionaryValue) {
235        // Use a permissive policy because we want to fail *iff* value set for
236        // key is invalid.
237        let checker =
238            ScopedPolicyChecker::new(PERMISSIVE_SECURITY_POLICY.clone(), TEST_MONIKER.clone());
239        let program = new_program_stanza(key, value);
240
241        let actual = ElfProgramConfig::parse_and_check(&program, &checker);
242
243        assert_matches!(
244            actual,
245            Err(ProgramError::Parse(StartInfoProgramError::InvalidValue(k, _, _)))
246            if k == key
247        );
248    }
249
250    #[test_case("lifecycle.stop_event", new_empty_vec() ; "for_stop_event")]
251    #[test_case("job_policy_ambient_mark_vmo_exec", new_empty_vec() ; "for_ambient_mark_vmo_exec")]
252    #[test_case("main_process_critical", new_empty_vec() ; "for_main_process_critical")]
253    #[test_case("job_policy_create_raw_processes", new_empty_vec() ; "for_create_raw_processes")]
254    #[test_case("forward_stdout_to", new_empty_vec() ; "for_stdout")]
255    #[test_case("forward_stderr_to", new_empty_vec() ; "for_stderr")]
256    #[test_case("use_next_vdso", new_empty_vec() ; "for_use_next_vdso")]
257    fn test_parse_and_check_with_invalid_type(key: &str, value: fdata::DictionaryValue) {
258        // Use a permissive policy because we want to fail *iff* value set for
259        // key is invalid.
260        let checker =
261            ScopedPolicyChecker::new(PERMISSIVE_SECURITY_POLICY.clone(), TEST_MONIKER.clone());
262        let program = new_program_stanza(key, value);
263
264        let actual = ElfProgramConfig::parse_and_check(&program, &checker);
265
266        assert_matches!(
267            actual,
268            Err(ProgramError::Parse(StartInfoProgramError::InvalidType(k)))
269            if k == key
270        );
271    }
272
273    fn default_valid_config() -> ElfProgramConfig {
274        ElfProgramConfig { binary: TEST_BINARY.to_string(), args: Vec::new(), ..Default::default() }
275    }
276
277    fn new_program_stanza(key: &str, value: fdata::DictionaryValue) -> fdata::Dictionary {
278        fdata::Dictionary {
279            entries: Some(vec![
280                fdata::DictionaryEntry {
281                    key: BINARY_KEY.to_owned(),
282                    value: Some(Box::new(new_string(TEST_BINARY))),
283                },
284                fdata::DictionaryEntry { key: key.to_owned(), value: Some(Box::new(value)) },
285            ]),
286            ..Default::default()
287        }
288    }
289
290    fn new_string(value: &str) -> fdata::DictionaryValue {
291        fdata::DictionaryValue::Str(value.to_owned())
292    }
293
294    fn new_vec(values: Vec<&str>) -> fdata::DictionaryValue {
295        fdata::DictionaryValue::StrVec(values.into_iter().map(str::to_owned).collect())
296    }
297
298    fn new_empty_string() -> fdata::DictionaryValue {
299        fdata::DictionaryValue::Str("".to_owned())
300    }
301
302    fn new_empty_vec() -> fdata::DictionaryValue {
303        fdata::DictionaryValue::StrVec(vec![])
304    }
305}