Plan: Fix ephemeral type argument soundness hole (#1931) #4978
Replies: 5 comments 8 replies
-
Also a few edge cases with intersections that can give caps that are technically sound for expressions but are not "stable" and can't be the type of a variable, like (A ref & A val), which again might be hidden behind indirections, and this ought not match against #any In any case, there should be a predicate already in the caps file for checking stability which is soundness to be a field. |
Beta Was this translation helpful? Give feedback.
-
|
The The expanded set is necessary. If Iterator has Lietar's Same applies to Worth noting this divergence explicitly in the discussion so it's clear this is an intentional extension of the paper's definition, not a restatement of it. |
Beta Was this translation helpful? Give feedback.
-
|
On the
The aliasing chain also holds through a second alias: |
Beta Was this translation helpful? Give feedback.
-
|
Open question: does the compiler already reject ephemeral capabilities as field types? If it does, then I believe it does, but I'll confirm. |
Beta Was this translation helpful? Give feedback.
-
|
@jasoncarr0's point about intersection types is a gap in the proposal. The proposal needs to address this: either a section on how intersection types interact with the new constraint checking, or an explicit scoping-out with reasoning. I'll look into this and come back with more. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Issue #1931 has been open for nearly nine years. It describes a soundness hole in how ephemeral types interact with generic constraints. I've been digging into it and have a concrete plan for fixing it. This discussion lays out the analysis, the proposed approach, and the places where I'd like input from the community.
The Bug
Ephemeral types like
iso^andtrn^can be used as type arguments for generic types even when the constraint doesn't account for ephemerality. This lets you violate isolation guarantees. Here's the simplest demonstration:Both
xandyareA isoreferences to the same object. That's two iso aliases, which is exactly what the capability system is supposed to prevent. This compiles and runs on the current compiler.There's a second variant that doesn't even need
#any. Unconstrained type parameters get desugared toX: X(a self-referential constraint), which trivially satisfies itself when the type argument is reified into it. Soclass Cell[X]withCell[A iso^]also compiles and runs.Root Causes
The constraint checking function
is_cap_sub_cap_constraint()in cap.c has a shortcut at line 181:if((sub == super) || (super == TK_CAP_ANY)) return true. This accepts any capability against#anywithout checking the ephemeral flag. The function already checks one direction (if the constraint is ephemeral, the argument must be too) but never checks the reverse: if the argument is ephemeral, should the constraint be too?The self-referential default makes it worse.
sugar_typeparam()desugars unconstrainedXtoX: X. When you instantiateCell[A iso^], the constraint gets reified toA iso^, and the check becomesis_subtype_constraint(A iso^, A iso^)which trivially passes.There's also a shortcut in
check_constraints(reify.c line 429) that skips constraint checking entirely when the type argument is a TYPEPARAMREF pointing to the same type parameter. This meansCell[X^]inside Cell's own body bypasses the constraint check too. That shortcut needs to be restricted to non-ephemeral cases.Why You Can't Just Reject Ephemeral Type Arguments
The obvious fix — reject ephemeral capabilities against non-ephemeral constraints — breaks the stdlib. Ephemeral type arguments are used everywhere, and for good reason.
Iterator[A^]appears in Seq'sappendandconcat, in Array, List, Map, Set, in PonyCheck's generators, in FileLines, in itertools. These are safe uses because Iterator's type parameter only appears in output position. The unsound uses are specifically when the type parameter ends up in a stored field.So the fix needs two things: make constraint checking actually enforce ephemerality, and give types a way to opt into accepting ephemeral type arguments.
Proposed Approach
This draws on the sync call discussion from 2017 in the original issue. The idea:
#anystays as it is (stable capabilities only), and#any^in constraint position means "stable and ephemeral." The user-facing syntax is familiar. The complexity lives in the compiler.Internally, two new cap tokens represent the expanded sets:
TK_CAP_ANY#anyTK_CAP_ANY_ALL#any^in constraintTK_CAP_SEND#sendTK_CAP_SEND_ALL#send^in constraintOnly
#anyand#sendneed the ALL variants. The other gencaps (#read,#share,#alias) don't becausecap_aliasingstrips the ephemeral flag for all caps in those sets.ref^is justref,val^is justval, and so on.The sugar pass converts source
#any^on a constraint toTK_CAP_ANY_ALL+TK_NONE(and likewise for#send^). The parser doesn't change. No new user-facing syntax. Per Pony's philosophy: "It is more important for the interface to be simple than the implementation."The Fix, Step by Step
Default constraint: Change unconstrained
XfromX: XtoX: Any #any. This makes the safe choice the default. Explicit self-referential constraints (class Cell[X: X]) get detected after name resolution and replaced with the default.Constraint checking: Add an ephemeral guard to
is_cap_sub_cap_constraint— if the argument is ephemeral and the constraint is not, reject it unless the constraint is an ALL variant. Fix thecheck_constraintsshortcut soX^doesn't bypass constraint checking when forwarded as a type argument.Stdlib updates: Types that need to accept ephemeral type arguments get
#any^on their constraints. The primary targets are Iterator, ReadElement, and the PonyCheck generator types. Collection types that store their type parameter in fields (Array, List, Map, Set, Seq, Heap) keep the default#any. Their methods usingIterator[A^]still work becauseA^where A has#anyconstraint produces a type satisfying Iterator's#any^constraint.Compiler plumbing: The new tokens need handling across the capability infrastructure.
cap_aliasing(ALL aliases to its base:#any_allaliased is#any, not#alias), viewpoint reification (ANY_ALL expands to all 8 members including iso^ and trn^), alias functions, compatibility checks, union/intersection constraint computation. About a dozen compiler files touched, mostly adding the new tokens alongside their base variants in switch statements.Breaking Change
Code that passes ephemeral type arguments to generics with non-ephemeral constraints will fail to compile. The migration is straightforward: add
^to the constraint.#anybecomes#any^.What I'd Like Input On
Does
#any^in constraint position feel right as the syntax? The 2017 discussion floated#any*as an alternative, but#any^avoids introducing new syntax and^already means "ephemeral" to Pony users.The default constraint changes from self-referential
X: XtoX: Any #any. This means unconstrained type parameters reject ephemeral type arguments by default. The rationale is safety: if your type stores the parameter in a field, ephemeral instantiation is unsound. Types that handle ephemerals safely (iterators, generators) opt in with#any^.Anything I'm missing? I spent hours going around with Claude on this (Claude is remarkably good at type system stuff). This analysis went through three rounds of review with Claude beating on it, but nine years of open issue suggests there might be subtleties I haven't caught.
Beta Was this translation helpful? Give feedback.
All reactions