[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