blob: 282d1bb3a23aadcf22d649b970aa7c4ea9ef9376 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
//! State-root discovery, ownership and mutually exclusive leases.
mod lock;
pub use lock::{FileLease, private_directory};
#[cfg(windows)]
pub mod windows;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::{
fs::{self, File},
io::{self, Read, Write},
path::{Path, PathBuf},
};
pub const ROOT_MARKER: &str = ".maka-storage-root.json";
pub const RUST_ROOT_MARKER: &str = ".maka-rust-runtime.json";
pub const ROOT_DATABASE: &str = "runtime-rust.sqlite";
const PROTOTYPE: &[u8] = b"{\"schemaVersion\":1,\"runtime\":\"rust-prototype\"}\n";
const REMOUNT_STAGING: &str = ".maka-storage-root.next";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RootNamespaces {
pub ownership: PathBuf,
pub control: PathBuf,
}
impl RootNamespaces {
pub fn for_current_account() -> io::Result<Self> {
let home = lock::account_home()?;
#[cfg(target_os = "macos")]
return Ok(Self {
ownership: home.join("Library/Application Support/Maka/state-root-owners"),
control: home.join("Library/Caches/Maka/runtime-hosts"),
});
#[cfg(target_os = "linux")]
return Ok(Self {
ownership: home.join(".local/share/Maka/state-root-owners"),
control: home.join(".cache/maka/runtime-hosts"),
});
#[cfg(windows)]
return Ok(Self {
ownership: home.join("AppData/Local/Maka/state-root-owners"),
control: home.join("AppData/Local/Maka/runtime-hosts"),
});
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
struct Marker {
schema_version: u8,
kind: String,
root_id: String,
root_identity: Identity,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
struct Identity {
dev: String,
ino: String,
}
/// Owns the durable root lease. It cannot be cloned or fabricated.
pub struct RootOwner {
canonical_path: PathBuf,
marker: Marker,
control_directory: PathBuf,
control_identity: Identity,
lock_path: PathBuf,
durable_lease: File,
}
/// A verified location, not a writer lease. Every mutation still needs RootOwner.
#[derive(Debug, Clone)]
pub struct RootLocation {
canonical_path: PathBuf,
root_id: String,
}
impl RootLocation {
pub fn canonical_path(&self) -> &Path {
&self.canonical_path
}
pub fn root_id(&self) -> &str {
&self.root_id
}
}
/// Inspect only an existing native root. Never initialize, repair or acquire it.
pub fn resolve(path: &Path) -> io::Result<RootLocation> {
let (canonical_path, marker) = inspect(path)?;
Ok(RootLocation {
canonical_path,
root_id: marker.root_id,
})
}
/// Explicitly confirm an unchanged Linux directory after its filesystem was
/// remounted. Equal inode numbers alone do not establish identity across volumes.
/// A matching root is a read-only no-op, including while its Host is running.
pub fn repair_after_remount(
path: &Path,
expected_root_id: &str,
namespaces: &RootNamespaces,
) -> io::Result<()> {
let canonical_path = path.canonicalize()?;
let identity = directory_identity(&canonical_path)?;
check_layout(&canonical_path)?;
let previous = read_marker(&canonical_path)?;
if previous.root_id != expected_root_id
|| directory_identity(&canonical_path)? != identity
|| canonical_path.canonicalize()? != canonical_path
{
return Err(io::Error::other("root changed before remount confirmation"));
}
if previous.root_identity == identity {
return Ok(());
}
if !cfg!(target_os = "linux") || previous.root_identity.ino != identity.ino {
return Err(io::Error::other("root is not an unchanged Linux remount"));
}
let mut next = previous.clone();
next.root_identity = identity;
// Use the same ownership lock as normal Host startup, not executor.lock
// or FileLease: Linux OFD locks are distinct from flock.
let owner = RootOwner::acquire(canonical_path, next, namespaces, false)?;
let validate = || {
owner.check_directory()?;
owner.validate_leases()?;
if read_marker(&owner.canonical_path)? != previous {
return Err(io::Error::other(
"root marker changed during remount confirmation",
));
}
owner.check_directory()
};
validate()?;
let staging = owner.canonical_path.join(REMOUNT_STAGING);
match staging.symlink_metadata() {
Ok(metadata) if metadata.is_file() => fs::remove_file(&staging)?,
Ok(_) => return Err(io::Error::other("remount staging is not a regular file")),
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
}
let mut options = fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options.open(&staging)?;
file.write_all(&serde_json::to_vec(&owner.marker)?)?;
file.sync_all()?;
lock::stable(&file, &staging)?;
validate()?;
fs::rename(&staging, owner.canonical_path.join(ROOT_MARKER))?;
// On an uncertain rename/fsync result, stop. The next call re-reads the
// official marker; it never promotes or trusts an abandoned staging file.
File::open(&owner.canonical_path)?.sync_all()?;
owner.validate_current()
}
fn inspect(path: &Path) -> io::Result<(PathBuf, Marker)> {
let canonical_path = path.canonicalize()?;
let identity = directory_identity(&canonical_path)?;
check_layout(&canonical_path)?;
let marker = read_marker(&canonical_path)?;
if marker.root_identity != identity
|| directory_identity(&canonical_path)? != identity
|| canonical_path.canonicalize()? != canonical_path
{
return Err(io::Error::other("root identity collision"));
}
Ok((canonical_path, marker))
}
/// Verify an existing native root without taking its writer lease, or initialize an empty one.
pub fn initialize(path: &Path, namespaces: &RootNamespaces) -> io::Result<String> {
if !path.join(ROOT_MARKER).exists() {
return Ok(RootOwner::create(path, namespaces)?.root_id().to_owned());
}
Ok(resolve(path)?.root_id)
}
impl RootOwner {
/// Initialize a nonexistent or empty disposable directory. Never adopt existing state.
pub fn create(path: &Path, namespaces: &RootNamespaces) -> io::Result<Self> {
#[cfg(not(windows))]
fs::create_dir_all(path)?;
#[cfg(windows)]
if !path.exists() {
lock::private_directory(path)?;
}
let path = path.canonicalize()?;
if fs::read_dir(&path)?.next().is_some() {
return Err(io::Error::other(
"refusing to initialize a nonempty State Root",
));
}
lock::private_directory(&path)?;
let identity = directory_identity(&path)?;
let marker = Marker {
schema_version: 1,
kind: "interactive".into(),
root_id: hex::encode(Sha256::digest(uuid::Uuid::new_v4().as_bytes())),
root_identity: identity,
};
publish(&path, ROOT_MARKER, &serde_json::to_vec(&marker)?)?;
publish(&path, RUST_ROOT_MARKER, PROTOTYPE)?;
Self::open(&path, namespaces)
}
/// Open only a marked Rust prototype root, rejecting legacy layouts before database access.
pub fn open(path: &Path, namespaces: &RootNamespaces) -> io::Result<Self> {
let (canonical_path, marker) = inspect(path)?;
let owner = Self::acquire(canonical_path, marker, namespaces, false)?;
owner.validate_current()?;
Ok(owner)
}
/// Acquire an existing Root without creating or repairing coordination state.
/// Uses the same exclusive ownership protocol as ordinary startup.
pub fn open_existing(path: &Path, namespaces: &RootNamespaces) -> io::Result<Self> {
let (canonical_path, marker) = inspect(path)?;
let owner = Self::acquire(canonical_path, marker, namespaces, true)?;
owner.validate_current()?;
Ok(owner)
}
fn acquire(
canonical_path: PathBuf,
marker: Marker,
namespaces: &RootNamespaces,
existing_only: bool,
) -> io::Result<Self> {
let directory = if existing_only {
lock::existing_private_directory
} else {
lock::private_directory
};
directory(&namespaces.ownership)?;
let lock_path = namespaces
.ownership
.join(format!("{}.lock", marker.root_id));
let durable_lease = if existing_only {
lock::acquire_existing(&lock_path)?
} else {
lock::acquire(&lock_path)?
};
directory(&namespaces.control)?;
let control_directory = namespaces.control.join(&marker.root_id);
directory(&control_directory)?;
let control_identity = directory_identity(&control_directory)?;
Ok(Self {
canonical_path,
marker,
control_directory,
control_identity,
lock_path,
durable_lease,
})
}
pub fn canonical_path(&self) -> &Path {
&self.canonical_path
}
pub fn root_id(&self) -> &str {
&self.marker.root_id
}
pub fn kind(&self) -> &str {
&self.marker.kind
}
pub fn control_directory(&self) -> &Path {
&self.control_directory
}
pub fn lock_path(&self) -> &Path {
&self.lock_path
}
/// Call before root mutations. Detect path rebinding, marker replacement and lease unlinking.
pub fn validate_current(&self) -> io::Result<()> {
self.check_directory()?;
let marker = read_marker(&self.canonical_path)?;
self.check_directory()?;
if marker != self.marker {
return Err(io::Error::other("root marker identity changed"));
}
self.validate_leases()
}
/// Verify a captured directory before using it for domain data. Checking
/// the pathname alone cannot detect a different handle captured during an
/// intervening rename, even after the original pathname has been restored.
pub fn validate_directory_handle(&self, directory: &File) -> io::Result<()> {
self.validate_current()?;
let metadata = directory.metadata()?;
if !metadata.is_dir() {
return Err(io::Error::other("State Root handle must be a directory"));
}
#[cfg(not(windows))]
let (dev, ino) = lock::identity(&metadata)?;
#[cfg(windows)]
let (dev, ino) = {
let identity = windows::file_identity(directory)?;
(identity.volume, identity.index)
};
let identity = Identity {
dev: dev.to_string(),
ino: ino.to_string(),
};
if identity != self.marker.root_identity {
return Err(io::Error::other(
"State Root directory handle identity changed",
));
}
Ok(())
}
fn validate_leases(&self) -> io::Result<()> {
lock::stable(&self.durable_lease, &self.lock_path)?;
// Discovery is disposable, but its loss must fence this live owner:
// another launcher can no longer discover or hand off this process.
if directory_identity(&self.control_directory)? != self.control_identity {
return Err(io::Error::other("Host control directory identity changed"));
}
Ok(())
}
fn check_directory(&self) -> io::Result<()> {
if self.canonical_path.canonicalize()? != self.canonical_path
|| directory_identity(&self.canonical_path)? != self.marker.root_identity
{
return Err(io::Error::other("root directory identity changed"));
}
Ok(())
}
}
fn directory_identity(path: &Path) -> io::Result<Identity> {
let metadata = path.symlink_metadata()?;
if !metadata.is_dir() {
return Err(io::Error::other("State Root must be a directory"));
}
#[cfg(not(windows))]
let (dev, ino) = lock::identity(&metadata)?;
#[cfg(windows)]
let (dev, ino) = {
let file = windows::open_nofollow(path, false)?;
let identity = windows::file_identity(&file)?;
(identity.volume, identity.index)
};
Ok(Identity {
dev: dev.to_string(),
ino: ino.to_string(),
})
}
fn read_bounded(path: &Path) -> io::Result<Vec<u8>> {
let file = lock::open_regular(path, false)?;
let before = file.metadata()?;
let mut bytes = Vec::new();
(&file).take(1025).read_to_end(&mut bytes)?;
lock::stable(&file, path)?;
let after = file.metadata()?;
if bytes.len() > 1024
|| before.len() != bytes.len() as u64
|| before.len() != after.len()
|| before.modified()? != after.modified()?
{
return Err(io::Error::other("invalid or changing bounded marker"));
}
Ok(bytes)
}
fn read_marker(path: &Path) -> io::Result<Marker> {
let marker: Marker = serde_json::from_slice(&read_bounded(&path.join(ROOT_MARKER))?)?;
if marker.schema_version != 1
|| marker.kind != "interactive"
|| marker.root_id.len() != 64
|| !marker
.root_id
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
{
return Err(io::Error::other("invalid State Root marker"));
}
Ok(marker)
}
fn check_layout(path: &Path) -> io::Result<()> {
if read_bounded(&path.join(RUST_ROOT_MARKER))? != PROTOTYPE {
return Err(io::Error::other(
"not a Rust prototype root; legacy state is unsupported",
));
}
for entry in fs::read_dir(path)? {
let entry = entry?;
let name = entry.file_name();
// Tolerate an old experimental file without reading, following or deleting it.
if name == "model-facts.json" {
continue;
}
let core_file = matches!(
name.to_str(),
Some(
ROOT_MARKER
| REMOUNT_STAGING
| RUST_ROOT_MARKER
| ROOT_DATABASE
| "runtime-rust.sqlite-wal"
| "runtime-rust.sqlite-shm"
| "runtime-rust.sqlite.writer.lock"
| "configuration-rust.sqlite"
| "configuration-rust.sqlite-journal"
| "configuration-rust.sqlite-wal"
| "configuration-rust.sqlite-shm"
)
);
let kind = entry.file_type()?;
// Root owns its core files, not the names or layouts of domain data.
// A reserved file cannot become a directory; other entries must be
// real directories, never aliases or alternate top-level databases.
let supported = if core_file {
kind.is_file()
} else {
kind.is_dir()
};
if !supported {
return Err(io::Error::other(format!(
"unsupported files in Rust prototype root: {name:?}"
)));
}
#[cfg(windows)]
windows::open_nofollow(&entry.path(), false)?;
}
Ok(())
}
fn publish(root: &Path, name: &str, bytes: &[u8]) -> io::Result<()> {
let temp = root.join(format!("{name}.{}.tmp", uuid::Uuid::new_v4()));
#[cfg(not(windows))]
let mut file = {
let mut options = std::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
options.open(&temp)?
};
#[cfg(windows)]
let mut file = windows::create_private_file(&temp)?;
let result = (|| {
file.write_all(bytes)?;
file.sync_all()?;
#[cfg(not(windows))]
{
fs::hard_link(&temp, root.join(name))?;
File::open(root)?.sync_all()
}
#[cfg(windows)]
windows::publish_file(&temp, &root.join(name))
})();
let cleanup = match fs::remove_file(&temp) {
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
other => other,
};
result.and(cleanup)
}