Skip to main content

starnix_kernel_runner/
features.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::ContainerStartInfo;
6use anyhow::{Error, anyhow};
7use starnix_container_structured_config::Config as ContainerStructuredConfig;
8use starnix_core::fs::tmpfs::register_selinux_test_suite_ramfs;
9use starnix_core::mm::MlockPinFlavor;
10use starnix_core::task::{Kernel, KernelFeatures, OomWatermarksMb, SystemLimits};
11use starnix_features::{Feature, FeatureAndArgs};
12use starnix_modules::{Framebuffer, Modules, validate_module};
13use starnix_uapi::error;
14use starnix_uapi::errors::Errno;
15use std::sync::Arc;
16
17/// A collection of parsed features, and their arguments.
18#[derive(Default, Debug)]
19pub struct Features {
20    /// Features that must be available to the kernel after initialization.
21    pub kernel: KernelFeatures,
22
23    /// SELinux configuration.
24    pub selinux: SELinuxFeature,
25
26    /// Limits value passed to the kernel during the initialization.
27    pub system_limits: SystemLimits,
28
29    /// This controls whether or not the default framebuffer background is black or colorful, to
30    /// aid debugging.
31    pub enable_visual_debugging: bool,
32
33    /// The value of the initial view ID annotation.
34    pub initial_view_id_annotation: String,
35
36    /// Include the /container directory in the root file system.
37    pub container: bool,
38
39    /// Include the /test_data directory in the root file system.
40    pub test_data: bool,
41
42    /// Include the /custom_artifacts directory in the root file system.
43    pub custom_artifacts: bool,
44
45    /// Whether to provide android with a serial number.
46    pub android_serialno: bool,
47
48    /// Whether to allow the root filesystem to be read/write.
49    pub rootfs_rw: bool,
50
51    pub enable_utc_time_adjustment: bool,
52
53    /// Whether to add android bootreason to kernel cmdline.
54    pub android_bootreason: bool,
55
56    pub additional_mounts: Option<Vec<String>>,
57
58    /// Whether to initialize unified tracing with a Fuchsia trace observer.
59    pub unified_tracing: bool,
60}
61
62#[derive(Default, Debug, PartialEq)]
63pub struct SELinuxFeature {
64    /// True if SELinux should be enabled in the container.
65    pub enabled: bool,
66
67    /// Optional set of options to pass to the SELinux module.
68    pub options: String,
69
70    /// Optional set of access-check exceptions to pass to the SELinux module.
71    pub exceptions: Vec<String>,
72}
73
74impl Features {
75    pub fn record_inspect(&self, parent_node: &fuchsia_inspect::Node) {
76        parent_node.record_child("features", |inspect_node| match self {
77            Features {
78                kernel:
79                    KernelFeatures {
80                        bpf_v2,
81                        devtmpfs,
82                        enable_suid,
83                        io_uring,
84                        error_on_failed_reboot,
85                        default_uid,
86                        default_seclabel,
87                        selinux_test_suite,
88                        default_ns_mount_options,
89                        mlock_always_onfault,
90                        mlock_pin_flavor,
91                        crash_report_throttling,
92                        wifi,
93                        cached_zx_map_info_bytes,
94                        dirent_cache_size,
95                        fake_ion,
96                        remotevol_in_process,
97                        log_sysfs_lookup_misses,
98                        oom_watermarks_mb,
99                        stop_last_on_shutdown,
100                    },
101                system_limits,
102                selinux,
103                enable_visual_debugging,
104                container,
105                test_data,
106                custom_artifacts,
107                android_serialno,
108                rootfs_rw,
109                enable_utc_time_adjustment,
110                android_bootreason,
111                additional_mounts,
112                unified_tracing,
113                initial_view_id_annotation,
114            } => {
115                inspect_node.record_bool("selinux", selinux.enabled);
116                inspect_node.record_bool("unified_tracing", *unified_tracing);
117                inspect_node.record_string(
118                    "initial_view_id_annotation",
119                    initial_view_id_annotation.as_str(),
120                );
121                inspect_node.record_bool("container", *container);
122                inspect_node.record_bool("test_data", *test_data);
123                inspect_node.record_bool("custom_artifacts", *custom_artifacts);
124                inspect_node.record_bool("android_serialno", *android_serialno);
125                inspect_node.record_bool("rootfs_rw", *rootfs_rw);
126                inspect_node.record_bool("android_bootreason", *android_bootreason);
127                inspect_node.record_string("ping_group_range", {
128                    let range = system_limits.socket.icmp_ping_gids.lock();
129                    std::format!("{},{}", range.start, range.end - 1)
130                });
131
132                inspect_node.record_child("kernel", |kernel_node| {
133                    kernel_node.record_bool("bpf_v2", *bpf_v2);
134                    kernel_node.record_bool("devtmpfs", *devtmpfs);
135                    kernel_node.record_bool("enable_suid", *enable_suid);
136                    kernel_node.record_bool("io_uring", *io_uring);
137                    kernel_node.record_bool("error_on_failed_reboot", *error_on_failed_reboot);
138                    kernel_node.record_bool("enable_visual_debugging", *enable_visual_debugging);
139                    kernel_node.record_int("default_uid", (*default_uid).into());
140                    kernel_node.record_string(
141                        "default_seclabel",
142                        default_seclabel.as_deref().unwrap_or_default(),
143                    );
144                    kernel_node.record_bool("selinux_test_suite", *selinux_test_suite);
145                    kernel_node.record_bool("crash_report_throttling", *crash_report_throttling);
146                    kernel_node
147                        .record_uint("cached_zx_map_info_bytes", *cached_zx_map_info_bytes as u64);
148                    kernel_node.record_bool("log_sysfs_lookup_misses", *log_sysfs_lookup_misses);
149                    if let Some(OomWatermarksMb { out_of_memory_mb, critical_mb, warning_mb }) =
150                        oom_watermarks_mb
151                    {
152                        kernel_node.record_uint("out_of_memory_mb", *out_of_memory_mb as u64);
153                        kernel_node.record_uint("critical_mb", *critical_mb as u64);
154                        kernel_node.record_uint("warning_mb", *warning_mb as u64);
155                    }
156                    inspect_node.record_string(
157                        "default_ns_mount_options",
158                        format!("{:?}", default_ns_mount_options),
159                    );
160                    inspect_node
161                        .record_bool("enable_utc_time_adjustment", *enable_utc_time_adjustment);
162                    inspect_node.record_bool("mlock_always_onfault", *mlock_always_onfault);
163                    inspect_node
164                        .record_string("mlock_pin_flavor", format!("{:?}", mlock_pin_flavor));
165                    inspect_node.record_bool("wifi", *wifi);
166                    inspect_node
167                        .record_string("additional_mounts", format!("{:?}", additional_mounts));
168                    inspect_node.record_uint("dirent_cache_size", *dirent_cache_size as u64);
169                    inspect_node.record_bool("fake_ion", *fake_ion);
170                    inspect_node.record_bool("remotevol_in_process", *remotevol_in_process);
171                    kernel_node.record_string(
172                        "stop_last_on_shutdown",
173                        format!("{:?}", stop_last_on_shutdown),
174                    );
175                });
176            }
177        });
178    }
179}
180
181/// Parses container features and modules from `start_info` and `kernel_extra_features`.
182pub fn parse_features(
183    start_info: &ContainerStartInfo,
184    kernel_extra_features: &[String],
185) -> Result<(Features, Modules), Error> {
186    let ContainerStructuredConfig {
187        crash_report_throttling,
188        enable_utc_time_adjustment,
189        extra_features,
190        cached_zx_map_info_bytes,
191        mlock_always_onfault,
192        mlock_pin_flavor,
193        selinux_exceptions,
194        ui_visual_debugging_level,
195        additional_mounts,
196        dirent_cache_size,
197        initial_view_id_annotation,
198    } = &start_info.config;
199
200    let mut features = Features::default();
201    let mut modules = Modules::default();
202
203    let mut legacy_boot_notifier_cpu_boost: Option<String> = None;
204    let mut legacy_aspect_ratio: Option<String> = None;
205
206    for entry in start_info
207        .program
208        .features
209        .iter()
210        .chain(kernel_extra_features.iter())
211        .chain(extra_features.iter())
212    {
213        let FeatureAndArgs { feature, raw_args } = FeatureAndArgs::try_parse(entry)?;
214        match (feature, raw_args) {
215            (Feature::AndroidSerialno, _) => features.android_serialno = true,
216            (Feature::AndroidBootreason, _) => features.android_bootreason = true,
217            (Feature::AspectRatio, Some(args)) => {
218                legacy_aspect_ratio = Some(args);
219            }
220            (Feature::AspectRatio, None) => {
221                return Err(anyhow!(
222                    "Aspect ratio feature must contain the aspect ratio in the format: aspect_ratio:w:h"
223                ));
224            }
225            (Feature::Container, _) => features.container = true,
226            (Feature::CustomArtifacts, _) => features.custom_artifacts = true,
227            (Feature::Devtmpfs, _) => features.kernel.devtmpfs = true,
228            (
229                Feature::AndroidUsb
230                | Feature::Ashmem
231                | Feature::BootNotifier
232                | Feature::DataCollectionConsentSync
233                | Feature::Fastrpc
234                | Feature::Framebuffer
235                | Feature::Gfxstream
236                | Feature::GoogleOdpm
237                | Feature::Gralloc
238                | Feature::HvdcpOpti
239                | Feature::Kgsl
240                | Feature::Magma
241                | Feature::MmcblkStub
242                | Feature::Nanohub
243                | Feature::NetworkManager
244                | Feature::Thermal
245                | Feature::Usbfs
246                | Feature::WakeupTest,
247                _,
248            ) => modules.enable(&feature.to_string()),
249            (Feature::BootNotifierCpuBoost, Some(arg)) => {
250                legacy_boot_notifier_cpu_boost = Some(arg);
251            }
252            (Feature::BootNotifierCpuBoost, None) => {
253                return Err(anyhow!(
254                    "boot_notifier_cpu_boost feature must have an argument (e.g. \"boot_notifier_cpu_boost:60\")"
255                ));
256            }
257            (Feature::MagmaSupportedVendors, Some(arg)) => {
258                modules.add_with_args("magma", vec![format!("supported_vendors={arg}")]);
259            }
260            (Feature::MagmaSupportedVendors, None) => {
261                return Err(anyhow!(
262                    "Feature format must be: magma_supported_vendors:0x1234[,0xabcd]"
263                ));
264            }
265            (Feature::UnifiedTracing, _) => features.unified_tracing = true,
266            (Feature::Bpf, Some(version)) => features.kernel.bpf_v2 = version == "v2",
267            (Feature::Bpf, None) => {
268                return Err(anyhow!("bpf feature must have an argument (e.g. \"bpf:v2\")"));
269            }
270            (Feature::EnableSuid, _) => features.kernel.enable_suid = true,
271            (Feature::IoUring, _) => features.kernel.io_uring = true,
272            (Feature::ErrorOnFailedReboot, _) => features.kernel.error_on_failed_reboot = true,
273            (Feature::Perfetto, Some(socket_path)) => {
274                modules.add_with_args("perfetto", vec![format!("socket_path={socket_path}")]);
275            }
276            (Feature::Perfetto, None) => {
277                return Err(anyhow!("Perfetto feature must contain a socket path"));
278            }
279            (Feature::PingGroupRange, Some(arg)) => {
280                let mut args = arg.split(',');
281                let (min, max) = (|| {
282                    let min = args.next()?.trim_ascii().parse::<u32>().ok()?;
283                    let max = args.next()?.trim_ascii().parse::<u32>().ok()?.checked_add(1)?;
284                    if args.next().is_some() {
285                        return None;
286                    }
287                    Some((min, max))
288                })()
289                .ok_or_else(|| anyhow!("Feature format must be: ping_group_range:0,100"))?;
290                *features.system_limits.socket.icmp_ping_gids.lock() = min..max;
291            }
292            (Feature::PingGroupRange, None) => {
293                return Err(anyhow!("Feature format must be: ping_group_range:0,100"));
294            }
295            (Feature::RootfsRw, _) => features.rootfs_rw = true,
296            (Feature::Selinux, arg) => {
297                features.selinux = SELinuxFeature {
298                    enabled: true,
299                    options: arg.unwrap_or_default(),
300                    exceptions: selinux_exceptions.clone(),
301                };
302            }
303            (Feature::SelinuxTestSuite, _) => features.kernel.selinux_test_suite = true,
304            (Feature::TestData, _) => features.test_data = true,
305            (Feature::Cooling, Some(arg)) => {
306                modules.add_with_args("thermal", vec![format!("cooling={arg}")]);
307            }
308            (Feature::Cooling, None) => {
309                return Err(anyhow!("cooling feature must have an argument"));
310            }
311            (Feature::Wifi, _) => features.kernel.wifi = true,
312            (Feature::AdditionalMounts, _) => {
313                features.additional_mounts = Some(additional_mounts.clone())
314            }
315            (Feature::FakeIon, _) => features.kernel.fake_ion = true,
316            (Feature::RemotevolInProcess, _) => features.kernel.remotevol_in_process = true,
317            (Feature::LogSysfsLookupMisses, _) => features.kernel.log_sysfs_lookup_misses = true,
318        };
319    }
320
321    let legacy_boot_notifier = modules.is_enabled("boot_notifier");
322    for spec in &start_info.program.modules {
323        modules.add_spec(spec)?;
324    }
325
326    if let Some(secs) = legacy_boot_notifier_cpu_boost {
327        if !legacy_boot_notifier && !modules.is_enabled("boot_notifier") {
328            return Err(anyhow!("boot_notifier_cpu_boost feature requires boot_notifier"));
329        }
330        let arg = format!("cpu_boost_secs={secs}");
331        validate_module("boot_notifier", Some(&[arg.as_str()]))?;
332        if modules.args("boot_notifier") == Some(&[]) {
333            modules.add_with_args("boot_notifier", vec![arg]);
334        }
335    }
336
337    if let Some(ratio) = legacy_aspect_ratio {
338        let arg = format!("aspect_ratio={ratio}");
339        validate_module("framebuffer", Some(&[arg.as_str()]))?;
340        if modules.args("framebuffer") == Some(&[]) {
341            modules.add_with_args("framebuffer", vec![arg]);
342        }
343    }
344
345    modules.validate()?;
346
347    if *ui_visual_debugging_level > 0 {
348        features.enable_visual_debugging = true;
349    }
350    features.enable_utc_time_adjustment = *enable_utc_time_adjustment;
351
352    features.kernel.default_uid = start_info.program.default_uid.0;
353    features.kernel.default_seclabel = start_info.program.default_seclabel.clone();
354    // Soft-transition fallback for out-of-tree container manifests that mount `devtmpfs`
355    // before adding `"devtmpfs"` to `program.features`.
356    if start_info.program.mounts.iter().chain(additional_mounts.iter()).any(|mount| {
357        let mut parts = mount.splitn(3, ':');
358        let _mount_point = parts.next();
359        parts.next() == Some("devtmpfs")
360    }) {
361        features.kernel.devtmpfs = true;
362    }
363    features.kernel.stop_last_on_shutdown =
364        start_info.program.stop_last_on_shutdown.iter().map(|path| path.as_str().into()).collect();
365    features.kernel.default_ns_mount_options =
366        if let Some(mount_options) = &start_info.program.default_ns_mount_options {
367            let options = mount_options
368                .iter()
369                .map(|item| {
370                    let mut splitter = item.splitn(2, ":");
371                    let key = splitter.next().expect("Failed to parse mount options");
372                    let value = splitter.next().expect("Failed to parse mount options");
373                    (key.to_string(), value.to_string())
374                })
375                .collect();
376            Some(options)
377        } else {
378            None
379        };
380
381    features.kernel.mlock_always_onfault = *mlock_always_onfault;
382    features.kernel.mlock_pin_flavor = MlockPinFlavor::parse(mlock_pin_flavor.as_str())?;
383    features.kernel.crash_report_throttling = *crash_report_throttling;
384    features.kernel.cached_zx_map_info_bytes = *cached_zx_map_info_bytes;
385    features.kernel.dirent_cache_size = *dirent_cache_size;
386    features.initial_view_id_annotation = initial_view_id_annotation.clone();
387    modules.set_framebuffer_config(
388        features.enable_visual_debugging,
389        features.initial_view_id_annotation.clone(),
390    );
391
392    Ok((features, modules))
393}
394
395/// Runs container-level kernel features enabled in `features`.
396pub fn run_container_features(kernel: &Arc<Kernel>, features: &Features) -> Result<(), Error> {
397    if features.unified_tracing {
398        kernel.trace_event_manager.init_trace_observer(kernel);
399    }
400    if features.kernel.selinux_test_suite {
401        register_selinux_test_suite_ramfs(kernel);
402    }
403    Ok(())
404}
405
406/// Runs features requested by individual components inside the container.
407pub fn run_component_features(
408    kernel: &Kernel,
409    entries: &Vec<String>,
410    mut incoming_dir: Option<fidl_fuchsia_io::DirectoryProxy>,
411) -> Result<(), Errno> {
412    for entry in entries {
413        match entry.as_str() {
414            "framebuffer" => {
415                Framebuffer::get(kernel)?.start_server(kernel, incoming_dir.take());
416            }
417            feature => {
418                return error!(ENOSYS, format!("Unsupported feature: {}", feature));
419            }
420        }
421    }
422    Ok(())
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428
429    fn test_start_info(features: Vec<String>, modules: Vec<String>) -> ContainerStartInfo {
430        ContainerStartInfo::new_for_test(
431            features,
432            modules,
433            ContainerStructuredConfig {
434                additional_mounts: Vec::new(),
435                cached_zx_map_info_bytes: 0,
436                crash_report_throttling: false,
437                dirent_cache_size: 0,
438                enable_utc_time_adjustment: false,
439                extra_features: Vec::new(),
440                initial_view_id_annotation: String::new(),
441                mlock_always_onfault: false,
442                mlock_pin_flavor: "noop".to_string(),
443                selinux_exceptions: Vec::new(),
444                ui_visual_debugging_level: 0,
445            },
446        )
447    }
448
449    #[fuchsia::test]
450    fn parse_program_modules() {
451        let start_info = test_start_info(
452            vec!["container".to_string()],
453            vec![
454                "ashmem".to_string(),
455                "boot_notifier cpu_boost_secs=60".to_string(),
456                "framebuffer aspect_ratio=9:16".to_string(),
457                "magma supported_vendors=0x13B5,0x5143,0x8086".to_string(),
458                "perfetto socket_path=/dev/socket/traced_consumer".to_string(),
459                "thermal cooling=cpu,fcc=8".to_string(),
460            ],
461        );
462
463        let (features, modules) =
464            parse_features(&start_info, &[]).expect("parse_features should succeed");
465        assert!(features.container);
466        assert!(modules.is_enabled("ashmem"));
467        assert_eq!(modules.args("ashmem"), Some(&[][..]));
468        assert_eq!(modules.args("boot_notifier"), Some(&["cpu_boost_secs=60".to_string()][..]));
469        assert_eq!(modules.args("framebuffer"), Some(&["aspect_ratio=9:16".to_string()][..]));
470        assert_eq!(
471            modules.args("magma"),
472            Some(&["supported_vendors=0x13B5,0x5143,0x8086".to_string()][..])
473        );
474        assert_eq!(
475            modules.args("perfetto"),
476            Some(&["socket_path=/dev/socket/traced_consumer".to_string()][..])
477        );
478        assert_eq!(modules.args("thermal"), Some(&["cooling=cpu,fcc=8".to_string()][..]));
479    }
480
481    #[fuchsia::test]
482    fn parse_legacy_module_features() {
483        let start_info = test_start_info(
484            vec![
485                "ashmem".to_string(),
486                "boot_notifier".to_string(),
487                "boot_notifier_cpu_boost:45".to_string(),
488                "framebuffer".to_string(),
489                "aspect_ratio:16:9".to_string(),
490                "magma_supported_vendors:0x13B5,0x8086".to_string(),
491                "mmcblk_stub".to_string(),
492                "perfetto:/dev/socket/traced_consumer".to_string(),
493                "thermal".to_string(),
494                "cooling:cpu,fcc=8".to_string(),
495            ],
496            Vec::new(),
497        );
498
499        let (_features, modules) =
500            parse_features(&start_info, &[]).expect("legacy features should translate");
501        assert!(modules.is_enabled("ashmem"));
502        assert!(modules.is_enabled("mmcblk_stub"));
503        assert_eq!(modules.args("boot_notifier"), Some(&["cpu_boost_secs=45".to_string()][..]));
504        assert_eq!(modules.args("framebuffer"), Some(&["aspect_ratio=16:9".to_string()][..]));
505        assert_eq!(
506            modules.args("magma"),
507            Some(&["supported_vendors=0x13B5,0x8086".to_string()][..])
508        );
509        assert_eq!(
510            modules.args("perfetto"),
511            Some(&["socket_path=/dev/socket/traced_consumer".to_string()][..])
512        );
513        assert_eq!(modules.args("thermal"), Some(&["cooling=cpu,fcc=8".to_string()][..]));
514    }
515
516    #[fuchsia::test]
517    fn reject_boot_notifier_cpu_boost_without_boot_notifier() {
518        let start_info =
519            test_start_info(vec!["boot_notifier_cpu_boost:60".to_string()], Vec::new());
520        assert!(parse_features(&start_info, &[]).is_err());
521    }
522
523    #[fuchsia::test]
524    fn reject_mutually_exclusive_odpm_and_hvdcp_opti() {
525        let start_info =
526            test_start_info(Vec::new(), vec!["google_odpm".to_string(), "hvdcp_opti".to_string()]);
527        assert!(parse_features(&start_info, &[]).is_err());
528
529        let mixed_info =
530            test_start_info(vec!["google_odpm".to_string()], vec!["hvdcp_opti".to_string()]);
531        assert!(parse_features(&mixed_info, &[]).is_err());
532    }
533
534    #[fuchsia::test]
535    fn hvdcp_opti_supersedes_default_battery_manager_and_rejects_explicit_conflict() {
536        let default_info = test_start_info(Vec::new(), Vec::new());
537        let (_features, default_modules) =
538            parse_features(&default_info, &[]).expect("default modules should parse");
539        assert!(default_modules.is_enabled("battery_manager"));
540        assert!(default_modules.is_enabled("goog_chip_info"));
541
542        let hvdcp_info = test_start_info(Vec::new(), vec!["hvdcp_opti".to_string()]);
543        let (_features, hvdcp_modules) =
544            parse_features(&hvdcp_info, &[]).expect("hvdcp_opti should supersede default battery");
545        assert!(hvdcp_modules.is_enabled("hvdcp_opti"));
546        assert!(!hvdcp_modules.is_enabled("battery_manager"));
547        assert_eq!(hvdcp_modules.args("battery_manager"), None);
548
549        let explicit_conflict = test_start_info(
550            Vec::new(),
551            vec!["battery_manager".to_string(), "hvdcp_opti".to_string()],
552        );
553        assert!(parse_features(&explicit_conflict, &[]).is_err());
554    }
555
556    #[fuchsia::test]
557    fn bare_module_does_not_overwrite_module_args() {
558        let start_info = test_start_info(
559            vec!["boot_notifier".to_string()],
560            vec![
561                "boot_notifier cpu_boost_secs=60".to_string(),
562                "boot_notifier".to_string(),
563                "framebuffer aspect_ratio=9:16".to_string(),
564                "framebuffer".to_string(),
565                "magma supported_vendors=0x13B5".to_string(),
566                "magma".to_string(),
567                "thermal cooling=cpu,fcc=8".to_string(),
568                "thermal".to_string(),
569            ],
570        );
571
572        let (_features, modules) =
573            parse_features(&start_info, &[]).expect("parse_features should succeed");
574        assert_eq!(modules.args("boot_notifier"), Some(&["cpu_boost_secs=60".to_string()][..]));
575        assert_eq!(modules.args("framebuffer"), Some(&["aspect_ratio=9:16".to_string()][..]));
576        assert_eq!(modules.args("magma"), Some(&["supported_vendors=0x13B5".to_string()][..]));
577        assert_eq!(modules.args("thermal"), Some(&["cooling=cpu,fcc=8".to_string()][..]));
578    }
579
580    #[fuchsia::test]
581    fn reject_empty_module_specification() {
582        let start_info = test_start_info(Vec::new(), vec!["   ".to_string()]);
583        assert!(parse_features(&start_info, &[]).is_err());
584    }
585
586    #[fuchsia::test]
587    fn disable_default_and_shard_modules() {
588        let start_info = test_start_info(
589            Vec::new(),
590            vec![
591                "-zram".to_string(),
592                "-gfxstream".to_string(),
593                "gfxstream".to_string(),
594                "ashmem".to_string(),
595                "-ashmem".to_string(),
596            ],
597        );
598
599        let (_features, modules) =
600            parse_features(&start_info, &[]).expect("parse_features should succeed");
601        assert!(modules.is_enabled("rtc"));
602        assert!(modules.is_enabled("loop"));
603        assert!(modules.is_enabled("power_residency"));
604        assert!(!modules.is_enabled("zram"));
605        assert_eq!(modules.args("zram"), None);
606        assert!(!modules.is_enabled("gfxstream"));
607        assert!(!modules.is_enabled("ashmem"));
608    }
609
610    #[fuchsia::test]
611    fn reject_invalid_disable_module_specifications() {
612        for invalid_spec in ["-", "--foo", "-zram --extra-arg", "-unknown_module"] {
613            let start_info = test_start_info(Vec::new(), vec![invalid_spec.to_string()]);
614            assert!(
615                parse_features(&start_info, &[]).is_err(),
616                "expected '{invalid_spec}' to be rejected"
617            );
618        }
619    }
620
621    #[fuchsia::test]
622    fn program_modules_override_legacy_feature_arguments() {
623        let start_info = test_start_info(
624            vec![
625                "boot_notifier".to_string(),
626                "boot_notifier_cpu_boost:45".to_string(),
627                "framebuffer".to_string(),
628                "aspect_ratio:16:9".to_string(),
629                "magma_supported_vendors:0x13B5".to_string(),
630                "perfetto:/dev/socket/old".to_string(),
631                "cooling:cpu".to_string(),
632            ],
633            vec![
634                "boot_notifier cpu_boost_secs=120".to_string(),
635                "framebuffer aspect_ratio=9:16".to_string(),
636                "magma supported_vendors=0x8086".to_string(),
637                "perfetto socket_path=/dev/socket/new".to_string(),
638                "thermal cooling=fcc=8".to_string(),
639            ],
640        );
641
642        let (_features, modules) =
643            parse_features(&start_info, &[]).expect("parse_features should succeed");
644        assert_eq!(modules.args("boot_notifier"), Some(&["cpu_boost_secs=120".to_string()][..]));
645        assert_eq!(modules.args("framebuffer"), Some(&["aspect_ratio=9:16".to_string()][..]));
646        assert_eq!(modules.args("magma"), Some(&["supported_vendors=0x8086".to_string()][..]));
647        assert_eq!(
648            modules.args("perfetto"),
649            Some(&["socket_path=/dev/socket/new".to_string()][..])
650        );
651        assert_eq!(modules.args("thermal"), Some(&["cooling=fcc=8".to_string()][..]));
652
653        let disabled_boot_notifier = test_start_info(
654            vec!["boot_notifier".to_string(), "boot_notifier_cpu_boost:45".to_string()],
655            vec!["-boot_notifier".to_string()],
656        );
657        let (_features, modules) = parse_features(&disabled_boot_notifier, &[])
658            .expect("disabling legacy boot_notifier via program.modules should succeed");
659        assert!(!modules.is_enabled("boot_notifier"));
660    }
661
662    #[fuchsia::test]
663    fn reject_unknown_modules_and_invalid_module_arguments() {
664        for invalid_spec in [
665            "unknown_module",
666            "ashmem unexpected",
667            "ashmem --unexpected",
668            "boot_notifier cpu_boost_secs=-1",
669            "boot_notifier --cpu-boost-secs=60",
670            "boot_notifier cpu-boost-secs=60",
671            "boot_notifier unknown_option=1",
672            "boot_notifier =60",
673            "framebuffer aspect_ratio=0:9",
674            "framebuffer aspect_ratio=9:0",
675            "framebuffer aspect_ratio=invalid",
676            "magma supported_vendors=not_hex",
677            "perfetto socket_path=",
678            "thermal cooling=",
679        ] {
680            let start_info = test_start_info(Vec::new(), vec![invalid_spec.to_string()]);
681            assert!(
682                parse_features(&start_info, &[]).is_err(),
683                "expected '{invalid_spec}' to be rejected"
684            );
685        }
686    }
687
688    #[fuchsia::test]
689    fn parse_devtmpfs_feature_and_mount_fallback() {
690        let default_info = test_start_info(Vec::new(), Vec::new());
691        let (features, _modules) =
692            parse_features(&default_info, &[]).expect("parse_features should succeed");
693        assert!(!features.kernel.devtmpfs);
694
695        let feature_info = test_start_info(vec!["devtmpfs".to_string()], Vec::new());
696        let (features, _modules) =
697            parse_features(&feature_info, &[]).expect("parse_features should succeed");
698        assert!(features.kernel.devtmpfs);
699
700        let mut program_mount_info = test_start_info(Vec::new(), Vec::new());
701        program_mount_info.program.mounts.push("/dev:devtmpfs::nosuid,relatime".to_string());
702        let (features, _modules) =
703            parse_features(&program_mount_info, &[]).expect("parse_features should succeed");
704        assert!(features.kernel.devtmpfs);
705
706        let mut additional_mount_info = test_start_info(Vec::new(), Vec::new());
707        additional_mount_info
708            .config
709            .additional_mounts
710            .push("/dev:devtmpfs::nosuid,relatime".to_string());
711        let (features, _modules) =
712            parse_features(&additional_mount_info, &[]).expect("parse_features should succeed");
713        assert!(features.kernel.devtmpfs);
714    }
715}