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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-uti
rusqlite = { version = "0.39", features = ["bundled"] }
regex = "1"
sha2 = "0.10"
base64 = "0.22"
serde_norway = "0.9.42"
semver = "1"
# `default-features = false` excludes network transports, credential helpers, and filter-driver command execution from the dependency graph.
Expand Down
18 changes: 16 additions & 2 deletions crates/mc-kernel/src/admission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1110,15 +1110,29 @@ impl Envelope<'_> {
replacement: DecisionSpec,
) -> Result<DecisionWriteOutcome, KernelError> {
let facts = load_subject_facts(self, replaced_object_id)?;
if load_prior_decision(self, &facts)?.is_some_and(|stored| {
self.refuse_barred_lineage(&facts)?;
// A live replacement is a fold target, so a barred survivor is refused before correction.
if self
.object_state(&replacement.object_id)?
.is_some_and(|state| state.object.invalidated_commit_seq.is_none())
{
let survivor = load_subject_facts(self, &replacement.object_id)?;
self.refuse_barred_lineage(&survivor)?;
}
self.correct_decision(replaced_object_id, replacement)
}

/// A lineage barred by its latest admission decision cannot be corrected or folded.
fn refuse_barred_lineage(&self, facts: &SubjectFacts) -> Result<(), KernelError> {
if load_prior_decision(self, facts)?.is_some_and(|stored| {
matches!(
stored.decision.disposition,
Disposition::Quarantined | Disposition::Rejected | Disposition::Contradicted
)
}) {
return Err(KernelError::AdmissionPolicy);
}
self.correct_decision(replaced_object_id, replacement)
Ok(())
}

pub fn revoke_approval(
Expand Down
67 changes: 67 additions & 0 deletions crates/mc-kernel/src/cas/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,35 @@ pub enum ArtifactErrorKind {
PurgeUnlinkPending,
}

impl ArtifactErrorKind {
/// Every variant, in declaration order, so consumers can prove they map
/// each one. Adding a variant fails `all_names_every_variant_once` to
/// compile until its match is extended, which is where this list is
/// revisited.
pub const ALL: &'static [Self] = &[
Self::PayloadTooLarge,
Self::Capacity,
Self::StorageExhausted,
Self::IngestionFailClosed,
Self::ReAdmissionBlocked,
Self::MissingObject,
Self::CorruptObject,
Self::ReferenceUnavailable,
Self::ReferenceCommit,
Self::AlignmentRebuild,
Self::ReclaimInProgress,
Self::UnredactableSecret,
Self::ScanIncomplete,
Self::DetectionLimit,
Self::TextFieldTooLong,
Self::InvalidInput,
Self::OperationKeyReused,
Self::StorageConstraint,
Self::PurgeIntent,
Self::PurgeUnlinkPending,
];
}

/// Bounded artifact failure with optional capacity or digest context.
///
/// Display and debug output expose no payload bytes.
Expand Down Expand Up @@ -474,6 +503,44 @@ pub(crate) fn is_artifact_digest(value: &str) -> bool {
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}

#[cfg(test)]
mod error_kind_tests {
use super::ArtifactErrorKind;

#[test]
fn all_names_every_variant_once() {
for kind in ArtifactErrorKind::ALL {
// The wildcard-free match forces a new arm, and with it a review
// of `ALL`, whenever a variant is added.
match kind {
ArtifactErrorKind::PayloadTooLarge
| ArtifactErrorKind::Capacity
| ArtifactErrorKind::StorageExhausted
| ArtifactErrorKind::IngestionFailClosed
| ArtifactErrorKind::ReAdmissionBlocked
| ArtifactErrorKind::MissingObject
| ArtifactErrorKind::CorruptObject
| ArtifactErrorKind::ReferenceUnavailable
| ArtifactErrorKind::ReferenceCommit
| ArtifactErrorKind::AlignmentRebuild
| ArtifactErrorKind::ReclaimInProgress
| ArtifactErrorKind::UnredactableSecret
| ArtifactErrorKind::ScanIncomplete
| ArtifactErrorKind::DetectionLimit
| ArtifactErrorKind::TextFieldTooLong
| ArtifactErrorKind::InvalidInput
| ArtifactErrorKind::OperationKeyReused
| ArtifactErrorKind::StorageConstraint
| ArtifactErrorKind::PurgeIntent
| ArtifactErrorKind::PurgeUnlinkPending => {}
}
}
for (index, kind) in ArtifactErrorKind::ALL.iter().enumerate() {
assert!(!ArtifactErrorKind::ALL[..index].contains(kind));
}
}
}

#[cfg(test)]
mod lattice_tests {
use super::ProviderEgress;
Expand Down
4 changes: 3 additions & 1 deletion crates/mc-kernel/src/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ impl Sensitivity {
}
}

pub(super) fn restrictive(self, other: Self) -> Self {
/// The stricter of two labels; a successor never relabels its
/// predecessor's content below the label it was admitted under.
pub fn restrictive(self, other: Self) -> Self {
match (self, other) {
(Self::Secret, _) | (_, Self::Secret) => Self::Secret,
(Self::Sensitive, _) | (_, Self::Sensitive) => Self::Sensitive,
Expand Down
14 changes: 9 additions & 5 deletions crates/mc-kernel/src/slice/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ impl KernelStore {
///
/// Returns [`KernelError::InvalidInput`] for a negative sequence,
/// [`KernelError::FutureSnapshot`] when `requested` exceeds the transaction's current tip,
/// [`KernelError::CorruptCanonicalRow`] for invalid stored JSON payloads, and
/// [`KernelError::Io`] for lock, SQLite, conversion, or commit failures.
/// [`KernelError::CorruptCanonicalRow`] for invalid stored JSON payloads,
/// the SQLite classification (`Busy`, `Conflict`, `Io`) for a row that
/// fails while being read, and [`KernelError::Io`] for lock, statement, or
/// commit failures.
pub fn slice_as_of(&self, requested: i64) -> Result<SliceSnapshot, KernelError> {
let mut reader = self.lock_reader()?;
let tx = reader
Expand All @@ -78,8 +80,10 @@ impl KernelStore {
///
/// Returns [`KernelError::InvalidInput`] for a negative sequence,
/// [`KernelError::FutureSnapshot`] when `requested` exceeds the transaction's current tip,
/// [`KernelError::CorruptCanonicalRow`] for invalid stored JSON payloads, and
/// [`KernelError::Io`] for lock, SQLite, conversion, or commit failures.
/// [`KernelError::CorruptCanonicalRow`] for invalid stored JSON payloads,
/// the SQLite classification (`Busy`, `Conflict`, `Io`) for a row that
/// fails while being read, and [`KernelError::Io`] for lock, statement, or
/// commit failures.
pub fn decisions_for_objects_as_of(
&self,
object_ids: &[String],
Expand Down Expand Up @@ -335,6 +339,6 @@ fn classify_row_error(error: rusqlite::Error) -> KernelError {
{
KernelError::CorruptCanonicalRow
}
_ => KernelError::Io,
_ => crate::map_sqlite(error),
}
}
50 changes: 50 additions & 0 deletions crates/mc-kernel/tests/kernel_slice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -972,3 +972,53 @@ fn supersede_decision_refuses_a_quarantined_predecessor() {
.unwrap_err();
assert_eq!(missing, KernelError::NotFound);
}

/// A supersession whose replacement is already live folds the predecessor
/// into that survivor, so the survivor's lineage is judged as well.
#[test]
fn a_fold_refuses_a_quarantined_survivor() {
let directory = tempfile::tempdir().unwrap();
let store = KernelStore::open(directory.path()).unwrap();
seed_domain(&store);
store
.commit(intent("seed", '1'), |envelope| {
envelope.insert_decision(decision(1))?;
envelope.insert_decision(decision(2))?;
envelope.insert_decision(decision(3))?;
envelope
.record_admission(subject_request("decision-object-3", EventKind::Quarantine))?;
Ok(String::new())
})
.unwrap();

// The survivor's revision advances past the predecessor's, so only the
// lineage guard stands between this fold and the write.
let barred_survivor = store
.commit(intent("fold-into-quarantined", '2'), |envelope| {
envelope.supersede_decision("decision-object-2", decision(3))?;
Ok(String::new())
})
.unwrap_err();
assert_eq!(barred_survivor, KernelError::AdmissionPolicy);
assert_eq!(
inspect_i64(
directory.path(),
"SELECT COUNT(*) FROM object_registry WHERE invalidated_commit_seq IS NOT NULL OR superseded_by IS NOT NULL"
),
0
);

store
.commit(intent("fold", '3'), |envelope| {
envelope.supersede_decision("decision-object-1", decision(2))?;
Ok(String::new())
})
.unwrap();
assert_eq!(
inspect_i64(
directory.path(),
"SELECT COUNT(*) FROM object_registry WHERE object_id = 'decision-object-1' AND superseded_by = 'decision-object-2'"
),
1
);
}
4 changes: 2 additions & 2 deletions crates/mc-module/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ memchr = "2"
tokio = { workspace = true, features = ["signal"] }
tokio-util = { version = "0.7", features = ["rt"] }
sha2 = { workspace = true }
# Artifact pages cross the JSON ring as base64; same version mc-tokenizer uses.
base64 = "0.22"
# Artifact pages cross the JSON ring as base64.
base64 = { workspace = true }
# Page decoding: SIMD with runtime dispatch and a scalar fallback; decodes
# the same inputs to the same bytes as `base64`'s STANDARD engine and rejects
# the same malformed ones.
Expand Down
Loading
Loading