diff --git a/.gitignore b/.gitignore index b97eb7334..d33151e91 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ secrets.db /results /dpkg-workdir /compiled.tar -/compiled-*.tar \ No newline at end of file +/compiled-*.tar +/firmware \ No newline at end of file diff --git a/Makefile b/Makefile index 31124bee9..65d4d79dd 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,8 @@ ARCH := $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo aarch64; else ec IMAGE_TYPE=$(shell if [ "$(PLATFORM)" = raspberrypi ]; then echo img; else echo iso; fi) BINS := core/target/$(ARCH)-unknown-linux-gnu/release/startbox core/target/aarch64-unknown-linux-musl/release/container-init core/target/x86_64-unknown-linux-musl/release/container-init WEB_UIS := web/dist/raw/ui web/dist/raw/setup-wizard web/dist/raw/diagnostic-ui web/dist/raw/install-wizard -BUILD_SRC := $(shell git ls-files build) build/lib/depends build/lib/conflicts +FIRMWARE_ROMS := ./firmware/$(PLATFORM) $(shell jq --raw-output '.[] | select(.platform[] | contains("$(PLATFORM)")) | "./firmware/$(PLATFORM)/" + .id + ".rom.gz"' build/lib/firmware.json) +BUILD_SRC := $(shell git ls-files build) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS) DEBIAN_SRC := $(shell git ls-files debian/) IMAGE_RECIPE_SRC := $(shell git ls-files image-recipe/) STARTD_SRC := core/startos/startd.service $(BUILD_SRC) @@ -72,6 +73,7 @@ clean: rm -rf dpkg-workdir rm -rf image-recipe/deb rm -rf results + rm -rf build/lib/firmware rm -f ENVIRONMENT.txt rm -f PLATFORM.txt rm -f GIT_HASH.txt @@ -134,6 +136,8 @@ install: $(ALL_TARGETS) $(call cp,system-images/compat/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/startos/system-images/compat.tar) $(call cp,system-images/utils/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/startos/system-images/utils.tar) $(call cp,system-images/binfmt/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/startos/system-images/binfmt.tar) + + $(call cp,firmware/$(PLATFORM),$(DESTDIR)/usr/lib/startos/firmware) update-overlay: $(ALL_TARGETS) @echo "\033[33m!!! THIS WILL ONLY REFLASH YOUR DEVICE IN MEMORY !!!\033[0m" @@ -165,6 +169,9 @@ upload-ota: results/$(BASENAME).squashfs build/lib/depends build/lib/conflicts: build/dpkg-deps/* build/dpkg-deps/generate.sh +$(FIRMWARE_ROMS): build/lib/firmware.json download-firmware.sh $(PLATFORM_FILE) + ./download-firmware.sh $(PLATFORM) + system-images/compat/docker-images/$(ARCH).tar: $(COMPAT_SRC) core/Cargo.lock cd system-images/compat && make docker-images/$(ARCH).tar && touch docker-images/$(ARCH).tar diff --git a/build/lib/firmware.json b/build/lib/firmware.json new file mode 100644 index 000000000..9637aa70a --- /dev/null +++ b/build/lib/firmware.json @@ -0,0 +1,13 @@ +[ + { + "id": "pureboot-librem_mini_v2-basic_usb_autoboot_blob_jail-Release-28.3", + "platform": ["x86_64"], + "system-product-name": "librem_mini_v2", + "bios-version": { + "semver-prefix": "PureBoot-Release-", + "semver-range": "<28.3" + }, + "url": "https://source.puri.sm/firmware/releases/-/raw/master/librem_mini_v2/custom/pureboot-librem_mini_v2-basic_usb_autoboot_blob_jail-Release-28.3.rom.gz", + "shasum": "5019bcf53f7493c7aa74f8ef680d18b5fc26ec156c705a841433aaa2fdef8f35" + } +] diff --git a/build/lib/firmware/librem_mini_v2/PureBoot-Release-28.1.rom.gz b/build/lib/firmware/librem_mini_v2/PureBoot-Release-28.1.rom.gz deleted file mode 100644 index 3e5376759..000000000 Binary files a/build/lib/firmware/librem_mini_v2/PureBoot-Release-28.1.rom.gz and /dev/null differ diff --git a/core/Cargo.lock b/core/Cargo.lock index dab184deb..cc7698271 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -4238,6 +4238,9 @@ name = "semver" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +dependencies = [ + "serde", +] [[package]] name = "semver-parser" @@ -5000,6 +5003,7 @@ dependencies = [ "rpc-toolkit", "rust-argon2", "scopeguard", + "semver 1.0.20", "serde", "serde_json", "serde_with", diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index c794b4a06..65d01b1db 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -128,6 +128,7 @@ rpassword = "7.2.0" rpc-toolkit = "0.2.2" rust-argon2 = "2.0.0" scopeguard = "1.1" # because avahi-sys fucks your shit up +semver = { version = "1.0.20", features = ["serde"] } serde = { version = "1.0", features = ["derive", "rc"] } serde_cbor = { package = "ciborium", version = "0.2.1" } serde_json = "1.0" diff --git a/core/startos/src/bins/start_init.rs b/core/startos/src/bins/start_init.rs index bafde4c74..1cb070851 100644 --- a/core/startos/src/bins/start_init.rs +++ b/core/startos/src/bins/start_init.rs @@ -9,7 +9,7 @@ use tracing::instrument; use crate::context::rpc::RpcContextConfig; use crate::context::{DiagnosticContext, InstallContext, SetupContext}; -use crate::disk::fsck::RepairStrategy; +use crate::disk::fsck::{RepairStrategy, RequiresReboot}; use crate::disk::main::DEFAULT_PASSWORD; use crate::disk::REPAIR_DISK_PATH; use crate::firmware::update_firmware; @@ -30,11 +30,18 @@ async fn setup_or_init(cfg_path: Option) -> Result, Er } })); - if update_firmware().await?.0 { - return Ok(Some(Shutdown { - export_args: None, - restart: true, - })); + match update_firmware().await { + Ok(RequiresReboot(true)) => { + return Ok(Some(Shutdown { + export_args: None, + restart: true, + })) + } + Err(e) => { + tracing::warn!("Error performing firmware update: {e}"); + tracing::debug!("{e:?}"); + } + _ => (), } Command::new("ln") diff --git a/core/startos/src/disk/fsck/mod.rs b/core/startos/src/disk/fsck/mod.rs index 2a67a6fb3..6758ddd58 100644 --- a/core/startos/src/disk/fsck/mod.rs +++ b/core/startos/src/disk/fsck/mod.rs @@ -11,7 +11,7 @@ use crate::Error; pub mod btrfs; pub mod ext4; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] #[must_use] pub struct RequiresReboot(pub bool); impl std::ops::BitOrAssign for RequiresReboot { diff --git a/core/startos/src/firmware.rs b/core/startos/src/firmware.rs index 9f3e3c52c..7f9a4a273 100644 --- a/core/startos/src/firmware.rs +++ b/core/startos/src/firmware.rs @@ -1,16 +1,63 @@ +use std::collections::BTreeSet; use std::path::Path; use async_compression::tokio::bufread::GzipDecoder; +use clap::ArgMatches; +use rpc_toolkit::command; +use serde::{Deserialize, Serialize}; use tokio::fs::File; -use tokio::io::{AsyncRead, BufReader}; +use tokio::io::BufReader; use tokio::process::Command; use crate::disk::fsck::RequiresReboot; use crate::prelude::*; use crate::util::Invoke; +use crate::PLATFORM; +/// Part of the Firmware, look there for more about +#[derive(Clone, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct VersionMatcher { + /// Strip this prefix on the version matcher + semver_prefix: Option, + /// Match the semver to this range + semver_range: Option, + /// Strip this suffix on the version matcher + semver_suffix: Option, +} + +/// Inside a file that is firmware.json, we +/// wanted a structure that could help decide what to do +/// for each of the firmware versions +#[derive(Clone, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct Firmware { + id: String, + /// This is the platform(s) the firmware was built for + platform: BTreeSet, + /// This usally comes from the dmidecode + system_product_name: Option, + /// The version comes from dmidecode, then we decide if it matches + bios_version: Option, + /// the hash of the firmware rom.gz + shasum: String, +} + +fn display_firmware_update_result(arg: RequiresReboot, _: &ArgMatches) { + if arg.0 { + println!("Firmware successfully updated! Reboot to apply changes."); + } else { + println!("No firmware update available."); + } +} + +/// We wanted to make sure during every init +/// that the firmware was the correct and updated for +/// systems like the Pure System that a new firmware +/// was released and the updates where pushed through the pure os. +#[command(rename = "update-firmware", display(display_firmware_update_result))] pub async fn update_firmware() -> Result { - let product_name = String::from_utf8( + let system_product_name = String::from_utf8( Command::new("dmidecode") .arg("-s") .arg("system-product-name") @@ -19,52 +66,84 @@ pub async fn update_firmware() -> Result { )? .trim() .to_owned(); - if product_name.is_empty() { + let bios_version = String::from_utf8( + Command::new("dmidecode") + .arg("-s") + .arg("bios-version") + .invoke(ErrorKind::Firmware) + .await?, + )? + .trim() + .to_owned(); + if system_product_name.is_empty() || bios_version.is_empty() { return Ok(RequiresReboot(false)); } - let firmware_dir = Path::new("/usr/lib/startos/firmware").join(&product_name); - if tokio::fs::metadata(&firmware_dir).await.is_ok() { - let current_firmware = String::from_utf8( - Command::new("dmidecode") - .arg("-s") - .arg("bios-version") - .invoke(ErrorKind::Firmware) - .await?, - )? - .trim() - .to_owned(); - if tokio::fs::metadata(firmware_dir.join(format!("{current_firmware}.rom.gz"))) - .await - .is_err() - && tokio::fs::metadata(firmware_dir.join(format!("{current_firmware}.rom"))) - .await - .is_err() - { - let mut firmware_read_dir = tokio::fs::read_dir(&firmware_dir).await?; - while let Some(entry) = firmware_read_dir.next_entry().await? { - let filename = entry.file_name().to_string_lossy().into_owned(); - let rdr: Option> = - if filename.ends_with(".rom.gz") { - Some(Box::new(GzipDecoder::new(BufReader::new( - File::open(entry.path()).await?, - )))) - } else if filename.ends_with(".rom") { - Some(Box::new(File::open(entry.path()).await?)) - } else { - None - }; - if let Some(mut rdr) = rdr { - Command::new("flashrom") - .arg("-p") - .arg("internal") - .arg("-w-") - .input(Some(&mut rdr)) - .invoke(ErrorKind::Firmware) - .await?; - return Ok(RequiresReboot(true)); + + let firmware_dir = Path::new("/usr/lib/startos/firmware"); + + for firmware in serde_json::from_str::>( + &tokio::fs::read_to_string("/usr/lib/startos/firmware.json").await?, + ) + .with_kind(ErrorKind::Deserialization)? + { + let id = firmware.id; + let matches_product_name = firmware + .system_product_name + .map_or(true, |spn| spn == system_product_name); + let matches_bios_version = firmware + .bios_version + .map_or(Some(true), |bv| { + let mut semver_str = bios_version.as_str(); + if let Some(prefix) = &bv.semver_prefix { + semver_str = semver_str.strip_prefix(prefix)?; + } + if let Some(suffix) = &bv.semver_suffix { + semver_str = semver_str.strip_suffix(suffix)?; } - } + let semver = semver_str + .split(".") + .filter_map(|v| v.parse().ok()) + .chain(std::iter::repeat(0)) + .take(3) + .collect::>(); + let semver = semver::Version::new(semver[0], semver[1], semver[2]); + Some( + bv.semver_range + .as_ref() + .map_or(true, |r| r.matches(&semver)), + ) + }) + .unwrap_or(false); + if firmware.platform.contains(&*PLATFORM) && matches_product_name && matches_bios_version { + let filename = format!("{id}.rom.gz"); + let firmware_path = firmware_dir.join(&filename); + Command::new("sha256sum") + .arg("-c") + .input(Some(&mut std::io::Cursor::new(format!( + "{} {}", + firmware.shasum, + firmware_path.display() + )))) + .invoke(ErrorKind::Filesystem) + .await?; + let mut rdr = if tokio::fs::metadata(&firmware_path).await.is_ok() { + GzipDecoder::new(BufReader::new(File::open(&firmware_path).await?)) + } else { + return Err(Error::new( + eyre!("Firmware {id}.rom.gz not found in {firmware_dir:?}"), + ErrorKind::NotFound, + )); + }; + Command::new("flashrom") + .arg("-p") + .arg("internal") + .arg("-w-") + .input(Some(&mut rdr)) + .invoke(ErrorKind::Firmware) + .await?; + return Ok(RequiresReboot(true)); } } + Ok(RequiresReboot(false)) } diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index 5dac1f628..5fde6513f 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -105,6 +105,7 @@ pub fn main_api() -> Result<(), RpcError> { shutdown::restart, shutdown::rebuild, update::update_system, + firmware::update_firmware, ))] pub fn server() -> Result<(), RpcError> { Ok(()) diff --git a/download-firmware.sh b/download-firmware.sh new file mode 100755 index 000000000..2457b3062 --- /dev/null +++ b/download-firmware.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +cd "$(dirname "${BASH_SOURCE[0]}")" + +set -e + +PLATFORM=$1 + +if [ -z "$PLATFORM" ]; then + >&2 echo "usage: $0 " + exit 1 +fi + +rm -rf ./firmware/$PLATFORM +mkdir -p ./firmware/$PLATFORM + +cd ./firmware/$PLATFORM + +mapfile -t firmwares <<< "$(jq -c ".[] | select(.platform[] | contains(\"$PLATFORM\"))" ../../build/lib/firmware.json)" +for firmware in "${firmwares[@]}"; do + if [ -n "$firmware" ]; then + id=$(echo "$firmware" | jq --raw-output '.id') + url=$(echo "$firmware" | jq --raw-output '.url') + shasum=$(echo "$firmware" | jq --raw-output '.shasum') + curl --fail -L -o "${id}.rom.gz" "$url" + echo "$shasum ${id}.rom.gz" | sha256sum -c + fi +done diff --git a/image-recipe/build.sh b/image-recipe/build.sh index 633944642..836fc49ed 100755 --- a/image-recipe/build.sh +++ b/image-recipe/build.sh @@ -344,4 +344,4 @@ elif [ "${IMAGE_TYPE}" = img ]; then mv $TARGET_NAME $RESULTS_DIR/$IMAGE_BASENAME.img -fi \ No newline at end of file +fi diff --git a/system-images/compat/Cargo.lock b/system-images/compat/Cargo.lock index 9ce44203a..c1e0959fb 100644 --- a/system-images/compat/Cargo.lock +++ b/system-images/compat/Cargo.lock @@ -3800,9 +3800,12 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.16" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +dependencies = [ + "serde", +] [[package]] name = "serde" @@ -4475,6 +4478,7 @@ dependencies = [ "rpc-toolkit", "rust-argon2 2.0.0", "scopeguard", + "semver", "serde", "serde_json", "serde_with",