Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

signpost: GPT priority manipulation utility #28

Merged
merged 26 commits into from Jun 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
29d295b
signpost: Initial commit
iliana May 31, 2019
8987bf7
signpost: Fix next set logic
iliana Jun 3, 2019
03d215c
signpost: cargo update
iliana Jun 3, 2019
d78643b
signpost: Add README.md
iliana Jun 3, 2019
0ac23ac
signpost: "clear-inactive" command; expand docs
iliana Jun 3, 2019
429da0a
signpost: Rollback errors when inactive is invalid
iliana Jun 3, 2019
2780fed
signpost: Remove type argument from PartitionSet
iliana Jun 5, 2019
4ef3b69
signpost: Improve context for Error::ActiveNotInSet
iliana Jun 5, 2019
2c3d3bc
signpost: Clarify std::ops::Not on self.active
iliana Jun 5, 2019
235d6f5
signpost: Add readability line breaks to Error
iliana Jun 5, 2019
530d873
signpost: Improve context for Error::InactiveInvalidRollback
iliana Jun 5, 2019
320f730
signpost: Better document BlockDevice return values
iliana Jun 5, 2019
4098d3c
signpost: Don't open disk for reading when writing
iliana Jun 5, 2019
57a777e
signpost: Explain effective globs in BlockDevice
iliana Jun 5, 2019
6b2990c
signpost: Use bit_field for GptPrio math
iliana Jun 5, 2019
0b56369
signpost: Drop unclear TryFrom<fs::Metadata> for BlockDevice
iliana Jun 5, 2019
954127c
signpost: Improve clarity of State::load
iliana Jun 5, 2019
4fdf00d
signpost: More accurate checks in rollback-to-inactive
iliana Jun 5, 2019
ded5805
signpost: Remove State::set()
iliana Jun 5, 2019
d6aff6a
signpost: README edits
iliana Jun 5, 2019
42b345d
signpost: Grammar nit
iliana Jun 13, 2019
6b14e35
signpost: Use SetSelect more consistently in State.fmt
iliana Jun 13, 2019
d3cd588
signpost: README.md grammar
iliana Jun 13, 2019
b488b86
signpost: Clearer `BlockDevice` constructor name
iliana Jun 13, 2019
1d22df6
signpost: Remove confusing sentence in documentation
iliana Jun 13, 2019
66ff55e
signpost: Better docstring for State::load
iliana Jun 13, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion bin/rpm2img
Expand Up @@ -37,8 +37,8 @@ VERITY_HASH_BLOCK_SIZE=4096
# for the boot partition, where we set gptprio bits in the GUID-specific use
# field, but we might as well do it for all of them.
THAR_BOOT_TYPECODE="6b636168-7420-6568-2070-6c616e657421"
THAR_HASH_TYPECODE="598f10af-c955-4456-6a99-7720068a6cea"
THAR_ROOT_TYPECODE="5526016a-1a97-4ea4-b39a-b7c8c6ca4502"
THAR_HASH_TYPECODE="598f10af-c955-4456-6a99-7720068a6cea"
THAR_RESERVED_TYPECODE="0c5d99a5-d331-4147-baef-08e2b855bdc9"

truncate -s 4G "${DISK_IMAGE}"
Expand Down
17 changes: 6 additions & 11 deletions packages/signpost/signpost.spec
Expand Up @@ -5,17 +5,12 @@ Version: 0.0
Release: 0%{?dist}
Summary: Thar GPT priority querier/switcher
# cargo-license output:
# Apache-2.0 (1): signpost
# Apache-2.0 OR MIT (1): uuid
# Apache-2.0/MIT (17): rand_core, rand_isaac, winapi-i686-pc-windows-gnu, rand_pcg, rand_os, bitflags, autocfg, rand_hc, rand_jitter, cfg-if, winapi-x86_64-pc-windows-gnu, rand_xorshift, rand_chacha, log, rand_core, winapi, rand
# BSD-2-Clause (1): cloudabi
# ISC (1): rdrand
# MIT (2): build_const, gpt
# MIT OR Apache-2.0 (2): libc, crc
# N/A (1): fuchsia-cprng
#
# fuchsia-cprng is BSD: https://fuchsia.googlesource.com/fuchsia/+/master/LICENSE
License: ASL 2.0 and (ASL 2.0 or MIT) and BSD and ISC
# Apache-2.0/MIT (9): autocfg, serde, serde_derive, proc-macro-hack, syn, proc-macro2, serde_plain, quote, unicode-xid
# MIT (3): build_const, gptman, bincode
# MIT OR Apache-2.0 (5): hex-literal, snafu-derive, crc, hex-literal-impl, snafu
# N/A (1): signpost
# Unlicense OR MIT (1): byteorder
License: Unknown AND (Apache-2.0 OR MIT) AND MIT AND (MIT OR Unlicense)
Source0: %{crate_name}.crate
%cargo_bundle_crates -n %{crate_name} -t 0
BuildRequires: gcc-%{_cross_target}
Expand Down
222 changes: 88 additions & 134 deletions workspaces/signpost/Cargo.lock

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions workspaces/signpost/Cargo.toml
Expand Up @@ -4,10 +4,15 @@ version = "0.1.0"
authors = ["iliana destroyer of worlds <iweller@amazon.com>"]
edition = "2018"
publish = false
license = "Apache-2.0"

[dependencies]
gpt = "0.6.1"

[profile.release]
debug = true
lto = true

[dependencies]
bit_field = "0.10.0"
gptman = { version = "0.3.0", default-features = false }
hex-literal = "0.2.0"
serde = { version = "1.0.91", features = ["derive"] }
serde_plain = "0.3.0"
snafu = { version = "0.4.1", default-features = false, features = ["rust_1_30"] }
46 changes: 46 additions & 0 deletions workspaces/signpost/README.md
@@ -0,0 +1,46 @@
# signpost

si**g**n**p**os**t** is a utility for modifying Chrome OS-style GPT priority bits on an OS disk.

```plain
USAGE:
signpost <SUBCOMMAND>

SUBCOMMANDS:
status Show partition sets and priority status
mark-successful-boot Mark the active partitions as successfully booted
clear-inactive Clears inactive priority information to prepare writing images disk
upgrade-to-inactive Sets the inactive partitions as new upgrade partitions
rollback-to-inactive Deprioritizes the inactive partitions
rewrite-table Rewrite the partition table with no changes to disk (used for testing this code)
```

## Background

The Thar OS disk has two partition sets, each containing three partitions:

* the *boot* partition, containing the `vmlinuz` Linux kernel image and the GRUB configuration.
* the *root* partition, containing the read-only `/` filesystem.
* the *hash* partition, containing the full dm-verity hash tree for the root partition.

The Thar boot partition uses the same GPT partition attribute flags as Chrome OS, which are used by GRUB to select the partition from which to read a `grub.cfg`:

| Bits | Content |
|-------|-------------------------------|
| 63-57 | Unused |
| 56 | Successful boot flag |
| 55-52 | Tries remaining |
| 51-48 | Priority |
| 47-0 | Reserved by GPT specification |

The boot partition GRUB selects contains a grub.cfg which references the root and hash partitions by offset, thus selecting all three partitions of a set.

## Upgrade procedure

1. Run `signpost clear-inactive` to clear the priority and successful bits before making any changes to the inactive partitions.
2. Copy the downloaded images to the inactive partitions on disk, then validate data was written correctly.
tjkirch marked this conversation as resolved.
Show resolved Hide resolved
3. Run `signpost upgrade-to-inactive` to prioritize the inactive partitions and allow it one boot attempt before automatically rolling back.

## Rollback procedure

1. Run `signpost rollback-to-inactive` to prioritize the inactive partitions without modifying whether the active partitions were successful.
116 changes: 116 additions & 0 deletions workspaces/signpost/src/error.rs
@@ -0,0 +1,116 @@
use crate::gptprio::GptPrio;
use crate::set::PartitionSet;
use snafu::Snafu;
use std::ffi::OsString;
use std::fmt;
use std::path::PathBuf;

#[derive(Debug, Snafu)]
#[snafu(visibility = "pub(crate)")]
pub(crate) enum Error {
jahkeup marked this conversation as resolved.
Show resolved Hide resolved
#[snafu(display(
"Active partition {} not in either detected partition set ({:?})",
active_partition.display(),
sets
))]
ActiveNotInSet {
active_partition: PathBuf,
sets: [PartitionSet; 2],
},

#[snafu(display("Failed to find GPT on device {}: {}", device.display(), source))]
GPTFind { device: PathBuf, source: GPTError },

#[snafu(display("Failed to write GPT onto device {}: {}", device.display(), source))]
GPTWrite { device: PathBuf, source: GPTError },

#[snafu(display("Inactive partition is not valid to roll back to ({})", flags))]
InactiveInvalidRollback { flags: GptPrio },

#[snafu(display(
"Path {} is a link to {} which does not have a final component (expected {})",
path.display(),
link_target.display(),
expected
))]
LinkWithoutFinalComponent {
path: PathBuf,
link_target: PathBuf,
expected: &'static str,
},

#[snafu(display("Failed to parse major:minor integers from string {:?}: {}", s, source))]
MajorMinorParseInt {
s: String,
source: std::num::ParseIntError,
},

#[snafu(display(
"Failed to parse major:minor integers from string {:?}: does not have exactly one colon",
tjkirch marked this conversation as resolved.
Show resolved Hide resolved
s
))]
MajorMinorLen { s: String },

#[snafu(display("No block device with partition {} found", device_name.to_string_lossy()))]
NoBlockDeviceForPartition { device_name: OsString },

#[snafu(display("Failed to open {} for {}: {}", path.display(), what, source))]
Open {
path: PathBuf,
what: &'static str,
source: std::io::Error,
},

#[snafu(display("Failed to find {} partition for set {}", part_type, set))]
PartitionMissingFromSet {
part_type: &'static str,
set: &'static str,
},

#[snafu(display("Failed to find device for partition {} on {}", num, device.display()))]
PartitionNotFoundOnDevice { num: u32, device: PathBuf },

#[snafu(display("Failed to parse partition number {:?} as integer: {}", s, source))]
PartitionParseInt {
s: String,
source: std::num::ParseIntError,
},

#[snafu(display("Failed to read directory {}: {}", path.display(), source))]
ReadDir {
path: PathBuf,
source: std::io::Error,
},

#[snafu(display("Failed to read from file {}: {}", path.display(), source))]
ReadFile {
path: PathBuf,
source: std::io::Error,
},

#[snafu(display("Failed to read link {}: {}", path.display(), source))]
ReadLink {
path: PathBuf,
source: std::io::Error,
},

#[snafu(display("Root device {} has no lower root devices", root_major_minor))]
RootHasNoLowerDevices { root_major_minor: String },

#[snafu(display("Failed to stat {}: {}", path.display(), source))]
Stat {
path: PathBuf,
source: std::io::Error,
},
}

#[derive(Debug)]
pub(crate) struct GPTError(pub gptman::Error);

impl fmt::Display for GPTError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.0.fmt(f)
}
}

impl std::error::Error for GPTError {}
iliana marked this conversation as resolved.
Show resolved Hide resolved
82 changes: 82 additions & 0 deletions workspaces/signpost/src/gptprio.rs
@@ -0,0 +1,82 @@
use bit_field::BitField;
use std::fmt;

#[derive(Debug, Clone, Copy)]
pub(crate) struct GptPrio(u64);

impl GptPrio {
pub(crate) fn priority(self) -> u64 {
self.0.get_bits(48..52)
tjkirch marked this conversation as resolved.
Show resolved Hide resolved
}

/// Panics if `priority > 15`.
pub(crate) fn set_priority(&mut self, priority: u64) {
self.0.set_bits(48..52, priority);
}

pub(crate) fn tries_left(self) -> u64 {
self.0.get_bits(52..56)
}

/// Panics if `tries_left > 15`.
pub(crate) fn set_tries_left(&mut self, tries_left: u64) {
self.0.set_bits(52..56, tries_left);
}

pub(crate) fn successful(self) -> bool {
self.0.get_bit(56)
}

pub(crate) fn set_successful(&mut self, successful: bool) {
self.0.set_bit(56, successful);
}

pub(crate) fn will_boot(self) -> bool {
(self.priority() > 0 && self.tries_left() > 0) || self.successful()
}
}

impl From<u64> for GptPrio {
fn from(flags: u64) -> Self {
Self(flags)
}
}

impl From<GptPrio> for u64 {
fn from(flags: GptPrio) -> Self {
flags.0
}
}

impl fmt::Display for GptPrio {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"priority={} tries_left={} successful={}",
self.priority(),
self.tries_left(),
self.successful()
)
}
}

#[cfg(test)]
mod tests {
use crate::gptprio::GptPrio;

#[test]
fn test() {
let mut prio = GptPrio(0x5555555555555555);
assert_eq!(prio.priority(), 5);
assert_eq!(prio.tries_left(), 5);
assert_eq!(prio.successful(), true);
assert_eq!(prio.will_boot(), true);
prio.set_priority(0);
assert_eq!(prio.0, 0x5550555555555555);
prio.set_tries_left(0);
assert_eq!(prio.0, 0x5500555555555555);
prio.set_successful(false);
assert_eq!(prio.0, 0x5400555555555555);
assert_eq!(prio.will_boot(), false);
}
}
22 changes: 22 additions & 0 deletions workspaces/signpost/src/guid.rs
@@ -0,0 +1,22 @@
#![allow(clippy::module_name_repetitions)]

pub const fn uuid_to_guid(uuid: [u8; 16]) -> [u8; 16] {
[
uuid[3], uuid[2], uuid[1], uuid[0], uuid[5], uuid[4], uuid[7], uuid[6], uuid[8], uuid[9],
uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15],
]
}

#[cfg(test)]
mod tests {
use crate::guid::uuid_to_guid;
use hex_literal::hex;

#[test]
fn test() {
assert_eq!(
uuid_to_guid(hex!("21686148 6449 6e6f 744e 656564454649")),
*b"Hah!IdontNeedEFI"
);
}
}
67 changes: 66 additions & 1 deletion workspaces/signpost/src/main.rs
@@ -1,3 +1,68 @@
#![warn(clippy::pedantic)]

mod error;
mod gptprio;
mod guid;
mod set;
mod state;

use crate::state::State;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
enum Command {
Status,
MarkSuccessfulBoot,
ClearInactive,
UpgradeToInactive,
RollbackToInactive,
RewriteTable,
}

fn usage() -> ! {
eprintln!("\
USAGE:
signpost <SUBCOMMAND>

SUBCOMMANDS:
status Show partition sets and priority status
mark-successful-boot Mark the active partitions as successfully booted
clear-inactive Clears inactive priority information to prepare writing images to disk
upgrade-to-inactive Sets the inactive partitions as new upgrade partitions
rollback-to-inactive Deprioritizes the inactive partitions
rewrite-table Rewrite the partition table with no changes to disk (used for testing this code)");
std::process::exit(1)
}

fn main() {
println!("Hello, world!");
let command_str = std::env::args().nth(1).unwrap_or_else(|| usage());
let command = serde_plain::from_str::<Command>(&command_str).unwrap_or_else(|_| usage());
tjkirch marked this conversation as resolved.
Show resolved Hide resolved
tjkirch marked this conversation as resolved.
Show resolved Hide resolved

if let Err(err) = State::load().and_then(|mut state| {
match command {
Command::Status => println!("{}", state),
tjkirch marked this conversation as resolved.
Show resolved Hide resolved
Command::ClearInactive => {
state.clear_inactive();
state.write()?;
}
Command::MarkSuccessfulBoot => {
state.mark_successful_boot();
state.write()?;
}
Command::UpgradeToInactive => {
state.upgrade_to_inactive();
state.write()?;
}
Command::RollbackToInactive => {
state.rollback_to_inactive()?;
state.write()?;
}
Command::RewriteTable => state.write()?,
}
Ok(())
}) {
eprintln!("{}", err);
std::process::exit(1)
}
}