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