[Oberon] Serious type loophole in type case statements and a possible fix
Andreas Pirklbauer
andreas_pirklbauer at yahoo.com
Fri Oct 30 14:05:06 CET 2020
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
More information about the Oberon
mailing list