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