blob: 02d534a1572e9581cbaf04bd1a4eb9513d8844bb [file]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
crate::update::{paver, BuildInfo},
anyhow::{anyhow, Error},
fidl_fuchsia_mem::Buffer,
fidl_fuchsia_paver::{Asset, BootManagerProxy, DataSinkProxy},
fuchsia_inspect as inspect,
fuchsia_syslog::{fx_log_err, fx_log_warn},
mundane::hash::{Hasher as _, Sha256},
serde::{Deserialize, Serialize},
std::{convert::TryInto as _, str::FromStr},
update_package::{SystemVersion, UpdatePackage},
};
/// The version of the OS.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Version {
/// The hash of the update package.
pub update_hash: String,
/// The hash of the system image package.
pub system_image_hash: String,
/// The vbmeta and zbi hash are SHA256 hash of the image with trailing zeros removed, we can't
/// use the exact image because when reading from paver we get the entire partition back.
pub vbmeta_hash: String,
pub zbi_hash: String,
/// The version in build-info.
pub build_version: SystemVersion,
}
impl Default for Version {
fn default() -> Self {
Version {
update_hash: Default::default(),
system_image_hash: Default::default(),
vbmeta_hash: Default::default(),
zbi_hash: Default::default(),
build_version: SystemVersion::Opaque("".to_string()),
}
}
}
impl Version {
#[cfg(test)]
pub fn for_hash(update_hash: impl Into<String>) -> Self {
Self { update_hash: update_hash.into(), ..Self::default() }
}
#[cfg(test)]
pub fn for_hash_and_empty_paver_hashes(update_hash: impl Into<String>) -> Self {
const EMPTY_HASH: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
Self {
update_hash: update_hash.into(),
vbmeta_hash: EMPTY_HASH.to_owned(),
zbi_hash: EMPTY_HASH.to_owned(),
..Self::default()
}
}
/// Returns the Version for the given update package.
pub async fn for_update_package(update_package: &UpdatePackage) -> Self {
let update_hash = match update_package.hash().await {
Ok(hash) => hash.to_string(),
Err(e) => {
fx_log_err!("Failed to get update hash: {:#}", anyhow!(e));
"".to_string()
}
};
let system_image_hash =
get_system_image_hash_from_update_package(update_package).await.unwrap_or_else(|e| {
fx_log_err!("Failed to get system image hash: {:#}", anyhow!(e));
"".to_string()
});
let vbmeta_hash = get_image_hash_from_update_package(update_package, "fuchsia.vbmeta")
.await
.unwrap_or_else(|e| {
fx_log_err!("Failed to read vbmeta hash: {:#}", anyhow!(e));
"".to_string()
});
let zbi_hash = match get_image_hash_from_update_package(update_package, "zbi").await {
Ok(v) => v,
Err(_) => get_image_hash_from_update_package(update_package, "zbi.signed")
.await
.unwrap_or_else(|e| {
fx_log_err!("Failed to read zbi hash: {:#}", anyhow!(e));
"".to_string()
}),
};
let build_version = update_package.version().await.unwrap_or_else(|e| {
fx_log_err!("Failed to read build version: {:#}", anyhow!(e));
SystemVersion::Opaque("".to_string())
});
Self { update_hash, system_image_hash, vbmeta_hash, zbi_hash, build_version }
}
/// Returns the Version for the current running system.
pub async fn current(
last_target_version: Option<&Version>,
data_sink: &DataSinkProxy,
boot_manager: &BootManagerProxy,
build_info: &impl BuildInfo,
pkgfs_system: &Option<pkgfs::system::Client>,
) -> Self {
let system_image_hash =
get_system_image_hash_from_pkgfs_system(pkgfs_system).await.unwrap_or_else(|e| {
fx_log_err!("Failed to read system image hash: {:#}", anyhow!(e));
"".to_string()
});
let (vbmeta_hash, zbi_hash) =
get_vbmeta_and_zbi_hash_from_environment(data_sink, boot_manager).await.unwrap_or_else(
|e| {
fx_log_err!("Failed to read vbmeta and/or zbi hash: {:#}", anyhow!(e));
("".to_string(), "".to_string())
},
);
let build_version = match build_info.version().await {
Ok(Some(version)) => version,
Ok(None) => {
fx_log_err!("Build version not found");
"".to_string()
}
Err(e) => {
fx_log_err!("Failed to read build version: {:#}", anyhow!(e));
"".to_string()
}
};
let build_version = SystemVersion::from_str(&build_version).unwrap();
let update_hash = match last_target_version {
Some(version) => {
if vbmeta_hash == version.vbmeta_hash
&& (system_image_hash == "" || system_image_hash == version.system_image_hash)
&& (zbi_hash == "" || zbi_hash == version.zbi_hash)
&& (build_version.is_empty() || build_version == version.build_version)
{
version.update_hash.clone()
} else {
"".to_string()
}
}
None => "".to_string(),
};
Self { update_hash, system_image_hash, vbmeta_hash, zbi_hash, build_version }
}
pub fn write_to_inspect(&self, node: &inspect::Node) {
// This destructure exists to use the compiler to guarantee we are copying all the
// UpdateAttempt fields to inspect.
let Version { update_hash, system_image_hash, vbmeta_hash, zbi_hash, build_version } = self;
node.record_string("update_hash", update_hash);
node.record_string("system_image_hash", system_image_hash);
node.record_string("vbmeta_hash", vbmeta_hash);
node.record_string("zbi_hash", zbi_hash);
node.record_string("build_version", build_version.to_string());
}
}
async fn get_system_image_hash_from_update_package(
update_package: &UpdatePackage,
) -> Result<String, Error> {
let packages = update_package.packages().await?;
let system_image = packages
.into_iter()
.find(|url| url.name() == "system_image" && url.variant() == Some("0"))
.ok_or_else(|| anyhow!("system image not found"))?;
let hash =
system_image.package_hash().ok_or_else(|| anyhow!("system image package has no hash"))?;
Ok(hash.to_string())
}
async fn get_image_hash_from_update_package(
update_package: &UpdatePackage,
image: &str,
) -> Result<String, Error> {
let buffer = update_package.open_image(&update_package::Image::new(image)).await?;
Ok(sha256_hash_with_no_trailing_zeros(buffer)?)
}
async fn get_system_image_hash_from_pkgfs_system(
pkgfs_system: &Option<pkgfs::system::Client>,
) -> Result<String, Error> {
match pkgfs_system.as_ref() {
Some(pkgfs_system) => Ok(pkgfs_system.hash().await?),
None => Err(anyhow!("pkgfs/system not available")),
}
}
async fn get_vbmeta_and_zbi_hash_from_environment(
data_sink: &DataSinkProxy,
boot_manager: &BootManagerProxy,
) -> Result<(String, String), Error> {
let current_configuration = paver::query_current_configuration(boot_manager).await?;
let configuration = current_configuration
.to_configuration()
.ok_or_else(|| anyhow!("device does not support ABR"))?;
let vbmeta_buffer =
paver::paver_read_asset(data_sink, configuration, Asset::VerifiedBootMetadata).await?;
let vbmeta_hash = sha256_hash_with_no_trailing_zeros(vbmeta_buffer)?;
let zbi_buffer = paver::paver_read_asset(data_sink, configuration, Asset::Kernel).await?;
let zbi_hash = sha256_hash_with_no_trailing_zeros(zbi_buffer)?;
Ok((vbmeta_hash, zbi_hash))
}
// Compute the SHA256 hash of the buffer with trailing zeros stripped.
fn sha256_hash_with_no_trailing_zeros(buffer: Buffer) -> Result<String, Error> {
let mut size = buffer.size.try_into()?;
let mut data = vec![0u8; size];
buffer.vmo.read(&mut data, 0)?;
while size > 0 && data[size - 1] == 0 {
size -= 1;
}
if size == 0 {
fx_log_warn!("entire buffer is 0, size: {}", buffer.size);
}
Ok(Sha256::hash(&data[..size]).to_string())
}
#[cfg(test)]
pub(super) async fn mock_pkgfs_system(
system_image_hash: impl AsRef<[u8]>,
) -> (pkgfs::system::Client, tempfile::TempDir) {
let pkgfs_dir = tempfile::tempdir().expect("/tmp to exist");
std::fs::create_dir(pkgfs_dir.path().join("system")).expect("create dir");
io_util::file::write_in_namespace(
pkgfs_dir.path().join("system/meta").to_str().unwrap(),
system_image_hash,
)
.await
.expect("write to temp dir");
let pkgfs_proxy = io_util::directory::open_in_namespace(
pkgfs_dir.path().to_str().unwrap(),
io_util::OPEN_RIGHT_READABLE,
)
.expect("temp dir to open");
(pkgfs::system::Client::open_from_pkgfs_root(&pkgfs_proxy).unwrap(), pkgfs_dir)
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::update::environment::NamespaceBuildInfo,
::version::Version as SemanticVersion,
fidl_fuchsia_paver::Configuration,
fuchsia_pkg_testing::TestUpdatePackage,
fuchsia_zircon::Vmo,
mock_paver::{hooks as mphooks, MockPaverServiceBuilder},
pretty_assertions::assert_eq,
std::sync::Arc,
};
#[fuchsia_async::run_singlethreaded(test)]
async fn version_for_invalid_update_package() {
let update_pkg = TestUpdatePackage::new();
assert_eq!(Version::for_update_package(&update_pkg).await, Version::default());
}
#[fuchsia_async::run_singlethreaded(test)]
async fn version_for_valid_update_package() {
let update_pkg = TestUpdatePackage::new()
.hash("2937013f2181810606b2a799b05bda2849f3e369a20982a4138f0e0a55984ce4")
.await
.add_package("fuchsia-pkg://fuchsia.com/system_image/0?hash=838b5199d12c8ff4ef92bfd9771d2f8781b7b8fd739dd59bcf63f353a1a93f67")
.await
.add_file("fuchsia.vbmeta", "vbmeta")
.await
.add_file("zbi", "zbi")
.await
.add_file("version", "1.2.3.4")
.await;
assert_eq!(
Version::for_update_package(&update_pkg).await,
Version {
update_hash: "2937013f2181810606b2a799b05bda2849f3e369a20982a4138f0e0a55984ce4"
.to_string(),
system_image_hash:
"838b5199d12c8ff4ef92bfd9771d2f8781b7b8fd739dd59bcf63f353a1a93f67".to_string(),
vbmeta_hash: "a0c6f07a4b3a17fb9348db981de3c5602e2685d626599be1bd909195c694a57b"
.to_string(),
// Should be "a7124150e065aa234710ab3875230f17deb36a9249938e11f2f3656954412ab8"
// See comment in sha256_hash_removed_trailing_zeros test.
zbi_hash: "a7124150e065aa234710ab387523f17deb36a9249938e11f2f3656954412ab8"
.to_string(),
build_version: SystemVersion::Semantic(SemanticVersion::from([1, 2, 3, 4])),
}
);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn version_current_copy_update_hash() {
let paver = Arc::new(
MockPaverServiceBuilder::new()
.active_config(Configuration::A)
.insert_hook(mphooks::read_asset(|configuration, asset| {
assert_eq!(configuration, Configuration::A);
match asset {
Asset::Kernel => Ok(vec![0x7a, 0x62, 0x69]),
Asset::VerifiedBootMetadata => Ok(vec![0x76, 0x62, 0x6d, 0x65, 0x74, 0x61]),
}
}))
.build(),
);
let data_sink = paver.spawn_data_sink_service();
let boot_manager = paver.spawn_boot_manager_service();
let (pkgfs_system, _pkgfs_dir) =
mock_pkgfs_system("838b5199d12c8ff4ef92bfd9771d2f8781b7b8fd739dd59bcf63f353a1a93f67")
.await;
let last_target_version = Version {
update_hash: "2937013f2181810606b2a799b05bda2849f3e369a20982a4138f0e0a55984ce4"
.to_string(),
system_image_hash: "838b5199d12c8ff4ef92bfd9771d2f8781b7b8fd739dd59bcf63f353a1a93f67"
.to_string(),
vbmeta_hash: "a0c6f07a4b3a17fb9348db981de3c5602e2685d626599be1bd909195c694a57b"
.to_string(),
// Should be "a7124150e065aa234710ab3875230f17deb36a9249938e11f2f3656954412ab8"
// See comment in sha256_hash_removed_trailing_zeros test.
zbi_hash: "a7124150e065aa234710ab387523f17deb36a9249938e11f2f3656954412ab8".to_string(),
build_version: SystemVersion::Opaque("".to_string()),
};
assert_eq!(
Version::current(
Some(&last_target_version),
&data_sink,
&boot_manager,
&NamespaceBuildInfo,
&Some(pkgfs_system)
)
.await,
last_target_version
);
}
#[test]
fn sha256_hash_empty_buffer() {
let buffer = Buffer { vmo: Vmo::create(0).unwrap(), size: 0 };
assert_eq!(
sha256_hash_with_no_trailing_zeros(buffer).unwrap(),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}
#[test]
fn sha256_hash_all_zero_buffer() {
let vmo = Vmo::create(100).unwrap();
vmo.write(&[0; 100], 0).unwrap();
let buffer = Buffer { vmo, size: 100 };
assert_eq!(
sha256_hash_with_no_trailing_zeros(buffer).unwrap(),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}
#[test]
fn sha256_hash_removed_trailing_zeros() {
let vmo = Vmo::create(100).unwrap();
vmo.write(&[0; 100], 0).unwrap();
vmo.write(&[1; 1], 0).unwrap();
let buffer = Buffer { vmo, size: 100 };
assert_eq!(
sha256_hash_with_no_trailing_zeros(buffer).unwrap(),
// This hash string is only 63 characters, but it should be 64:
// 4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a
// note the "00" becomes "0", this is a bug in mundane, see fxr/410079.
"4bf5122f344554c53bde2ebb8cd2b7e3d160ad631c385a5d7cce23c7785459a"
);
}
}