fw_cfg.rs22.54%
1
// Copyright 2024 Google LLC2
//3
// Licensed under the Apache License, Version 2.0 (the "License");4
// you may not use this file except in compliance with the License.5
// You may obtain a copy of the License at6
//7
// https://www.apache.org/licenses/LICENSE-2.08
//9
// Unless required by applicable law or agreed to in writing, software10
// distributed under the License is distributed on an "AS IS" BASIS,11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12
// See the License for the specific language governing permissions and13
// limitations under the License.14
15
#[cfg(target_arch = "x86_64")]16
pub mod acpi;17
18
use std::ffi::CString;19
use std::fmt;20
use std::fs::File;21
use std::io::{ErrorKind, Read, Result, Seek, SeekFrom};22
#[cfg(target_arch = "x86_64")]23
use std::mem::size_of;24
use std::mem::size_of_val;25
use std::os::unix::fs::FileExt;26
#[cfg(target_arch = "x86_64")]27
use std::path::Path;28
use std::sync::Arc;29
30
use alioth_macros::Layout;31
use bitfield::bitfield;32
use parking_lot::Mutex;33
use serde::de::{self, MapAccess, Visitor};34
use serde::{Deserialize, Deserializer};35
use serde_aco::Help;36
use zerocopy::{FromBytes, Immutable, IntoBytes};37
38
#[cfg(target_arch = "x86_64")]39
use crate::arch::layout::{40
PORT_FW_CFG_DATA, PORT_FW_CFG_DMA_HI, PORT_FW_CFG_DMA_LO, PORT_FW_CFG_SELECTOR,41
};42
use crate::device::{self, MmioDev, Pause};43
#[cfg(target_arch = "x86_64")]44
use crate::firmware::acpi::AcpiTable;45
#[cfg(target_arch = "x86_64")]46
use crate::loader::linux::bootparams::{47
BootE820Entry, BootParams, E820_ACPI, E820_PMEM, E820_RAM, E820_RESERVED,48
};49
use crate::mem;50
use crate::mem::emulated::{Action, Mmio};51
use crate::mem::mapped::RamBus;52
#[cfg(target_arch = "x86_64")]53
use crate::mem::{MemRegionEntry, MemRegionType};54
use crate::utils::endian::{Bu16, Bu32, Bu64, Lu16, Lu32, Lu64};55
56
#[cfg(target_arch = "x86_64")]57
use self::acpi::create_acpi_loader;58
59
pub const SELECTOR_WR: u16 = 1 << 14;60
61
pub const FW_CFG_SIGNATURE: u16 = 0x00;62
pub const FW_CFG_ID: u16 = 0x01;63
pub const FW_CFG_UUID: u16 = 0x02;64
pub const FW_CFG_RAM_SIZE: u16 = 0x03;65
pub const FW_CFG_NOGRAPHIC: u16 = 0x04;66
pub const FW_CFG_NB_CPUS: u16 = 0x05;67
pub const FW_CFG_MACHINE_ID: u16 = 0x06;68
pub const FW_CFG_KERNEL_ADDR: u16 = 0x07;69
pub const FW_CFG_KERNEL_SIZE: u16 = 0x08;70
pub const FW_CFG_KERNEL_CMDLINE: u16 = 0x09;71
pub const FW_CFG_INITRD_ADDR: u16 = 0x0a;72
pub const FW_CFG_INITRD_SIZE: u16 = 0x0b;73
pub const FW_CFG_BOOT_DEVICE: u16 = 0x0c;74
pub const FW_CFG_NUMA: u16 = 0x0d;75
pub const FW_CFG_BOOT_MENU: u16 = 0x0e;76
pub const FW_CFG_MAX_CPUS: u16 = 0x0f;77
pub const FW_CFG_KERNEL_ENTRY: u16 = 0x10;78
pub const FW_CFG_KERNEL_DATA: u16 = 0x11;79
pub const FW_CFG_INITRD_DATA: u16 = 0x12;80
pub const FW_CFG_CMDLINE_ADDR: u16 = 0x13;81
pub const FW_CFG_CMDLINE_SIZE: u16 = 0x14;82
pub const FW_CFG_CMDLINE_DATA: u16 = 0x15;83
pub const FW_CFG_SETUP_ADDR: u16 = 0x16;84
pub const FW_CFG_SETUP_SIZE: u16 = 0x17;85
pub const FW_CFG_SETUP_DATA: u16 = 0x18;86
pub const FW_CFG_FILE_DIR: u16 = 0x19;87
pub const FW_CFG_KNOWN_ITEMS: usize = 0x20;88
89
pub const FW_CFG_FILE_FIRST: u16 = 0x20;90
pub const FW_CFG_DMA_SIGNATURE: [u8; 8] = *b"QEMU CFG";91
pub const FW_CFG_FEATURE: [u8; 4] = [0b11, 0, 0, 0];92
93
pub const FILE_NAME_SIZE: usize = 56;94
95
fn create_file_name(name: &str) -> [u8; FILE_NAME_SIZE] {96
let mut c_name = [0u8; FILE_NAME_SIZE];97
let c_len = std::cmp::min(FILE_NAME_SIZE - 1, name.len());98
c_name[0..c_len].copy_from_slice(&name.as_bytes()[0..c_len]);99
c_name100
}101
102
#[derive(Debug)]103
pub enum FwCfgContent {104
Bytes(Vec<u8>),105
Slice(&'static [u8]),106
File(u64, File),107
Lu16(Lu16),108
Lu32(Lu32),109
Lu64(Lu64),110
}111
112
struct FwCfgContentAccess<'a> {113
content: &'a FwCfgContent,114
offset: u32,115
}116
117
impl Read for FwCfgContentAccess<'_> {118
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {12x119
match self.content {12x120
FwCfgContent::File(offset, f) => {2x121
Seek::seek(&mut (&*f), SeekFrom::Start(offset + self.offset as u64))?;2x122
Read::read(&mut (&*f), buf)2x123
}124
FwCfgContent::Bytes(b) => match b.get(self.offset as usize..) {2x125
Some(mut s) => s.read(buf),1x126
None => Err(ErrorKind::UnexpectedEof)?,1x127
},128
FwCfgContent::Slice(b) => match b.get(self.offset as usize..) {2x129
Some(mut s) => s.read(buf),1x130
None => Err(ErrorKind::UnexpectedEof)?,1x131
},132
FwCfgContent::Lu16(n) => match n.as_bytes().get(self.offset as usize..) {2x133
Some(mut s) => s.read(buf),2x134
None => Err(ErrorKind::UnexpectedEof)?,135
},136
FwCfgContent::Lu32(n) => match n.as_bytes().get(self.offset as usize..) {2x137
Some(mut s) => s.read(buf),1x138
None => Err(ErrorKind::UnexpectedEof)?,1x139
},140
FwCfgContent::Lu64(n) => match n.as_bytes().get(self.offset as usize..) {2x141
Some(mut s) => s.read(buf),2x142
None => Err(ErrorKind::UnexpectedEof)?,143
},144
}145
}12x146
}147
148
impl Default for FwCfgContent {149
fn default() -> Self {3x150
FwCfgContent::Slice(&[])3x151
}3x152
}153
154
impl FwCfgContent {155
fn size(&self) -> Result<u32> {7x156
let ret = match self {7x157
FwCfgContent::Bytes(v) => v.len(),1x158
FwCfgContent::File(offset, f) => (f.metadata()?.len() - offset) as usize,1x159
FwCfgContent::Slice(s) => s.len(),2x160
FwCfgContent::Lu16(n) => size_of_val(n),1x161
FwCfgContent::Lu32(n) => size_of_val(n),1x162
FwCfgContent::Lu64(n) => size_of_val(n),1x163
};164
u32::try_from(ret).map_err(|_| std::io::ErrorKind::InvalidInput.into())7x165
}7x166
167
fn access(&self, offset: u32) -> FwCfgContentAccess<'_> {12x168
FwCfgContentAccess {12x169
content: self,12x170
offset,12x171
}12x172
}12x173
174
fn read(&self, offset: u32) -> Option<u8> {8x175
match self {8x176
FwCfgContent::Bytes(b) => b.get(offset as usize).copied(),1x177
FwCfgContent::Slice(s) => s.get(offset as usize).copied(),2x178
FwCfgContent::File(o, f) => {2x179
let mut buf = [0u8];2x180
match f.read_exact_at(&mut buf, o + offset as u64) {2x181
Ok(_) => Some(buf[0]),1x182
Err(e) => {1x183
log::error!("fw_cfg: reading {f:?}: {e:?}");1x184
None1x185
}186
}187
}188
FwCfgContent::Lu16(n) => n.as_bytes().get(offset as usize).copied(),1x189
FwCfgContent::Lu32(n) => n.as_bytes().get(offset as usize).copied(),1x190
FwCfgContent::Lu64(n) => n.as_bytes().get(offset as usize).copied(),1x191
}192
}8x193
}194
195
#[derive(Debug, Default)]196
pub struct FwCfgItem {197
pub name: String,198
pub content: FwCfgContent,199
}200
201
/// https://www.qemu.org/docs/master/specs/fw_cfg.html202
#[derive(Debug)]203
pub struct FwCfg {204
selector: u16,205
data_offset: u32,206
dma_address: u64,207
items: Vec<FwCfgItem>, // 0x20 and above208
known_items: [FwCfgContent; FW_CFG_KNOWN_ITEMS], // 0x0 to 0x19209
memory: Arc<RamBus>,210
}211
212
#[repr(C)]213
#[derive(Debug, IntoBytes, FromBytes, Immutable, Layout)]214
struct FwCfgDmaAccess {215
control: Bu32,216
length: Bu32,217
address: Bu64,218
}219
220
bitfield! {221
struct AccessControl(u32);222
impl Debug;223
error, set_error: 0;224
read, _: 1;225
skip, _: 2;226
select, _ : 3;227
write, _ :4;228
selector, _: 31, 16;229
}230
231
#[repr(C)]232
#[derive(Debug, IntoBytes, Immutable)]233
struct FwCfgFilesHeader {234
count: Bu32,235
}236
237
#[repr(C)]238
#[derive(Debug, IntoBytes, Immutable)]239
struct FwCfgFile {240
size: Bu32,241
select: Bu16,242
_reserved: u16,243
name: [u8; FILE_NAME_SIZE],244
}245
246
impl FwCfg {247
pub fn new(memory: Arc<RamBus>, items: Vec<FwCfgItem>) -> Result<Self> {248
const DEFAULT_ITEM: FwCfgContent = FwCfgContent::Slice(&[]);249
let mut known_items = [DEFAULT_ITEM; FW_CFG_KNOWN_ITEMS];250
known_items[FW_CFG_SIGNATURE as usize] = FwCfgContent::Slice(&FW_CFG_DMA_SIGNATURE);251
known_items[FW_CFG_ID as usize] = FwCfgContent::Slice(&FW_CFG_FEATURE);252
let file_buf = Vec::from(FwCfgFilesHeader { count: 0.into() }.as_bytes());253
known_items[FW_CFG_FILE_DIR as usize] = FwCfgContent::Bytes(file_buf);254
255
let mut dev = Self {256
selector: 0,257
data_offset: 0,258
dma_address: 0,259
memory,260
items: vec![],261
known_items,262
};263
for item in items {264
dev.add_item(item)?;265
}266
Ok(dev)267
}268
269
fn get_file_dir_mut(&mut self) -> &mut Vec<u8> {270
let FwCfgContent::Bytes(file_buf) = &mut self.known_items[FW_CFG_FILE_DIR as usize] else {271
unreachable!("fw_cfg: selector {FW_CFG_FILE_DIR:#x} should be FwCfgContent::Byte!")272
};273
file_buf274
}275
276
fn update_count(&mut self) {277
let header = FwCfgFilesHeader {278
count: (self.items.len() as u32).into(),279
};280
self.get_file_dir_mut()[0..4].copy_from_slice(header.as_bytes());281
}282
283
pub fn add_ram_size(&mut self, size: u64) {284
self.known_items[FW_CFG_RAM_SIZE as usize] = FwCfgContent::Lu64(size.into());285
}286
287
pub fn add_cpu_count(&mut self, count: u16) {288
self.known_items[FW_CFG_NB_CPUS as usize] = FwCfgContent::Lu16(count.into());289
}290
291
#[cfg(target_arch = "x86_64")]292
pub(crate) fn add_e820(&mut self, mem_regions: &[(u64, MemRegionEntry)]) -> Result<()> {293
let mut bytes = vec![];294
for (addr, region) in mem_regions.iter() {295
let type_ = match region.type_ {296
MemRegionType::Ram => E820_RAM,297
MemRegionType::Reserved => E820_RESERVED,298
MemRegionType::Acpi => E820_ACPI,299
MemRegionType::Pmem => E820_PMEM,300
MemRegionType::Hidden => continue,301
};302
let entry = BootE820Entry {303
addr: *addr,304
size: region.size,305
type_,306
};307
bytes.extend_from_slice(entry.as_bytes());308
}309
let item = FwCfgItem {310
name: "etc/e820".to_owned(),311
content: FwCfgContent::Bytes(bytes),312
};313
self.add_item(item)314
}315
316
#[cfg(target_arch = "x86_64")]317
pub(crate) fn add_acpi(&mut self, acpi_table: AcpiTable) -> Result<()> {318
let [table_loader, acpi_rsdp, apci_tables] = create_acpi_loader(acpi_table);319
self.add_item(table_loader)?;320
self.add_item(acpi_rsdp)?;321
self.add_item(apci_tables)322
}323
324
#[cfg(target_arch = "x86_64")]325
pub fn add_kernel_data(&mut self, p: &Path) -> Result<()> {326
let file = File::open(p)?;327
let mut buffer = vec![0u8; size_of::<BootParams>()];328
file.read_exact_at(&mut buffer, 0)?;329
let bp = BootParams::mut_from_bytes(&mut buffer).unwrap();330
if bp.hdr.setup_sects == 0 {331
bp.hdr.setup_sects = 4;332
}333
bp.hdr.type_of_loader = 0xff;334
let kernel_start = (bp.hdr.setup_sects as usize + 1) * 512;335
self.known_items[FW_CFG_SETUP_SIZE as usize] =336
FwCfgContent::Lu32((buffer.len() as u32).into());337
self.known_items[FW_CFG_SETUP_DATA as usize] = FwCfgContent::Bytes(buffer);338
self.known_items[FW_CFG_KERNEL_SIZE as usize] =339
FwCfgContent::Lu32((file.metadata()?.len() as u32 - kernel_start as u32).into());340
self.known_items[FW_CFG_KERNEL_DATA as usize] =341
FwCfgContent::File(kernel_start as u64, file);342
Ok(())343
}344
345
pub fn add_initramfs_data(&mut self, p: &Path) -> Result<()> {346
let file = File::open(p)?;347
let initramfs_size = file.metadata()?.len() as u32;348
self.known_items[FW_CFG_INITRD_SIZE as usize] = FwCfgContent::Lu32(initramfs_size.into());349
self.known_items[FW_CFG_INITRD_DATA as usize] = FwCfgContent::File(0, file);350
Ok(())351
}352
353
pub fn add_kernel_cmdline(&mut self, s: CString) {354
let bytes = s.into_bytes_with_nul();355
self.known_items[FW_CFG_CMDLINE_SIZE as usize] =356
FwCfgContent::Lu32((bytes.len() as u32).into());357
self.known_items[FW_CFG_CMDLINE_DATA as usize] = FwCfgContent::Bytes(bytes);358
}359
360
pub fn add_item(&mut self, item: FwCfgItem) -> Result<()> {361
let index = self.items.len();362
let c_name = create_file_name(&item.name);363
let size = item.content.size()?;364
let cfg_file = FwCfgFile {365
size: size.into(),366
select: (FW_CFG_FILE_FIRST + index as u16).into(),367
_reserved: 0,368
name: c_name,369
};370
self.get_file_dir_mut()371
.extend_from_slice(cfg_file.as_bytes());372
self.items.push(item);373
self.update_count();374
Ok(())375
}376
377
fn dma_read_content(378
&self,379
content: &FwCfgContent,380
offset: u32,381
len: u32,382
address: u64,383
) -> Result<u32> {384
let content_size = content.size()?.saturating_sub(offset);385
let op_size = std::cmp::min(content_size, len);386
let r = self387
.memory388
.write_range(address, op_size as u64, content.access(offset));389
match r {390
Err(e) => {391
log::error!("fw_cfg: dam read error: {e:x?}");392
Err(ErrorKind::InvalidInput.into())393
}394
Ok(()) => Ok(op_size),395
}396
}397
398
fn dma_read(&mut self, selector: u16, len: u32, address: u64) -> Result<()> {399
let op_size = if let Some(content) = self.known_items.get(selector as usize) {400
self.dma_read_content(content, self.data_offset, len, address)401
} else if let Some(item) = self.items.get((selector - FW_CFG_FILE_FIRST) as usize) {402
self.dma_read_content(&item.content, self.data_offset, len, address)403
} else {404
log::error!("fw_cfg: selector {selector:#x} does not exist.");405
Err(ErrorKind::NotFound.into())406
}?;407
self.data_offset += op_size;408
Ok(())409
}410
411
fn dma_write(&self, _selector: u16, _len: u32, _address: u64) -> Result<()> {412
unimplemented!()413
}414
415
fn do_dma(&mut self) {416
let dma_address = self.dma_address;417
let dma_access: FwCfgDmaAccess = match self.memory.read_t(dma_address) {418
Ok(access) => access,419
Err(e) => {420
log::error!("fw_cfg: invalid address of dma access {dma_address:#x}: {e:?}");421
return;422
}423
};424
let control = AccessControl(dma_access.control.into());425
if control.select() {426
self.selector = control.select() as u16;427
}428
let len = dma_access.length.to_ne();429
let addr = dma_access.address.to_ne();430
let ret = if control.read() {431
self.dma_read(self.selector, len, addr)432
} else if control.write() {433
self.dma_write(self.selector, len, addr)434
} else if control.skip() {435
self.data_offset += len;436
Ok(())437
} else {438
Err(ErrorKind::InvalidData.into())439
};440
let mut access_resp = AccessControl(0);441
if let Err(e) = ret {442
log::error!("fw_cfg: dma operation {dma_access:x?}: {e:x?}");443
access_resp.set_error(true);444
}445
if let Err(e) = self.memory.write_t(446
dma_address + FwCfgDmaAccess::OFFSET_CONTROL as u64,447
&Bu32::from(access_resp.0),448
) {449
log::error!("fw_cfg: finishing dma: {e:?}")450
}451
}452
453
fn read_data(&mut self) -> u8 {454
let ret = if let Some(content) = self.known_items.get(self.selector as usize) {455
content.read(self.data_offset)456
} else if let Some(item) = self.items.get((self.selector - FW_CFG_FILE_FIRST) as usize) {457
item.content.read(self.data_offset)458
} else {459
log::error!("fw_cfg: selector {:#x} does not exist.", self.selector);460
None461
};462
if let Some(val) = ret {463
self.data_offset += 1;464
val465
} else {466
0467
}468
}469
470
fn write_data(&self, _val: u8) {471
if self.selector & SELECTOR_WR != SELECTOR_WR {472
log::error!("fw_cfg: data is read only");473
return;474
}475
log::warn!("fw_cfg: write data no op.")476
}477
}478
479
impl Mmio for Mutex<FwCfg> {480
fn size(&self) -> u64 {481
16482
}483
484
fn read(&self, offset: u64, size: u8) -> mem::Result<u64> {485
let mut fw_cfg = self.lock();486
let port = offset as u16 + PORT_FW_CFG_SELECTOR;487
let ret = match (port, size) {488
(PORT_FW_CFG_SELECTOR, _) => {489
log::error!("fw_cfg: selector registerīis write-only.");490
0491
}492
(PORT_FW_CFG_DATA, 1) => fw_cfg.read_data() as u64,493
(PORT_FW_CFG_DMA_HI, 4) => {494
let addr = fw_cfg.dma_address;495
let addr_hi = (addr >> 32) as u32;496
addr_hi.to_be() as u64497
}498
(PORT_FW_CFG_DMA_LO, 4) => {499
let addr = fw_cfg.dma_address;500
let addr_lo = (addr & 0xffff_ffff) as u32;501
addr_lo.to_be() as u64502
}503
_ => {504
log::error!("fw_cfg: read unknown port {port:#x} with size {size}.");505
0506
}507
};508
Ok(ret)509
}510
511
fn write(&self, offset: u64, size: u8, val: u64) -> mem::Result<Action> {512
let mut fw_cfg = self.lock();513
let port = offset as u16 + PORT_FW_CFG_SELECTOR;514
match (port, size) {515
(PORT_FW_CFG_SELECTOR, 2) => {516
fw_cfg.selector = val as u16;517
fw_cfg.data_offset = 0;518
}519
(PORT_FW_CFG_DATA, 1) => fw_cfg.write_data(val as u8),520
(PORT_FW_CFG_DMA_HI, 4) => {521
fw_cfg.dma_address &= 0xffff_ffff;522
fw_cfg.dma_address |= (u32::from_be(val as u32) as u64) << 32;523
}524
(PORT_FW_CFG_DMA_LO, 4) => {525
fw_cfg.dma_address &= !0xffff_ffff;526
fw_cfg.dma_address |= u32::from_be(val as u32) as u64;527
fw_cfg.do_dma();528
}529
_ => log::error!(530
"fw_cfg: write 0x{val:0width$x} to unknown port {port:#x}.",531
width = 2 * size as usize,532
),533
};534
Ok(Action::None)535
}536
}537
538
impl Pause for Mutex<FwCfg> {539
fn pause(&self) -> device::Result<()> {540
Ok(())541
}542
543
fn resume(&self) -> device::Result<()> {544
Ok(())545
}546
}547
548
impl MmioDev for Mutex<FwCfg> {}549
550
#[derive(Debug, PartialEq, Eq, Deserialize, Help)]551
pub enum FwCfgContentParam {552
/// Path to a file with binary contents.553
#[serde(alias = "file")]554
File(Box<Path>),555
/// A UTF-8 encoded string.556
#[serde(alias = "string")]557
String(String),558
}559
560
#[derive(Debug, PartialEq, Eq, Help)]561
pub struct FwCfgItemParam {562
/// Selector key of an item.563
pub name: String,564
/// Item content.565
#[serde_aco(flatten)]566
pub content: FwCfgContentParam,567
}568
569
impl<'de> Deserialize<'de> for FwCfgItemParam {570
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>2x571
where2x572
D: Deserializer<'de>,2x573
{574
#[derive(Deserialize)]575
#[serde(field_identifier, rename_all = "lowercase")]576
enum Field {577
Name,578
File,579
String,580
}581
582
struct ParamVisitor;583
584
impl<'de> Visitor<'de> for ParamVisitor {585
type Value = FwCfgItemParam;586
587
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {588
formatter.write_str("struct FwCfgItemParam")589
}590
591
fn visit_map<V>(self, mut map: V) -> std::result::Result<Self::Value, V::Error>2x592
where2x593
V: MapAccess<'de>,2x594
{595
let mut name = None;2x596
let mut content = None;2x597
while let Some(key) = map.next_key()? {6x598
match key {4x599
Field::Name => {600
if name.is_some() {2x601
return Err(de::Error::duplicate_field("file"));602
}2x603
name = Some(map.next_value()?);2x604
}605
Field::String => {606
if content.is_some() {1x607
return Err(de::Error::duplicate_field("string,file"));608
}1x609
content = Some(FwCfgContentParam::String(map.next_value()?));1x610
}611
Field::File => {612
if content.is_some() {1x613
return Err(de::Error::duplicate_field("string,file"));614
}1x615
content = Some(FwCfgContentParam::File(map.next_value()?));1x616
}617
}618
}619
let name = name.ok_or_else(|| de::Error::missing_field("name"))?;2x620
let content = content.ok_or_else(|| de::Error::missing_field("file,string"))?;2x621
Ok(FwCfgItemParam { name, content })2x622
}2x623
}624
625
const FIELDS: &[&str] = &["name", "file", "string"];626
deserializer.deserialize_struct("FwCfgItemParam", FIELDS, ParamVisitor)2x627
}2x628
}629
630
impl FwCfgItemParam {631
pub fn build(self) -> Result<FwCfgItem> {632
match self.content {633
FwCfgContentParam::File(file) => {634
let f = File::open(file)?;635
Ok(FwCfgItem {636
name: self.name,637
content: FwCfgContent::File(0, f),638
})639
}640
FwCfgContentParam::String(string) => Ok(FwCfgItem {641
name: self.name,642
content: FwCfgContent::Bytes(string.into()),643
}),644
}645
}646
}647
648
#[cfg(test)]649
#[path = "fw_cfg_test.rs"]650
mod tests;651