Skip to content

Commit 359ad8f

Browse files
author
Strata Core
committed
Security fix: Ty::kind() propagates affinity through compound types
Critical bypass: generic ADTs containing capabilities (e.g., Smuggler<FsCap>) were classified as Kind::Unrestricted, allowing capability duplication via wrapper copying. Ty::kind() now recurses into ADT type args, tuples, and lists. Found during pre-hardening for issue-011 442 tests, zero warnings.
1 parent 14fa390 commit 359ad8f

File tree

5 files changed

+356
-10
lines changed

5 files changed

+356
-10
lines changed

crates/strata-types/src/checker.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -796,8 +796,13 @@ impl TypeChecker {
796796
})
797797
.collect::<Result<Vec<_>, TypeError>>()?;
798798

799-
crate::move_check::check_function_body(&param_info, &decl.body, &self.env)
800-
.map_err(move_error_to_type_error)?;
799+
crate::move_check::check_function_body(
800+
&param_info,
801+
&decl.body,
802+
&self.env,
803+
&self.adt_registry,
804+
)
805+
.map_err(move_error_to_type_error)?;
801806
}
802807

803808
// NOW generalize: compute env vars excluding this function's own type vars

crates/strata-types/src/infer/ty.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,34 @@ impl Ty {
171171
/// `Unrestricted`. Unresolved type variables (`Ty::Var`) are treated
172172
/// as `Unrestricted` — the move checker should only call this on
173173
/// fully-resolved types after substitution.
174+
///
175+
/// RULE: Closures capturing affine values must themselves be affine.
176+
/// Currently not enforceable because Ty::Arrow doesn't track captures
177+
/// (closures/lambdas are not yet a language feature). The absence of
178+
/// closure syntax is the current enforcement mechanism.
179+
/// When closures gain capture tracking (post-borrowing), add:
180+
/// Ty::Arrow with captured_env → if captured_env.contains_affine() { return Kind::Affine; }
174181
pub fn kind(&self) -> Kind {
175182
match self {
176183
Ty::Cap(_) => Kind::Affine,
184+
// Compound types containing affine components are themselves affine.
185+
// This prevents capability laundering through generic ADTs:
186+
// e.g. Smuggler<FsCap> must be affine so it can't be copied.
187+
Ty::Adt { args, .. } => {
188+
if args.iter().any(|arg| arg.kind() == Kind::Affine) {
189+
Kind::Affine
190+
} else {
191+
Kind::Unrestricted
192+
}
193+
}
194+
Ty::Tuple(tys) => {
195+
if tys.iter().any(|ty| ty.kind() == Kind::Affine) {
196+
Kind::Affine
197+
} else {
198+
Kind::Unrestricted
199+
}
200+
}
201+
Ty::List(inner) => inner.kind(),
177202
_ => Kind::Unrestricted,
178203
}
179204
}

crates/strata-types/src/move_check.rs

Lines changed: 132 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
//! bindings need single-use tracking, and resolves polymorphic return types
99
//! by manually instantiating callee schemes with known argument types.
1010

11+
use crate::adt::AdtRegistry;
1112
use crate::infer::ty::{Kind, Scheme, Ty, TypeVarId};
1213
use std::collections::HashMap;
1314
use strata_ast::ast::{Block, Expr, Pat, Stmt};
@@ -104,10 +105,12 @@ pub struct MoveChecker<'a> {
104105
/// The environment with generalized function schemes (for resolving
105106
/// polymorphic call return types).
106107
env: &'a HashMap<String, Scheme>,
108+
/// ADT registry for resolving generic field types in pattern destructuring.
109+
adt_registry: &'a AdtRegistry,
107110
}
108111

109112
impl<'a> MoveChecker<'a> {
110-
fn new(env: &'a HashMap<String, Scheme>) -> Self {
113+
fn new(env: &'a HashMap<String, Scheme>, adt_registry: &'a AdtRegistry) -> Self {
111114
MoveChecker {
112115
name_to_id: HashMap::new(),
113116
tracked: HashMap::new(),
@@ -116,6 +119,7 @@ impl<'a> MoveChecker<'a> {
116119
errors: Vec::new(),
117120
binding_types: HashMap::new(),
118121
env,
122+
adt_registry,
119123
}
120124
}
121125

@@ -236,6 +240,106 @@ impl<'a> MoveChecker<'a> {
236240
}
237241
}
238242

243+
// -----------------------------------------------------------------------
244+
// ADT field type resolution for pattern destructuring
245+
// -----------------------------------------------------------------------
246+
247+
/// Resolve variant field types by matching the scrutinee type against the
248+
/// ADT definition and substituting generic type parameters.
249+
///
250+
/// For example, if `Box<T>` has variant `Val(T)` and the scrutinee is
251+
/// `Box<FsCap>`, this returns `[FsCap]` for the variant fields.
252+
///
253+
/// Returns None if the ADT or variant can't be resolved (fallback to unit).
254+
fn resolve_variant_field_types(
255+
&self,
256+
path: &strata_ast::ast::Path,
257+
scrutinee_ty: &Ty,
258+
) -> Option<Vec<Ty>> {
259+
// Path for a variant is e.g. ["Box", "Val"] — first segment is ADT name
260+
if path.segments.len() < 2 {
261+
return None;
262+
}
263+
let adt_name = &path.segments[0].text;
264+
let variant_name = &path.segments[1].text;
265+
266+
let adt_def = self.adt_registry.get(adt_name)?;
267+
let variant = adt_def.find_variant(variant_name)?;
268+
269+
let raw_field_types = match &variant.fields {
270+
crate::adt::VariantFields::Unit => return Some(vec![]),
271+
crate::adt::VariantFields::Tuple(tys) => tys,
272+
};
273+
274+
// Build substitution: type_params[i] → scrutinee_args[i]
275+
let scrutinee_args = match scrutinee_ty {
276+
Ty::Adt { args, .. } => args,
277+
_ => return None,
278+
};
279+
280+
if adt_def.type_params.is_empty() || scrutinee_args.is_empty() {
281+
// Non-generic ADT — raw field types are already concrete
282+
return Some(raw_field_types.clone());
283+
}
284+
285+
// Map type param TypeVarIds to the concrete types from the scrutinee.
286+
// ADT type params are registered with TypeVarId(0), TypeVarId(1), etc.
287+
let mut mapping: HashMap<TypeVarId, Ty> = HashMap::new();
288+
for (i, arg) in scrutinee_args.iter().enumerate() {
289+
mapping.insert(TypeVarId(i as u32), arg.clone());
290+
}
291+
292+
Some(
293+
raw_field_types
294+
.iter()
295+
.map(|t| apply_type_mapping(t, &mapping))
296+
.collect(),
297+
)
298+
}
299+
300+
/// Resolve struct field types by matching the scrutinee type against the
301+
/// ADT definition and substituting generic type parameters.
302+
///
303+
/// Returns a map from field name to resolved type, or None if unresolvable.
304+
fn resolve_struct_field_types(
305+
&self,
306+
path: &strata_ast::ast::Path,
307+
scrutinee_ty: &Ty,
308+
) -> Option<HashMap<String, Ty>> {
309+
let adt_name = &path.segments.first()?.text;
310+
311+
let adt_def = self.adt_registry.get(adt_name)?;
312+
let fields = adt_def.fields()?;
313+
314+
// Build substitution from scrutinee args
315+
let scrutinee_args = match scrutinee_ty {
316+
Ty::Adt { args, .. } => args,
317+
_ => return None,
318+
};
319+
320+
if adt_def.type_params.is_empty() || scrutinee_args.is_empty() {
321+
// Non-generic struct — raw field types are already concrete
322+
return Some(
323+
fields
324+
.iter()
325+
.map(|f| (f.name.clone(), f.ty.clone()))
326+
.collect(),
327+
);
328+
}
329+
330+
let mut mapping: HashMap<TypeVarId, Ty> = HashMap::new();
331+
for (i, arg) in scrutinee_args.iter().enumerate() {
332+
mapping.insert(TypeVarId(i as u32), arg.clone());
333+
}
334+
335+
Some(
336+
fields
337+
.iter()
338+
.map(|f| (f.name.clone(), apply_type_mapping(&f.ty, &mapping)))
339+
.collect(),
340+
)
341+
}
342+
239343
// -----------------------------------------------------------------------
240344
// Type resolution for function call return types
241345
// -----------------------------------------------------------------------
@@ -553,15 +657,34 @@ impl<'a> MoveChecker<'a> {
553657
}
554658
}
555659
}
556-
Pat::Variant { fields, .. } => {
557-
// Caps in ADT fields are banned, so variant fields are unrestricted
558-
for p in fields {
559-
self.introduce_pattern_bindings(p, &Ty::unit());
660+
Pat::Variant { path, fields, .. } => {
661+
// DEFENSE-IN-DEPTH: When the caps-in-ADTs ban is lifted (post-linear
662+
// types), this substitution ensures generic variant fields are properly
663+
// resolved so capability bindings are tracked as affine.
664+
// Currently the ban prevents caps from reaching ADT fields, so all
665+
// field types resolve to unrestricted. But when the ban is lifted,
666+
// e.g. Box<FsCap> matched as Box::Val(inner), inner must be FsCap.
667+
let field_types = self.resolve_variant_field_types(path, ty);
668+
let unit = Ty::unit();
669+
for (i, p) in fields.iter().enumerate() {
670+
let field_ty = field_types
671+
.as_ref()
672+
.and_then(|tys| tys.get(i))
673+
.unwrap_or(&unit);
674+
self.introduce_pattern_bindings(p, field_ty);
560675
}
561676
}
562-
Pat::Struct { fields, .. } => {
677+
Pat::Struct { path, fields, .. } => {
678+
// DEFENSE-IN-DEPTH: Same as variant — resolve struct field types
679+
// through the generic substitution when possible.
680+
let field_types = self.resolve_struct_field_types(path, ty);
681+
let unit = Ty::unit();
563682
for f in fields {
564-
self.introduce_pattern_bindings(&f.pat, &Ty::unit());
683+
let field_ty = field_types
684+
.as_ref()
685+
.and_then(|m| m.get(&f.name.text))
686+
.unwrap_or(&unit);
687+
self.introduce_pattern_bindings(&f.pat, field_ty);
565688
}
566689
}
567690
}
@@ -657,8 +780,9 @@ pub fn check_function_body(
657780
params: &[(String, Ty, Span)],
658781
body: &Block,
659782
env: &HashMap<String, Scheme>,
783+
adt_registry: &AdtRegistry,
660784
) -> Result<(), MoveError> {
661-
let mut checker = MoveChecker::new(env);
785+
let mut checker = MoveChecker::new(env, adt_registry);
662786

663787
// Introduce function parameters as alive bindings
664788
for (name, ty, span) in params {

crates/strata-types/tests/capabilities.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,69 @@ fn multiple_cap_params_same_kind() {
382382
);
383383
}
384384

385+
// ============================================================================
386+
// CAPS-IN-ADT BAN REGRESSION TESTS (Defense-in-Depth for Generic ADTs)
387+
//
388+
// These tests verify the caps-in-ADT ban covers generic ADT instantiation
389+
// with capability types. When the ban is eventually lifted (post-linear types),
390+
// the move checker's generic field resolution must correctly track affinity.
391+
// ============================================================================
392+
393+
#[test]
394+
fn caps_in_direct_adt_field_banned() {
395+
// Direct capability type in an ADT field is rejected at registration time.
396+
// This is the existing enforcement mechanism.
397+
let err = check_err(
398+
r#"
399+
enum Wrapper { Hold(FsCap) }
400+
"#,
401+
);
402+
assert!(
403+
err.contains("capability") || err.contains("FsCap"),
404+
"Expected error about capability in ADT field, got: {err}"
405+
);
406+
}
407+
408+
#[test]
409+
fn generic_adt_with_cap_single_use_ok() {
410+
// Generic ADTs instantiated with caps are NOT caught by the caps-in-ADT
411+
// registration ban (ban checks field types at definition time where fields
412+
// are type variables). However, Ty::kind() propagates affinity through ADT
413+
// type args, so MyOption<FsCap> is Kind::Affine. This means:
414+
// - Single use (constructing and returning) is fine
415+
// - Duplication (let a = x; let b = x;) is caught by the move checker
416+
check_ok(
417+
r#"
418+
enum MyOption<T> { Some(T), None }
419+
fn wraps_cap(fs: FsCap) -> MyOption<FsCap> & {} {
420+
MyOption::Some(fs)
421+
}
422+
"#,
423+
);
424+
}
425+
426+
#[test]
427+
fn generic_adt_cap_field_double_use_after_extract() {
428+
// When a cap is extracted from a generic ADT via pattern match,
429+
// the extracted binding must be affine. Double use is rejected.
430+
let err = check_err(
431+
r#"
432+
enum Box<T> { Val(T) }
433+
extern fn use_fs(fs: FsCap) -> () & {Fs};
434+
fn bad(fs: FsCap) -> () & {Fs} {
435+
let b = Box::Val(fs);
436+
match b {
437+
Box::Val(inner) => { use_fs(inner); use_fs(inner) },
438+
}
439+
}
440+
"#,
441+
);
442+
assert!(
443+
err.contains("already been used"),
444+
"Expected double-use error on cap extracted from generic ADT, got: {err}"
445+
);
446+
}
447+
385448
// ============================================================================
386449
// ADT NAME SHADOWING TESTS
387450
// ============================================================================

0 commit comments

Comments
 (0)