[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