There seems to be an issue in the Rectype_ok rules that allow oob REC typeuses to be valid. The relevant rules are the following:
rule Rectype_ok/empty:
C |- REC eps : OK(x)
rule Rectype_ok/cons:
C |- REC (subtype_1 subtype*) : OK(x)
-- Subtype_ok: C |- subtype_1 : OK(x)
-- Rectype_ok: C |- REC subtype* : OK($(x+1))
rule Rectype_ok/_rec2:
C |- REC subtype* : OK(x)
-- Rectype_ok2: C, RECS subtype* |- REC subtype* : OK x 0
rule Rectype_ok2/empty:
C |- REC eps : OK x i
rule Rectype_ok2/cons:
C |- REC (subtype_1 subtype*) : OK x i
-- Subtype_ok2: C |- subtype_1 : OK x i
-- Rectype_ok2: C |- REC subtype* : OK $(x+1) $(i+1)
Note that Rectype_ok/cons allows the subtype to be validated directly using whatever C.RECS was. Now consider the following example, with an outer 2-member rec group sts = REC [st_a, st_b] containing the following:
st_a = SUB [] [] (STRUCT [ref (deftype inner_dt)])
st_b = SUB [] [] (STRUCT [])
where the inner deftype is the 0th projection of a length-1 rec group which contains an oob reference:
inner_dt = DEF (REC [inner_st]) 0
inner_st = SUB [] [] (STRUCT [ref (REC 1)])
I claim that the outer rec group sts is valid under an empty context C via the following chain of sufficient conditions (I include the explicit *type_ok relations instead of using the C |- notations to avoid ambiguity):
rectype_ok C sts x
<= (by (rectype_ok/rec2)) rectype_ok2 (C' := {C with RECS := sts}) (REC sts) x 0
<= (by (rectype_ok2/cons)) subtype_ok2 C' st_a x 0 && rectype_ok2 C' [st_b] (x+1) 1
The latter rectype_ok2 is trivial since st_b contains no interesting information. For the first part, all conditions except comptype_ok is trivial, since st_a declares no supertype (so nothing to prove for supertype validity and the $before predicate). This proceeds further by chasing down a long chain of constructors for the sufficient conditions (note that due to soundness's extension of typeuse to deftype and REC, all the various *_ok relations are actually in a large mutually recursive group):
comptype_ok C' (STRUCT [ref (deftype inner_dt)])
<= fieldtype_ok C' (ref (deftype inner_dt))
<= storagetype_ok C' (ref (deftype inner_dt))
<= valtype_ok C' (ref (deftype inner_dt))
<= reftype_ok C' (ref (deftype inner_dt))
<= heaptype_ok C' (deftype inner_dt)
<= typeuse_ok C' (deftype inner_dt)
<= deftype_ok C' inner_dt
<= rectype_ok C' [inner_st]
Now, we should expect the last line to be impossible, because rectype_ok/rec2 requires [inner_st] to be typed under a new context C'' := {C' with RECS := [inner_st]}, thereby revealing the oob REC 1 in inner_st. However, rectype_ok/cons gives a loophole that allows inner_st to be typed under C' := {C with RECS := [st_a, st_b]}, which was the original outer rec group. This would go through the above again until hitting typeuse, which then validates the REC 1 reference against the outer C'.RECS -- and is accepted because typeuse/rec_ only requires the REC index to be within bound. This completes a proof that the original rec group sts is well-typed under an empty context C despite it containing an inner subtype inner_st that contains an invalid REC index. The root cause is that rectype_ok/cons gives an incorrect context for a rec group to be checked.
IMO the mathematically correct fix is to directly remove rectype_ok/empty and rectype_ok/cons. This will force the typing of rec group to always go through rectype_ok/rec2, which will mandate a corresponding shadowing of the C.RECS field (as intended). As a result, rectype_ok will simply be a wrapper that updates the RECS field of the context, while the iterative check of each individual subtype in the rec group will be handled by the inductive rules of rectype_ok2.
Note that the current rectype_ok/empty+cons are redundant anyway, since rectype_ok2 already contains the iterative structure. However, this fix might not work directly for the spec document, as it may have complications regarding how the rectype_ok rule is currently rendered -- I believe the validation section only renders rectype_ok (which is actually wrong) instead of rectype_ok2 (which is a soundness-appendix-only artifact).
Lastly, I don't think this counterexample can currently appear during validation of an actual Wasm module, mainly because REC doesn't have a binary encoding; plus that REC only appears by rolling type indices, but rolling doesn't produce nested REC type variables in inner rectype groups due to the substitution functions correctly stopping REC substitution at rectype boundaries (by minus_recs), so there's no exploitable module-level counterexample.
There seems to be an issue in the
Rectype_okrules that allow oobRECtypeuses to be valid. The relevant rules are the following:Note that
Rectype_ok/consallows the subtype to be validated directly using whateverC.RECSwas. Now consider the following example, with an outer 2-memberrecgroupsts = REC [st_a, st_b]containing the following:where the inner deftype is the 0th projection of a length-1
recgroup which contains an oob reference:I claim that the outer rec group
stsis valid under an empty contextCvia the following chain of sufficient conditions (I include the explicit *type_ok relations instead of using the C |- notations to avoid ambiguity):The latter
rectype_ok2is trivial sincest_bcontains no interesting information. For the first part, all conditions exceptcomptype_okis trivial, sincest_adeclares no supertype (so nothing to prove for supertype validity and the$beforepredicate). This proceeds further by chasing down a long chain of constructors for the sufficient conditions (note that due to soundness's extension of typeuse to deftype andREC, all the various *_ok relations are actually in a large mutually recursive group):Now, we should expect the last line to be impossible, because
rectype_ok/rec2requires[inner_st]to be typed under a new contextC'' := {C' with RECS := [inner_st]}, thereby revealing the oobREC 1ininner_st. However,rectype_ok/consgives a loophole that allowsinner_stto be typed underC' := {C with RECS := [st_a, st_b]}, which was the original outer rec group. This would go through the above again until hittingtypeuse, which then validates theREC 1reference against the outerC'.RECS-- and is accepted becausetypeuse/rec_only requires theRECindex to be within bound. This completes a proof that the original rec groupstsis well-typed under an empty context C despite it containing an inner subtypeinner_stthat contains an invalidRECindex. The root cause is that rectype_ok/cons gives an incorrect context for a rec group to be checked.IMO the mathematically correct fix is to directly remove
rectype_ok/emptyandrectype_ok/cons. This will force the typing ofrecgroup to always go throughrectype_ok/rec2, which will mandate a corresponding shadowing of theC.RECSfield (as intended). As a result,rectype_okwill simply be a wrapper that updates theRECSfield of the context, while the iterative check of each individualsubtypein the rec group will be handled by the inductive rules ofrectype_ok2.Note that the current
rectype_ok/empty+consare redundant anyway, sincerectype_ok2already contains the iterative structure. However, this fix might not work directly for the spec document, as it may have complications regarding how therectype_okrule is currently rendered -- I believe the validation section only rendersrectype_ok(which is actually wrong) instead ofrectype_ok2(which is a soundness-appendix-only artifact).Lastly, I don't think this counterexample can currently appear during validation of an actual Wasm module, mainly because
RECdoesn't have a binary encoding; plus thatREConly appears by rolling type indices, but rolling doesn't produce nested REC type variables in innerrectypegroups due to the substitution functions correctly stoppingRECsubstitution atrectypeboundaries (byminus_recs), so there's no exploitable module-level counterexample.