Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions docs/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ support under the installation lock. An interrupted transaction completes its
original recovery before that migration. If the saved pair requires repair,
keep the files in place and follow Runtime's operator repair step.

Running `install.sh` again uses the same installation lock and journal. Before it
stops Runtime, it restores an interrupted install and refuses an older release,
another channel, a pending Home update, a second writer and an installed Runtime
without a readable `sources.json`. Until an interrupted install is restored,
Home does not start and asks you to run `install.sh` again.

### Recover an interrupted update

If Runtime reports an interrupted command-line update, run `elastos update`
Expand Down Expand Up @@ -234,9 +240,10 @@ gateways. After confirmation, Runtime accepts release signatures from the new
DID and refuses signatures from the former DID.

An existing Home that keeps its old source pin refuses a release signed by the
new maintainer with a signer-mismatch error. That Home continues to trust the
old key and remains exposed if a copy of that key exists. The owner must compare
the public DID and complete the explicit re-trust step before its next update.
new maintainer. Its `elastos update` error names the signer mismatch and says to
run the publisher's `install.sh`. That run is an explicit re-trust: it pins the
new DID, installs the release and keeps identity, accounts and user files. Until
then the Home trusts the old key and remains exposed if a copy of that key exists.

## Installed files

Expand Down
44 changes: 27 additions & 17 deletions elastos/crates/elastos-server/src/install_transaction.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! Installed release-file transaction. Journal schemas bind their fixed destinations.
//! Installer adoption of this lock is a separate release-owner integration.
//! Updates stage all five release files; the installer leaves components to setup.

use std::collections::{BTreeMap, BTreeSet};
use std::fs::{self, File, OpenOptions};
Expand Down Expand Up @@ -63,6 +63,15 @@ impl ReleaseFile {
Self::ReleaseManifest => "release_manifest",
}
}

/// Installer transactions omit components, which Runtime setup installs.
fn complete_set(ids: impl Iterator<Item = Self>) -> bool {
let mut count = 0;
let ids = ids.inspect(|_| count += 1).collect::<BTreeSet<_>>();
ids.len() == count
&& (ids.len() == Self::ALL.len()
|| ids.len() == Self::ALL.len() - 1 && !ids.contains(&Self::Components))
}
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
Expand Down Expand Up @@ -728,11 +737,8 @@ impl InstallTransaction {
bail!("Legacy recovery must finish before preparing a new installed release.");
}
self.require_empty_scratch()?;
if files.len() != ReleaseFile::ALL.len()
|| files.iter().map(|item| item.0).collect::<BTreeSet<_>>()
!= ReleaseFile::ALL.into_iter().collect()
{
bail!("release transaction requires exactly five release files");
if !ReleaseFile::complete_set(files.iter().map(|item| item.0)) {
bail!("release transaction requires the complete release file set");
}
let mut entries = Vec::new();
let mut allocations = BTreeMap::new();
Expand Down Expand Up @@ -1031,7 +1037,11 @@ impl InstallTransaction {
self.write_journal(&journal)?;
let result = (|| {
for id in [ReleaseFile::RuntimeBinary, ReleaseFile::Components] {
let entry = journal.entries.iter().find(|entry| entry.id == id).unwrap();
let entry = journal
.entries
.iter()
.find(|entry| entry.id == id)
.context("support activation requires all five release files")?;
fs::rename(self.scratch(id, STAGE), &self.destinations[&id])?;
sync_directory(self.destinations[&id].parent().unwrap())?;
self.require_state(id, Some(&entry.staged_sha256), Some(entry.staged_mode))?;
Expand Down Expand Up @@ -1176,13 +1186,7 @@ impl InstallTransaction {
.transaction_id
.bytes()
.all(|b| b.is_ascii_hexdigit())
|| journal.entries.len() != ReleaseFile::ALL.len()
|| journal
.entries
.iter()
.map(|entry| entry.id)
.collect::<BTreeSet<_>>()
!= ReleaseFile::ALL.into_iter().collect()
|| !ReleaseFile::complete_set(journal.entries.iter().map(|entry| entry.id))
{
bail!("installation journal identity is incompatible with this writer");
}
Expand Down Expand Up @@ -1636,10 +1640,16 @@ pub(crate) fn refuse_pending_home_start(data_dir: &Path, binary: &Path) -> anyho
}

fn pending_home_recovery_hint(journal: &Journal) -> &'static str {
if journal.restart.is_none() {
"An interrupted command-line update requires recovery. Run `elastos update` again before starting Home."
} else {
if journal.restart.is_some() {
"Home restart recovery is pending. Start the retained update controller with its receipt."
} else if journal
.entries
.iter()
.all(|entry| entry.id != ReleaseFile::Components)
{
"An interrupted installation requires recovery. Run install.sh again before starting Home."
} else {
"An interrupted command-line update requires recovery. Run `elastos update` again before starting Home."
}
}

Expand Down
63 changes: 47 additions & 16 deletions elastos/crates/elastos-server/src/installed_release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,52 @@ fn admit_pair_and_binary(
head_bytes: &[u8],
release_bytes: &[u8],
) -> Result<(InstalledRelease, serde_json::Value)> {
let release = admit_pair(source, head_bytes, release_bytes)?;
anyhow::ensure!(
fs::symlink_metadata(&source.install_path)?.is_file()
&& fs::canonicalize(&source.install_path)? == binary,
"Installed source binary path differs from its writer"
);
let platform = &release["payload"]["platforms"][crate::update::detect_release_platform()];
let (binary_sha256, binary_size) = file_digest(binary)?;
admit_descriptor(&platform["binary"], &binary_sha256, binary_size)?;
Ok((
InstalledRelease {
head: head_bytes.to_vec(),
release: release_bytes.to_vec(),
binary_sha256,
},
release,
))
}

/// The installer admits its candidate set before the shared writer stages it.
/// Its source is the explicit trust choice, so it may pin a new publisher key.
pub(crate) fn admit_candidate(
binary: &Path,
source: &TrustedSource,
executable: &[u8],
head_bytes: &[u8],
release_bytes: &[u8],
) -> Result<()> {
let release = admit_pair(source, head_bytes, release_bytes)?;
anyhow::ensure!(
Path::new(&source.install_path) == binary,
"Installer source binary path differs from its writer"
);
let platform = &release["payload"]["platforms"][crate::update::detect_release_platform()];
admit_descriptor(
&platform["binary"],
&hex::encode(Sha256::digest(executable)),
executable.len() as u64,
)
}

fn admit_pair(
source: &TrustedSource,
head_bytes: &[u8],
release_bytes: &[u8],
) -> Result<serde_json::Value> {
anyhow::ensure!(
!source.publisher_dids.is_empty()
&& source.publisher_dids.iter().all(|did| !did.is_empty()),
Expand Down Expand Up @@ -272,22 +318,7 @@ fn admit_pair_and_binary(
"Installed signed version differs from its source record"
);
semver::Version::parse(&source.installed_version)?;
anyhow::ensure!(
fs::symlink_metadata(&source.install_path)?.is_file()
&& fs::canonicalize(&source.install_path)? == binary,
"Installed source binary path differs from its writer"
);
let platform = &release["payload"]["platforms"][crate::update::detect_release_platform()];
let (binary_sha256, binary_size) = file_digest(binary)?;
admit_descriptor(&platform["binary"], &binary_sha256, binary_size)?;
Ok((
InstalledRelease {
head: head_bytes.to_vec(),
release: release_bytes.to_vec(),
binary_sha256,
},
release,
))
Ok(release)
}

pub(crate) fn admit_descriptor(
Expand Down
34 changes: 34 additions & 0 deletions elastos/crates/elastos-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,23 @@ enum Commands {
backup_dir: PathBuf,
},

/// Admit and install the installer's verified release through the shared writer
#[command(name = "install-release", hide = true)]
InstallRelease {
/// Runtime data root of this installation
#[arg(long)]
data_dir: PathBuf,
/// Installed Runtime binary path
#[arg(long)]
binary: PathBuf,
/// Verified candidate Runtime, trusted sources, release head and release
#[arg(long, num_args = 4, value_names = ["RUNTIME", "SOURCES", "HEAD", "RELEASE"])]
candidate: Vec<PathBuf>,
/// Admit only; change nothing
#[arg(long)]
check: bool,
},

/// Show runtime version
Version,

Expand Down Expand Up @@ -1586,6 +1603,23 @@ async fn main() -> anyhow::Result<()> {
println!("{}", serde_json::to_string_pretty(&receipt)?);
}

Commands::InstallRelease {
data_dir,
binary,
candidate,
check,
} => {
let [runtime, sources, head, release] = candidate.as_slice() else {
anyhow::bail!("install-release requires four candidate files");
};
elastos_server::update::install_release(
&data_dir,
&binary,
[runtime, sources, head, release].map(PathBuf::as_path),
check,
)?;
}

Commands::Version => {
release_cmd::run_version(ELASTOS_VERSION);
}
Expand Down
Loading
Loading