[Oberon] Serious type loophole in type case statements and a possible fix

Luca Boasso luke.boasso at gmail.com
Fri Oct 30 15:25:14 CET 2020


Andreas,

I remember reading about a similar issue related to the WITH statement of
Oberon-90, so the way I solved this problem in oberonc is to add an
additional type test each time the case variable is referred in the
statements of a particular case label.
Test0 correctly fails at runtime once it is compiled by oberonc. See
https://github.com/lboasso/oberonc/blob/master/src/OJG.Mod#L868 for the
details in oberonc.

This would be equivalent to the following Oberon-07 code for MODULE Test0
where the reference of p0 is guarded by the label type *P1* each time it is
used in the statements that follows.

PROCEDURE check0();
  BEGIN p0 := p1;  (*sets the dynamic type of the global variable p0 to P1*)
    CASE p0 OF
      P1:
        AssignGlobal(); (*sets the dynamic type of p0 to P2*)
        p0*(P1)*.fld1 := 123; (*but the CASE stmt still thinks p0 is of
type P1, but writes the data to p2.fld2*)
    END ;
    Texts.WriteInt(W, p2.fld2, 4); Texts.Append(Oberon.Log, W.buf)
(*123!!*)
  END check0;

I know that one of the rationales of the WITH statement / CASE type was to
pay for the dynamic type check *once* and avoid additional checks on the
following statements/expressions, but this has always been a source of
implementation bugs when VAR parameters are used.



On Fri, Oct 30, 2020 at 8:06 AM Andreas Pirklbauer <
andreas_pirklbauer at yahoo.com> wrote:

> The official Oberon-07 compiler, as published at www.projectoberon.com,
> contains a serious type loophole in type case statements
>
>    CASE x OF T0:.. | T1:.. END
>
> where x is a pointer or record VAR param and T0, T1.. are pointer or
> record types.
>
> For example, the test program Test0 below *compiles* without any error
> message, but leads to incorrect behaviour at run time. In this particular
> case, it is because the case variable p0 is a *global* variable with is
> assigned
> to a *different* global variable p2 of a *different* extension P2 *within*
> (!) the
> case statement. But the CASE statement still thinks p0 is of type P1, not
> P2...
>
> There are other situations, where type errors can occur at runtime. For
> example, if the case variable which is of a pointer type is passed as a
> VAR parameter to another procedure, which then modifies its dynamic
> type through an assignment within the procedure..
>
> I have therefore modified the Oberon-07 compiler (a little tricky, but
> doable with reasonable effort), such that it..
>
> now DISALLOWS...
>
>     1. Global case variables p in a case statement CASE p OF .. END
>
>     2. Assignments *to* case variables p *within* (the scope of) a CASE
> statement
>
>     3. Passing a case variable p of type POINTER as a VAR parameter to a
> procedure P(p)
>
> but continues to ALLOW…
>
>
>    1. Local variables or parameters (value or VAR) parameters as case
> variables
>
>    2. Assignments to *fields* v.f of a case variable v
>       (where v can be either a pointer or a record)
>
>    3. Passing a case variable v as a *value* parameter to a procedure P(v),
>       (where v can be either a pointer or a record)
>
>    4. Passing a case variable r of type RECORD as a VAR parameter
>       to a procedure P(r) (where the r must itself be a VAR parameter)
>
>
> I believe the above restrictions are acceptable. If not, one would
> have to tinker with the syntax of the type case statement itself.
>
> Below is a test program Test1 which - I believe - now produces the correct
> error messages (shown just below the test program Test1).
>
> The question is:
> ~~~~~~~~~~~~~
>
> Are there any other cases one may need to consider, which could lead to
> type loop holes?
>
>
> -ap
>
>
> ——————— module Test0 ———————————
>
> MODULE Test0;
>   (*
>
>    * The following module compiles under the regular Oberon-07 compiler
>
>    * but has a type loophole at run time
>
>    *)
>
>   IMPORT Texts, Oberon;
>
>   TYPE R0 = RECORD fld0: INTEGER END;
>     R1 = RECORD (R0) fld1: INTEGER END ;
>     R2 = RECORD (R0) fld2: INTEGER END ;  (*same field offset as R1.fld1!*)
>     P0 = POINTER TO R0;
>     P1 = POINTER TO R1;
>     P2 = POINTER TO R2;
>
>   VAR p0: P0; p1: P1; p2: P2; r0: R0; r1: R1; r2: R2; W: Texts.Writer;
>
>   PROCEDURE AssignGlobal();
>   BEGIN p0 := p2
>   END AssignGlobal;
>
>   PROCEDURE check0();
>   BEGIN p0 := p1;  (*sets the dynamic type of the global variable p0 to
> P1*)
>     CASE p0 OF
>       P1:
>         AssignGlobal(); (*sets the dynamic type of p0 to P2*)
>         p0.fld1 := 123; (*but the CASE stmt still thinks p0 is of type P1,
> but writes the data to p2.fld2*)
>     END ;
>     Texts.WriteInt(W, p2.fld2, 4); Texts.Append(Oberon.Log, W.buf)
> (*123!!*)
>   END check0;
>
>   PROCEDURE Go*;
>   BEGIN check0()
>   END Go;
>
> BEGIN NEW(p0); NEW(p1); NEW(p2); Texts.OpenWriter(W)
> END Test0.
>
> ORP.Compile Test0.Mod/s ~
> System.Free Test0 ~
> Test0.Go
>
> When compiled with the official Oberon-07 compiler, there is no
> compile-time
> error, but an incorrect behaviour at run time (see description above)
>
> When compiled with the *modified* Oberon-07 compiler, the following error
> message is produced at compile time:
>
> OR Compiler  8.3.2020 / AP 6.8.20
>   compiling Test0
>   pos 664 global case variable not allowed
> compilation FAILED
>
>
> ——————— module Test1 ———————————
>
> MODULE Test1;
>   (*
>
>    * The error messages below are produced by a modified Oberon-07
> compiler
>    * modifies the type case statement (CASE p OF T1... | T2... ) such that
> it
>    *
>
>    * Disallows...
>
>    *
>
>    *   1. Global case variables p in a case statement CASE p OF .. END
>
>    *   2. Assignments *to* case variables p *within* (the scope of) a CASE
> statement
>    *   3. Passing a case variable p of type POINTER as a VAR parameter to
> a procedure P(p)
>    *
>
>    * But contines to allow...
>
>    *
>
>    *   1. Local variables or parameters (value or VAR) parameters as case
> variables
>    *   2. Assignments to *fields* p.f of a case variable p (either a
> pointer or record)
>    *   3. Passing a case variable p as a *value* parameter to a procedure
> P(p)
>    *   4. Passing a case variable r of type RECORD as a VAR parameter to a
> procedure P(r)
>    *      (note: the record r must itself be a VAR parameter in this
> case)
>    *)
>
>   TYPE R0 = RECORD fld0: INTEGER END;
>     R1 = RECORD (R0) fld1: INTEGER END ;
>     R2 = RECORD (R0) fld2: INTEGER END ;  (*same field offset as R1.fld1!*)
>
>     P0 = POINTER TO R0;
>     P1 = POINTER TO R1;
>     P2 = POINTER TO R2;
>
>   VAR p0: P0; p1: P1; p2: P2; r0: R0; r1: R1; r2: R2;
>
>   PROCEDURE AssignGlobal();
>   BEGIN p0 := p2
>   END AssignGlobal;
>
>   PROCEDURE Proc1(p0: P0); BEGIN END Proc1;
>   PROCEDURE Proc2(VAR p1: P1); BEGIN END Proc2;
>   PROCEDURE Proc3(VAR r0: R0); BEGIN r0 := r2 END Proc3; (*projection*)
>   PROCEDURE Proc4(VAR i: INTEGER); BEGIN END Proc4;
>   PROCEDURE Proc5(b: BOOLEAN); BEGIN END Proc5;
>
>   PROCEDURE check0();
>   BEGIN p0 := p1;  (*sets the dynamic type of the global variable p0 to
> P1*)
>     CASE p0 OF (*ERROR "global case variable not allowed"*)
>       P1:
>         AssignGlobal(); (*if p were allowed to be global, the dynamic type
> of p0 would be set to P2 here*)
>         p0.fld1 := 123 (*and therefore, there would be a type violation
> here, as the CASE stmt thinks p0 is still of type P1*)
>     END ;
>   END check0;
>
>
>   PROCEDURE check1();
>     VAR p, pa: P0;
>   BEGIN p := p1;  (*sets the dynamic type of the local variable p to P1*)
>     CASE p OF
>     P1:
>       pa := p;  (*allowed, since p is not modified*)
>       Proc1(p);  (*allowed, since p is passed as a value parameter*)
>       Proc2(p);  (*ERROR "read-only" -> not allowed, since p is passed as
> a VAR parameter*)
>       Proc4(p.fld1);  (*allowed, since a field of p, but not p itself is
> passed*)
>       p := p2;  (*ERROR "assignment not allowed"  -> not allowed, since p
> is modified through an assignment*)
>       p.fld1 := 123;  (*guaranteed to be correct, since the runtime type
> of p cannot change inside CASE*)
>       CASE pa OF  (*nested case statement*)
>         P1:
>           Proc1(p);  (*allowed, since p is passed as a value parameter*)
>           Proc2(p)   (*ERROR "read-only" -> not allowed, since p is passed
> as a VAR parameter*)
>       END ;
>       p := p1  (*ERROR "assignment not allowed" -> not allowed, since p is
> modified through an assignment*)
>     END
>   END check1;
>
>   PROCEDURE check2();
>   BEGIN p0 := p1;  (*sets the dynamic type of the global variable p0 to
> P1*)
>     IF p0 IS P1 THEN
>       AssignGlobal(); (*the runtime type of p0 is not P1 anymore, but P2*)
>       p0(P1).fld1 := 123; (*thereore, there (correctly) is a type guard
> failure (TRAP 2) at runtime*)
>     END
>   END check2;
>
>   PROCEDURE check3(VAR r: R0);
>     VAR rr: R1;
>   BEGIN r := r1; (*allowed since pr is a VAR parameter (projection to
> fields of base type)*)
>     CASE r OF
>       R1:
>         rr := r;  (*allowed, since r itself is not modified*)
>         r := r1;  (*ERROR "assignment not allowed" -> not allowed, since r
> is modified through an assignment*)
>         Proc3(r);  (*allowed since r is a record passed as a VAR
> parameter*)
>         r.fld1 := 234
>     END
>   END check3;
>
>   PROCEDURE check4();
>     VAR r: R1;
>   BEGIN r := r1;
>     CASE r OF  (*ERROR "invalid type" -> not allowed since r is neither a
> pointer nor a VAR parameter*)
>       R1: r.fld1 := 345
>     END ;
>   END check4;
>
>
>   PROCEDURE check5(p: P0);
>   BEGIN
>     CASE p OF
>       P1: Proc5(p IS P1)
>     END
>   END check5;
>
>   PROCEDURE Go*;
>   BEGIN
>     check0();
>     check1();
>     check2();
>     check3(r0);
>     check4();
>     check5(p0)
>   END Go;
>
> BEGIN NEW(p0); NEW(p1); NEW(p2)
> END Test1.
>
> ORP.Compile Test1.Mod/s ~    # compiled with MODIFIED compiler
>
> OR Compiler  8.3.2020 / AP 6.8.20
>   compiling Test1
>   pos 1679 global case variable not allowed
>   pos 2284 read-only
>   pos 2456 assignment not allowed
>   pos 2814 read-only
>   pos 2916 assignment not allowed
>   pos 3613 assignment not allowed
>   pos 3910 invalid type
>   pos 4011 not a constant
> compilation FAILED
>
>
> --
> Oberon at lists.inf.ethz.ch mailing list for ETH Oberon and related systems
> https://lists.inf.ethz.ch/mailman/listinfo/oberon
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.inf.ethz.ch/pipermail/oberon/attachments/20201030/7eb72899/attachment.html>


More information about the Oberon mailing list