Skip to content

Commit 233f3cc

Browse files
committed
fix(stack-encrypt): an empty batch still has its context checked before any key request
A slice or Vec batch checked the call's context only through each item's own check, so an empty batch checked nothing: a plan with no context and none from the call, or a plan with its own context given a second one, passed the pre-I/O check, loaded the keyset the chain named, and resolved to an empty success. Opening a Vec of records had the same gap. The batch impls of Runs (for fields plans and one-value plans) and Opens (for a fields plan over Vec<FieldValues>) now check the context once, apart from the items, before checking each item; their pending and decryption fail on the same context, so a direct caller and a chain agree. A context field's expected value is still checked per record, since only a record holds the field. The one-value plans' Vec openings already checked the context alone and only share the helper now. Tests pin both refusals for an empty slice and Vec, run and opened, before any keyset load, and the plan_build fuzz target checks an empty batch's context for every plan it builds.
1 parent 6e68d17 commit 233f3cc

4 files changed

Lines changed: 281 additions & 16 deletions

File tree

‎packages/stack-encrypt/fuzz/fuzz_targets/plan_build.rs‎

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,24 @@
1313
//! it), and a field may be declared as the plan's context field, so a
1414
//! context can also be given twice.
1515
//!
16-
//! Four invariants. Building and rendering never panic, whatever the
16+
//! Five invariants. Building and rendering never panic, whatever the
1717
//! names. A plan built with its context gives every sealed or indexed field
1818
//! a well-formed label: `<context>/<identity>`, which renders and parses
1919
//! back to itself; a plan without one gives none, and keys each such field
2020
//! under a plain identity segment. A passthrough or context field is under
2121
//! no label, so a plan of passthrough fields with distinct names, under a
2222
//! plain context or none, builds whatever text the names are. And a plan
2323
//! whose context is given twice is refused with `TwoContextSources`.
24+
//! And a built plan checks an empty batch's call context as it would one
25+
//! record's: run with none and no context of its own is `NoContext`, run
26+
//! with one beside its own is `TwoContextSources`, and opened likewise,
27+
//! except that a context field's expected value waits for a record.
2428
2529
use std::collections::HashSet;
2630

2731
use arbitrary::Arbitrary;
2832
use libfuzzer_sys::fuzz_target;
29-
use stack_encrypt::plan::{FieldKind, FieldValues, PlanError};
33+
use stack_encrypt::plan::{FieldKind, FieldValues, Opens, PlanError, Runs};
3034
use stack_encrypt::{Equality, Error, Label, Plan};
3135

3236
/// A name: one of a few that collide or sit on a rule's edge, or free text.
@@ -135,8 +139,10 @@ fuzz_target!(|input: Input| {
135139
.unwrap_or(&declared.name)
136140
.as_str();
137141
assert_eq!(built.identity(), identity);
138-
let keys_nothing =
139-
matches!(built.kind(), FieldKind::Passthrough | FieldKind::ContextField);
142+
let keys_nothing = matches!(
143+
built.kind(),
144+
FieldKind::Passthrough | FieldKind::ContextField
145+
);
140146
match (built.label(), &context) {
141147
(None, Some(_)) => assert!(keys_nothing),
142148
(None, None) => {
@@ -160,6 +166,40 @@ fuzz_target!(|input: Input| {
160166
assert!(plan.field(built.name()).is_ok());
161167
}
162168
let _ = format!("{plan:?}");
169+
170+
let has_own = context.is_some() || context_fields == 1;
171+
let call = Label::parse("users").expect("a plain label");
172+
for call in [None, Some(&call)] {
173+
let expected = match (has_own, call.is_some()) {
174+
(false, false) => Some(PlanError::NoContext),
175+
(true, true) => Some(PlanError::TwoContextSources {
176+
first: if context_fields == 1 {
177+
"a context field"
178+
} else {
179+
"the plan"
180+
},
181+
second: "the call",
182+
}),
183+
_ => None,
184+
};
185+
let run = Runs::<Vec<FieldValues>, ()>::check(&plan, &Vec::new(), call);
186+
match (&expected, run) {
187+
(None, Ok(())) => {}
188+
(Some(expected), Err(Error::Plan(error))) => assert_eq!(&error, expected),
189+
(expected, run) => panic!("an empty run checked {run:?}, not {expected:?}"),
190+
}
191+
let opened = Opens::<Vec<FieldValues>, ()>::check(&plan, &Vec::new(), call);
192+
match (&expected, opened) {
193+
(_, Ok(())) if context_fields == 1 => {}
194+
(None, Ok(())) => {}
195+
(Some(expected), Err(Error::Plan(error))) if context_fields == 0 => {
196+
assert_eq!(&error, expected)
197+
}
198+
(expected, opened) => {
199+
panic!("an empty opening checked {opened:?}, not {expected:?}")
200+
}
201+
}
202+
}
163203
}
164204
Err(Error::Plan(error)) => {
165205
assert!(

‎packages/stack-encrypt/src/plan/build.rs‎

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,6 +1324,13 @@ impl<S: 'static, K: 'static> Plan<S, K> {
13241324
}
13251325
}
13261326

1327+
/// The context the call names against the plan's own source, apart
1328+
/// from any value: exactly one of them. A batch asks this once, so an
1329+
/// empty one is refused as one value would be.
1330+
fn check_call(&self, call: Option<&Label>) -> Result<(), Error> {
1331+
Ok(self.base(call.cloned()).map(drop)?)
1332+
}
1333+
13271334
/// A value against the plan, as running it checks it under the context
13281335
/// the call names: one context source, the value's fields are the
13291336
/// plan's (when it reads any by name), each is of the type the plan
@@ -1352,6 +1359,20 @@ impl<S: 'static, K: 'static> Plan<S, K> {
13521359
self.opening_base(record, call.cloned()).map(drop)
13531360
}
13541361

1362+
/// The context the call names against the plan's own source, as
1363+
/// opening checks it apart from any record: exactly one, for a plan
1364+
/// that does not read its context from a field. For one that does, the
1365+
/// call names what the record's field should hold, which only a record
1366+
/// can answer. A batch asks this once, so an empty one is refused as
1367+
/// one record would be.
1368+
fn check_opening_call(&self, call: Option<&Label>) -> Result<(), Error> {
1369+
match &self.inner.source {
1370+
Source::Field(_) => Ok(()),
1371+
Source::Plan(label) => Ok(resolve_context(Some(label), call.cloned()).map(drop)?),
1372+
Source::Call => Ok(resolve_context(None, call.cloned()).map(drop)?),
1373+
}
1374+
}
1375+
13551376
/// The context a stored record opens under.
13561377
fn opening_base(&self, record: &FieldValues, call: Option<Label>) -> Result<Label, Error> {
13571378
match (&self.inner.source, call) {
@@ -1545,9 +1566,11 @@ impl<S: 'static, K: 'static> Runs<S, K> for Plan<S, K> {
15451566
}
15461567

15471568
/// A collection of sources runs one description per item, merged into one
1548-
/// batch.
1569+
/// batch. The context the call names is checked once, by the plan's
1570+
/// `$check_call`, before any item: an empty collection runs no item, and
1571+
/// would otherwise accept a context missing or given twice.
15491572
macro_rules! runs_over_collections {
1550-
($([$($generics:tt)*] $plan:ty => $item:ty where [$($bounds:tt)*];)+) => {$(
1573+
($([$($generics:tt)*] $plan:ty => $item:ty where [$($bounds:tt)*] $check_call:ident;)+) => {$(
15511574
impl<$($generics)*> Runs<[$item], K> for $plan where $($bounds)* {
15521575
type Output = Vec<<Self as Runs<$item, K>>::Output>;
15531576
fn pending<'p>(
@@ -1557,6 +1580,9 @@ macro_rules! runs_over_collections {
15571580
context: Option<Label>,
15581581
extend: DeclaredContext,
15591582
) -> Pending<'p, Self::Output, K> {
1583+
if let Err(error) = self.$check_call(context.as_ref()) {
1584+
return Pending::failed(keyset, error);
1585+
}
15601586
Pending::all(
15611587
keyset,
15621588
source
@@ -1574,6 +1600,7 @@ macro_rules! runs_over_collections {
15741600
)
15751601
}
15761602
fn check(&self, source: &[$item], context: Option<&Label>) -> Result<(), Error> {
1603+
self.$check_call(context)?;
15771604
source
15781605
.iter()
15791606
.try_for_each(|item| Runs::<$item, K>::check(self, item, context))
@@ -1598,7 +1625,7 @@ macro_rules! runs_over_collections {
15981625
}
15991626
pub(crate) use runs_over_collections;
16001627
runs_over_collections! {
1601-
[S, K] Plan<S, K> => S where [S: 'static, K: 'static];
1628+
[S, K] Plan<S, K> => S where [S: 'static, K: 'static] check_call;
16021629
}
16031630

16041631
impl<S: 'static, K: 'static> Opens<FieldValues, K> for Plan<S, K> {
@@ -1624,13 +1651,17 @@ impl<S: 'static, K: 'static> Opens<Vec<FieldValues>, K> for Plan<S, K> {
16241651
context: Option<Label>,
16251652
extend: DeclaredContext,
16261653
) -> Decryption<Vec<FieldValues>, K> {
1654+
if let Err(error) = self.check_opening_call(context.as_ref()) {
1655+
return Decryption::failed(error);
1656+
}
16271657
Decryption::all(
16281658
records
16291659
.into_iter()
16301660
.map(|record| Plan::decryption(self, record, context.clone(), extend.clone())),
16311661
)
16321662
}
16331663
fn check(&self, records: &Vec<FieldValues>, context: Option<&Label>) -> Result<(), Error> {
1664+
self.check_opening_call(context)?;
16341665
records
16351666
.iter()
16361667
.try_for_each(|record| self.check_opening(record, context))

‎packages/stack-encrypt/src/plan/value.rs‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,13 @@ impl<S, X> ValuePlan<S, X> {
315315
resolve_context(self.context.as_ref(), call)
316316
}
317317

318+
/// The context the call names against the plan's own: exactly one of
319+
/// them, which is all a one-value plan checks before a run or an
320+
/// opening.
321+
fn check_call(&self, call: Option<&Label>) -> Result<(), Error> {
322+
Ok(self.resolve(call.cloned()).map(drop)?)
323+
}
324+
318325
/// The description one run of this plan executes, given the context the
319326
/// call names (for a plan built without one; `None` otherwise):
320327
/// the layout `.under(context)`. A context missing or given twice is a
@@ -375,12 +382,12 @@ where
375382
}
376383

377384
fn check(&self, _source: &S, context: Option<&Label>) -> Result<(), Error> {
378-
Ok(self.resolve(context.cloned()).map(drop)?)
385+
self.check_call(context)
379386
}
380387
}
381388

382389
runs_over_collections! {
383-
[S, X, K] ValuePlan<S, X> => S where [X: ValueShape<S>, K: 'static];
390+
[S, X, K] ValuePlan<S, X> => S where [X: ValueShape<S>, K: 'static] check_call;
384391
}
385392

386393
impl<S, X> ValuePlan<S, X> {
@@ -444,7 +451,7 @@ macro_rules! indexed_opens {
444451
self.open_one(record, context, extend)
445452
}
446453
fn check(&self, _: &$record, context: Option<&Label>) -> Result<(), Error> {
447-
Ok(self.resolve(context.cloned()).map(drop)?)
454+
self.check_call(context)
448455
}
449456
}
450457
impl<S, X, K, $($generics)*> Opens<Vec<$record>, K> for ValuePlan<S, Indexed<X>>
@@ -462,7 +469,7 @@ macro_rules! indexed_opens {
462469
self.open_all(records, context, extend)
463470
}
464471
fn check(&self, _: &Vec<$record>, context: Option<&Label>) -> Result<(), Error> {
465-
Ok(self.resolve(context.cloned()).map(drop)?)
472+
self.check_call(context)
466473
}
467474
}
468475
)+};
@@ -491,7 +498,7 @@ where
491498
self.open_one(record, context, extend)
492499
}
493500
fn check(&self, _: &T, context: Option<&Label>) -> Result<(), Error> {
494-
Ok(self.resolve(context.cloned()).map(drop)?)
501+
self.check_call(context)
495502
}
496503
}
497504

@@ -512,6 +519,6 @@ where
512519
self.open_all(records, context, extend)
513520
}
514521
fn check(&self, _: &Vec<T>, context: Option<&Label>) -> Result<(), Error> {
515-
Ok(self.resolve(context.cloned()).map(drop)?)
522+
self.check_call(context)
516523
}
517524
}

0 commit comments

Comments
 (0)