Skip to content

[spec] Rectype_ok rules contain a loophole that allows oob REC typeuses in nested deftypes #2142

@raoxiaojia

Description

@raoxiaojia

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions