use anyhow::{bail, Context, Result};
use bytes::Buf;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::{copy, BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use crate::io::*;
use crate::iso9660::{self, IsoFs};
pub(super) const INITRD_IGNITION_PATH: &str = "config.ign";
pub(super) const INITRD_NETWORK_DIR: &str = "etc/coreos-firstboot-network";
lazy_static! {
pub(super) static ref INITRD_IGNITION_GLOB: GlobMatcher =
GlobMatcher::new(&[INITRD_IGNITION_PATH]).unwrap();
pub(super) static ref INITRD_NETWORK_GLOB: GlobMatcher =
GlobMatcher::new(&[&format!("{INITRD_NETWORK_DIR}/*")]).unwrap();
}
const COREOS_IGNINFO_PATH: &str = "COREOS/IGNINFO.JSO";
const COREOS_INITRD_DEFAULT_EMBED_PATH: &str = "IMAGES/IGNITION.IMG";
const COREOS_INITRD_HEADER_SIZE: u64 = 24;
const COREOS_KARG_EMBED_AREA_HEADER_MAGIC: &[u8] = b"coreKarg";
const COREOS_KARG_EMBED_AREA_HEADER_SIZE: u64 = 72;
const COREOS_KARG_EMBED_AREA_HEADER_MAX_OFFSETS: usize = 6;
const COREOS_KARG_EMBED_AREA_MAX_SIZE: usize = 2048;
const COREOS_KARG_EMBED_INFO_PATH: &str = "COREOS/KARGS.JSO";
pub(super) struct IsoConfig {
initrd: InitrdEmbedArea,
kargs: Option<KargEmbedAreas>,
}
impl IsoConfig {
pub fn for_file(file: &mut File) -> Result<Self> {
let mut iso = IsoFs::from_file(file.try_clone().context("cloning file")?)
.context("parsing ISO9660 image")?;
IsoConfig::for_iso(&mut iso)
}
pub fn for_iso(iso: &mut IsoFs) -> Result<Self> {
Ok(Self {
initrd: InitrdEmbedArea::for_iso(iso).context("Unrecognized CoreOS ISO image.")?,
kargs: KargEmbedAreas::for_iso(iso)?,
})
}
pub fn have_ignition(&self) -> bool {
self.initrd().get(INITRD_IGNITION_PATH).is_some()
}
pub fn have_network(&self) -> bool {
!self.initrd().find(&INITRD_NETWORK_GLOB).is_empty()
}
pub fn remove_network(&mut self) {
let initrd = self.initrd_mut();
let paths: Vec<String> = initrd
.find(&INITRD_NETWORK_GLOB)
.keys()
.map(|p| p.to_string())
.collect();
for path in paths {
initrd.remove(&path);
}
}
pub fn initrd(&self) -> &Initrd {
self.initrd.initrd()
}
pub fn initrd_mut(&mut self) -> &mut Initrd {
self.initrd.initrd_mut()
}
pub fn initrd_header_json(&self) -> Result<Vec<u8>> {
let mut ret =
serde_json::to_vec_pretty(&self.initrd).context("failed to serialize initrd header")?;
ret.push(b'\n');
Ok(ret)
}
pub fn kargs(&self) -> Result<&str> {
Ok(self.unwrap_kargs()?.kargs())
}
pub fn kargs_default(&self) -> Result<&str> {
Ok(self.unwrap_kargs()?.kargs_default())
}
pub fn set_kargs(&mut self, kargs: &str) -> Result<()> {
self.unwrap_kargs_mut()?.set_kargs(kargs)
}
pub fn kargs_supported(&self) -> bool {
self.kargs.is_some()
}
pub fn kargs_header_json(&self) -> Result<Vec<u8>> {
let mut ret =
serde_json::to_vec_pretty(&self.kargs).context("failed to serialize kargs header")?;
ret.push(b'\n');
Ok(ret)
}
fn unwrap_kargs(&self) -> Result<&KargEmbedAreas> {
self.kargs
.as_ref()
.context("No karg embed areas found; old or corrupted CoreOS ISO image.")
}
fn unwrap_kargs_mut(&mut self) -> Result<&mut KargEmbedAreas> {
self.kargs
.as_mut()
.context("No karg embed areas found; old or corrupted CoreOS ISO image.")
}
pub fn write(&self, file: &mut File) -> Result<()> {
self.initrd.write(file)?;
if let Some(kargs) = &self.kargs {
kargs.write(file)?;
}
Ok(())
}
pub fn stream(&self, input: &mut File, writer: &mut (impl Write + ?Sized)) -> Result<()> {
let initrd_region = self.initrd.region()?;
let mut regions = vec![&initrd_region];
if let Some(kargs) = &self.kargs {
regions.extend(kargs.regions.iter())
}
regions.stream(input, writer)
}
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
struct Region {
pub offset: u64,
pub length: usize,
#[serde(skip_serializing)]
pub contents: Vec<u8>,
#[serde(skip_serializing)]
pub modified: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pad: Option<char>,
#[serde(skip_serializing_if = "Option::is_none")]
end: Option<char>,
}
impl Region {
pub fn read(
file: &mut File,
offset: u64,
length: usize,
pad: Option<char>,
end: Option<char>,
) -> Result<Self> {
let mut contents = vec![0; length];
file.seek(SeekFrom::Start(offset))
.with_context(|| format!("seeking to offset {offset}"))?;
file.read_exact(&mut contents)
.with_context(|| format!("reading {length} bytes at {offset}"))?;
Ok(Self {
offset,
length,
contents,
modified: false,
pad,
end,
})
}
pub fn write(&self, file: &mut File) -> Result<()> {
self.validate()?;
if self.modified {
file.seek(SeekFrom::Start(self.offset))
.with_context(|| format!("seeking to offset {}", self.offset))?;
file.write_all(&self.contents)
.with_context(|| format!("writing {} bytes at {}", self.length, self.offset))?;
}
Ok(())
}
pub fn validate(&self) -> Result<()> {
if self.length != self.contents.len() {
bail!(
"expected region contents length {}, found {}",
self.length,
self.contents.len()
);
}
Ok(())
}
}
trait Stream {
fn stream(&self, input: &mut File, writer: &mut (impl Write + ?Sized)) -> Result<()>;
}
impl Stream for [&Region] {
fn stream(&self, input: &mut File, writer: &mut (impl Write + ?Sized)) -> Result<()> {
input.rewind().context("seeking to start")?;
let mut regions: Vec<&&Region> = self.iter().filter(|r| r.modified).collect();
regions.sort_unstable();
let mut buf = [0u8; BUFFER_SIZE];
let mut cursor: u64 = 0;
for region in ®ions {
region.validate()?;
if region.offset < cursor {
bail!(
"region starting at {} precedes current offset {}",
region.offset,
cursor
);
}
cursor = region.offset + region.length as u64;
}
cursor = 0;
for region in ®ions {
assert!(region.offset >= cursor);
copy_exactly_n(input, writer, region.offset - cursor, &mut buf)
.with_context(|| format!("copying bytes from {} to {}", cursor, region.offset))?;
writer.write_all(®ion.contents).with_context(|| {
format!(
"writing region for {} at offset {}",
region.length, region.offset
)
})?;
cursor = input
.seek(SeekFrom::Current(region.length as i64))
.with_context(|| format!("seeking region length {}", region.length))?;
}
let mut write_buf = BufWriter::with_capacity(BUFFER_SIZE, writer);
copy(
&mut BufReader::with_capacity(BUFFER_SIZE, input),
&mut write_buf,
)
.context("copying file")?;
write_buf.flush().context("flushing output")?;
Ok(())
}
}
#[derive(Serialize)]
struct KargEmbedAreas {
length: usize,
default: String,
#[serde(rename = "kargs")]
regions: Vec<Region>,
#[serde(skip_serializing)]
args: String,
}
#[derive(Deserialize, Serialize)]
struct KargEmbedInfo {
default: String,
files: Vec<KargEmbedLocation>,
size: usize,
}
#[derive(Deserialize, Serialize)]
struct KargEmbedLocation {
path: String,
offset: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pad: Option<char>,
#[serde(skip_serializing_if = "Option::is_none")]
end: Option<char>,
}
impl KargEmbedInfo {
pub fn for_iso(iso: &mut IsoFs) -> Result<Option<Self>> {
let iso_file = match iso.get_path(COREOS_KARG_EMBED_INFO_PATH) {
Ok(record) => record.try_into_file()?,
Err(e) if e.is::<iso9660::NotFound>() => return Ok(None),
Err(e) => return Err(e),
};
let info: KargEmbedInfo = serde_json::from_reader(
iso.read_file(&iso_file)
.context("reading kargs embed area info")?,
)
.context("decoding kargs embed area info")?;
Ok(Some(info))
}
pub fn update_iso(&self, iso: &mut IsoFs) -> Result<()> {
let iso_file = iso.get_path(COREOS_KARG_EMBED_INFO_PATH)?.try_into_file()?;
let mut w = iso.overwrite_file(&iso_file)?;
let new_json = serde_json::to_string_pretty(&self).context("serializing object")?;
if new_json.len() > iso_file.length as usize {
bail!(
"New version of {} does not fit in space ({} vs {})",
COREOS_KARG_EMBED_INFO_PATH,
new_json.len(),
iso_file.length,
);
}
let mut contents = vec![b' '; iso_file.length as usize];
contents[..new_json.len()].copy_from_slice(new_json.as_bytes());
w.write_all(&contents)
.with_context(|| format!("failed to update {COREOS_KARG_EMBED_INFO_PATH}"))?;
w.flush().context("flushing ISO")?;
Ok(())
}
}
impl KargEmbedAreas {
pub fn for_iso(iso: &mut IsoFs) -> Result<Option<Self>> {
let info = match KargEmbedInfo::for_iso(iso)? {
Some(info) => info,
None => return Self::for_file_via_system_area(iso.as_file()?),
};
if info.size > COREOS_KARG_EMBED_AREA_MAX_SIZE {
bail!(
"karg embed area size larger than {} (found {})",
COREOS_KARG_EMBED_AREA_MAX_SIZE,
info.size
);
}
if info.default.len() > info.size {
bail!(
"default kargs size {} larger than embed areas ({})",
info.default.len(),
info.size
);
}
let mut regions = Vec::new();
for loc in info.files {
let iso_file = iso
.get_path(&loc.path.to_uppercase())
.with_context(|| format!("looking up '{}'", loc.path))?
.try_into_file()?;
regions.push(
Region::read(
iso.as_file()?,
iso_file.address.as_offset() + loc.offset,
info.size,
loc.pad,
loc.end,
)
.context("reading kargs embed area")?,
);
}
regions.sort_unstable_by_key(|r| r.offset);
Some(Self::build(info.size, info.default, regions)).transpose()
}
fn for_file_via_system_area(file: &mut File) -> Result<Option<Self>> {
let region = Region::read(
file,
32768 - COREOS_INITRD_HEADER_SIZE - COREOS_KARG_EMBED_AREA_HEADER_SIZE,
COREOS_KARG_EMBED_AREA_HEADER_SIZE as usize,
None,
None,
)
.context("reading karg embed header")?;
let mut header = ®ion.contents[..];
if header.copy_to_bytes(8) != COREOS_KARG_EMBED_AREA_HEADER_MAGIC {
return Ok(None);
}
let length: usize = header
.get_u64_le()
.try_into()
.context("karg embed area length too large to allocate")?;
if length > COREOS_KARG_EMBED_AREA_MAX_SIZE {
bail!(
"karg embed area length larger than {} (found {})",
COREOS_KARG_EMBED_AREA_MAX_SIZE,
length
);
}
let offset = header.get_u64_le();
let default_region =
Region::read(file, offset, length, None, None).context("reading default kargs")?;
let default = Self::parse(&default_region)?;
let mut regions = Vec::new();
while regions.len() < COREOS_KARG_EMBED_AREA_HEADER_MAX_OFFSETS {
let offset = header.get_u64_le();
if offset == 0 {
break;
}
regions.push(
Region::read(file, offset, length, None, None)
.context("reading kargs embed area")?,
);
}
Some(Self::build(length, default, regions)).transpose()
}
fn build(length: usize, default: String, regions: Vec<Region>) -> Result<Self> {
if regions.is_empty() {
bail!("No karg embed areas found; corrupted CoreOS ISO image.");
}
let args = Self::parse(®ions[0])?;
for region in regions.iter().skip(1) {
let current_args = Self::parse(region)?;
if current_args != args {
bail!(
"kargs don't match at all offsets! (expected '{}', but offset {} has: '{}')",
args,
region.offset,
current_args
);
}
}
Ok(Self {
length,
default,
regions,
args,
})
}
fn parse(region: &Region) -> Result<String> {
Ok(String::from_utf8(region.contents.clone())
.context("invalid UTF-8 in karg area")?
.trim_end_matches(region.pad.unwrap_or('#'))
.trim_end_matches(region.end.unwrap_or('\n'))
.trim()
.into())
}
pub fn kargs_default(&self) -> &str {
&self.default
}
pub fn kargs(&self) -> &str {
&self.args
}
pub fn set_kargs(&mut self, kargs: &str) -> Result<()> {
let unformatted = kargs.trim();
if unformatted.len() >= self.length {
bail!(
"kargs too large for area: {} vs {}",
unformatted.len() + 1,
self.length
);
}
for region in &mut self.regions {
let mut formatted = unformatted.to_string();
formatted.push(region.end.unwrap_or('\n'));
let pad = region.pad.unwrap_or('#');
let mut contents = vec![pad as u8; self.length];
contents[..formatted.len()].copy_from_slice(formatted.as_bytes());
region.contents = contents.clone();
region.modified = true;
}
self.args = unformatted.to_string();
Ok(())
}
pub fn write(&self, file: &mut File) -> Result<()> {
for region in &self.regions {
region.write(file)?;
}
Ok(())
}
}
#[derive(Debug, Serialize)]
struct InitrdEmbedArea {
#[serde(flatten)]
region: Region,
#[serde(skip)]
initrd: Initrd,
}
#[derive(Deserialize)]
struct IgnInfo {
file: String,
offset: Option<u64>,
length: Option<usize>,
}
impl InitrdEmbedArea {
pub fn for_iso(iso: &mut IsoFs) -> Result<Self> {
let igninfo: IgnInfo = match iso.get_path(COREOS_IGNINFO_PATH) {
Ok(record) => {
let f = record.try_into_file()?;
serde_json::from_reader(iso.read_file(&f).context("reading igninfo")?)
.context("decoding igninfo")?
}
Err(e) if e.is::<iso9660::NotFound>() => IgnInfo {
file: COREOS_INITRD_DEFAULT_EMBED_PATH.to_string(),
offset: None,
length: None,
},
Err(e) => return Err(e),
};
let f = iso
.get_path(&igninfo.file.to_uppercase())
.context("finding initrd embed area")?
.try_into_file()?;
let file_offset = igninfo.offset.unwrap_or(0);
let iso_offset = f.address.as_offset() + file_offset;
let length = igninfo
.length
.unwrap_or(f.length as usize - file_offset as usize);
let mut region = Region::read(iso.as_file()?, iso_offset, length, None, None)
.context("reading initrd embed area")?;
let initrd = if region.contents.iter().any(|v| *v != 0) {
Initrd::from_reader(&*region.contents).context("decoding initrd embed area")?
} else {
Initrd::default()
};
region.contents = Vec::new();
Ok(Self { region, initrd })
}
pub fn initrd(&self) -> &Initrd {
&self.initrd
}
pub fn initrd_mut(&mut self) -> &mut Initrd {
self.region.modified = true;
&mut self.initrd
}
pub fn write(&self, file: &mut File) -> Result<()> {
self.region()?.write(file)
}
pub fn region(&self) -> Result<Region> {
let mut region = self.region.clone();
let capacity = region.length;
let mut data = if !self.initrd().is_empty() {
self.initrd().to_bytes()?
} else {
Vec::new()
};
if data.len() > capacity {
bail!(
"Compressed initramfs is too large: {} > {}",
data.len(),
capacity
)
}
data.extend(std::iter::repeat_n(0, capacity - data.len()));
region.contents = data;
Ok(region)
}
}
pub(super) fn set_default_kargs(iso: &mut IsoFs, default: String) -> Result<()> {
let mut kargs_info = KargEmbedInfo::for_iso(iso)?.context(
"minimal ISO does not have kargs.json; please report this as a bug",
)?;
kargs_info.default = default;
kargs_info.update_iso(iso)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::copy;
use tempfile::tempfile;
use xz2::read::XzDecoder;
fn open_iso_file() -> File {
let iso_bytes: &[u8] = include_bytes!("../../fixtures/iso/embed-areas-2021-09.iso.xz");
let mut decoder = XzDecoder::new(iso_bytes);
let mut iso_file = tempfile().unwrap();
copy(&mut decoder, &mut iso_file).unwrap();
iso_file
}
#[test]
fn test_initrd_embed_area() {
let mut iso_file = open_iso_file();
let mut iso = IsoFs::from_file(iso_file.try_clone().unwrap()).unwrap();
let area = InitrdEmbedArea::for_iso(&mut iso).unwrap();
assert_eq!(area.region.offset, 102400);
assert_eq!(area.region.length, 262144);
iso_file.seek(SeekFrom::Start(65903)).unwrap();
iso_file.write_all(b"Z").unwrap();
let mut iso = IsoFs::from_file(iso_file).unwrap();
InitrdEmbedArea::for_iso(&mut iso).unwrap_err();
}
#[test]
fn test_karg_embed_area() {
let mut iso_file = open_iso_file();
check_karg_embed_areas(&mut iso_file);
iso_file.seek(SeekFrom::Start(32672)).unwrap();
iso_file.write_all(&[0; 8]).unwrap();
check_karg_embed_areas(&mut iso_file);
iso_file.seek(SeekFrom::Start(32672)).unwrap();
iso_file.write_all(b"coreKarg").unwrap();
iso_file.seek(SeekFrom::Start(63725)).unwrap();
iso_file.write_all(b"Z").unwrap();
check_karg_embed_areas(&mut iso_file);
iso_file.seek(SeekFrom::Start(32672)).unwrap();
iso_file.write_all(&[0; 8]).unwrap();
let mut iso = IsoFs::from_file(iso_file).unwrap();
assert!(KargEmbedAreas::for_iso(&mut iso).unwrap().is_none());
}
fn check_karg_embed_areas(iso_file: &mut File) {
let iso_file = iso_file.try_clone().unwrap();
let mut iso = IsoFs::from_file(iso_file).unwrap();
let areas = KargEmbedAreas::for_iso(&mut iso).unwrap().unwrap();
assert_eq!(areas.length, 1139);
assert_eq!(areas.default, "mitigations=auto,nosmt coreos.liveiso=fedora-coreos-34.20210921.dev.0 ignition.firstboot ignition.platform.id=metal");
assert_eq!(areas.regions.len(), 2);
assert_eq!(areas.regions[0].offset, 98126);
assert_eq!(areas.regions[0].length, 1139);
assert_eq!(areas.regions[1].offset, 371658);
assert_eq!(areas.regions[1].length, 1139);
}
}