1use 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#[derive(Default, Debug)]
19pub struct Features {
20 pub kernel: KernelFeatures,
22
23 pub selinux: SELinuxFeature,
25
26 pub system_limits: SystemLimits,
28
29 pub enable_visual_debugging: bool,
32
33 pub initial_view_id_annotation: String,
35
36 pub container: bool,
38
39 pub test_data: bool,
41
42 pub custom_artifacts: bool,
44
45 pub android_serialno: bool,
47
48 pub rootfs_rw: bool,
50
51 pub enable_utc_time_adjustment: bool,
52
53 pub android_bootreason: bool,
55
56 pub additional_mounts: Option<Vec<String>>,
57
58 pub mmcblk_stub: bool,
61
62 pub unified_tracing: bool,
64}
65
66#[derive(Default, Debug, PartialEq)]
67pub struct SELinuxFeature {
68 pub enabled: bool,
70
71 pub options: String,
73
74 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
172pub 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
374pub 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
385pub 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}