Design of member access levels in statically typed capability based language

Constantine Plotnikov

Revision History
Revision 0.0.11 Mar 2002 
First version published on the web.

Table of Contents

Introduction and Problem Statement
Generic VM design
Amplifier object design.
Considered alternatives
Amplified types design
Native method access
Application launching example in posix system

Abstract

This paper discuss implementing access to private, public, and internal members using amplifier design pattern in capability based system "Sebyla". This is paper has status of design note and should not be considered as commitment to implement it in this particular way.

Introduction and Problem Statement

This paper were written as part of attempt to design virtual machine for Java/C#-like language in capability way. Starting point were ECMA CLI class library as quite small by volume and with definitive profiles.

One of the biggest problems that I have meet is problem of private/internal/protected members. Design presented here is somewhat error prone, and could lead to capability leaks.

On one hand I wanted to support all design patters that used in modern OO languages, on other hand I'm seeking to design it in the way that does not compromise capability architecture. I have identified following set of requirements to the system.

  1. System should be simple, intuitive, and non-error-prone.

  2. Statically typed language.

  3. Capability based.

  4. Distribution and type verification unit is assembly.

  5. System should support following access levels.

    public

    Member is accessible to everyone.

    protected

    Member is accessible from members of subclasses.

    internal

    Member is accessible from members that are defined in the same assembly (shared library).

    private

    Member is accessible only from members of this class. or members of member classes.

  6. It should be possible to get information about members will all access levels. About internal or private members only if those methods are accessible from context or right to learn about members were delegated to current access.

  7. Downcast is allowed.

  8. Class checking is allowed.

  9. Static methods of all access levels are allowed.

  10. Static immutable data is allowed. But immutability of it should be proved.

  11. Possibility of direct member access is checked during bytecode verification.

  12. Possibility of member access using reflection is checked during run time. It should be possible to delegate these rights.

  13. Native methods should be allowed, but not for everyone. Ability to access native methods should be dynamically granted rather then statically derived.

  14. System should be also have the same or better efficiency as one in Java of CLI for verified and compiled code. Valid access reflective should not be slower. There are no restrictions on invalid access.

Generic VM design

The VM is of classical stack based design with static type checking. There are local variables (allocated on stack frame), there are calculation stack state and there are instance variables and methods. Design is provided here in minimal way. And some elements that usually part of vm specification are left out for simplicity.

Every memory location accessible from bytecode has type. Operation upon invocation consumes argument's values on the stack and leave results on the stack. Operation could be invoked only if types of all arguments match signature of operation. This is checked on verification stage.

There are stack manipulation operations. load.local, store.local. And load.constiant operations. Similar operations are load.static/store.static and load.var/store.var. Condition checking operations jump operations are jump, jump.if.true. And return value operation return.

Example 1. simple function in xcl

to sum(a:int[]):int {
  var n = a.length - 1
  var rc = 0 as int 
  while(n>=0)
    rc = rc + a[n]
    n = n - 1
  }
  return rc
}

Example 2. simple function in sebyla assembler

to sum(a:int[]):int {
  local a:int[]
  local n:int
  local rc:int
  load.local a
  load.var "int[].length"
  store.local n
  load.constant 0 // name of constant will be specified there
  store.local rc
label loopStart  
  load.local n
  load.constant 0
  invoke "int.lessOrEquals(int):bool"
  jump.if.false loopExit
  load.local rc
  load.local a
  load.local n
  invoke "int[].op__GetAt(int):int"
  invoke "int.op__Plus(int):int"
  store.local rc
  load.local n
  load.constant 1
  invoke "int.op__Minus(int):int"
  store.local n
  jump loopStart
label loopExit  
  load rc
  return 
}

Amplifier object design.

In this design each instance have following private static constants of type system.core.Amplifier PRIVATE_AMPLIFIER, PROTECTED_AMPLIFIER, INTERNAL_AMPLIFIER. All this constant are private, and they are set at class loading time.

All reflection methods, that get list of member descriptors or types, have additional variant with Amplifier parameter. For example Assembly.getTypes():List have variant Assembly.getTypes(amplifer:Amplifier):List, that require internal amplifier for assembly.

To use non public member info, user should supply a valid Amplifier. For example to set value of private variable user should supply private amplifer to amplifiable set method. For example: Field.set(a: Amplifier, value:object)

PRIVATE_AMPLIFIER implies having corresponding internal and protected amplifiers, so it is possible to use it to access internal or protected methods.

Considered alternatives

Amplified types design

In this design there are different types for public/protected/private/internal access. Client will be able to get access to private value only if it got boxed value of private type.

Problems with design

  • It is very easy to leak boxed value by code generators.

  • It is difficult to limit access to private type information. So bug querying is possible.

  • For every type there will be for type objects.

Good things with design

  • Delegation is by one instance, rather then universal.

  • It is possible to delegate amplified access to anyone.

Native method access

This is a description for C based implementation, implementation over CLI or over Java will differ.

There are to kinds of native members. vm_implemented members and types and native members. Implementation for vm_implemented members are provided by virtual machine. They all are in corelib library. They are primitive and could not be implemented in vm bytecodes. Arrays are example of vm implemented type and integer addition is example or vm_implemented operation.

Other kind native members are members that are implemented by user to access system resources. There are two levels of protection for them.

First there are extern methods, these methods provide signature of some method in external library. They also have additional information like calling convention or shared library name attached as custom attributes. All extern methods are private and static.

These extern methods could be called only from unsafe methods. Unsafe methods are implemented in bytecode and have additional operations that allow to fix object location direct memory access, type conversion, pointer arithmetic and able to call native or unsafe methods. All unsafe methods are also private methods, but could be static or instance methods.

Usually unsafe methods are just wrappers around extern methods that convert values to form that is acceptable to extern methods and process results of calls. They are introduced to avoid large amount of wrapper code that is practically the same for many platforms of one type.

All unsafe methods take Amplifier as first parameter. If amplifier is valid native amplifier, method is called, if it is not valid, unsafe method call is rejected. There is only one valid amplifier for VM. And this amplifier is kept in unsafe_amplifier register of VM. While anyone will be able to create instance of unsafe amplifier it will not be recognized as valid one because it will not match amplifier in VM register. This amplifier is passed to launcher component, then launcher creates resources that are needed for application and passes unsafe amplifier to other resources.

Because unsafe amplifier is very critical resource it should be normally passed only to constructors or to write only properties and stored in private variables. It is not possible to keep it in static variable as such could would not verify.

Native methods are loaded from library only when first method that will access them will be loaded. Loading them earilier could cause successfull attack with buggy libary or libary loading code.

Native method access ideology strogly depends on platform. For example EROS operating system have concept of component which is foundation for all other access to the system. There could be a layer that works with those components and give references to component wrappers to upper layer. The same could apply to l4 servers and its receive and send capabilities.

Application launching example in posix system

Lets consider hello world example. It will accept name as optional argument.

Example 3. HelloWorld.xcl


namespace Test {
  using System.Apps.Console
  class HelloWorld {
    extends ConsoleApp
    slot Name:string
    to public Run() {
      if(name == null) {
        AppConsole.Out.WriteLine("Hello, World!")
      } else {
        AppConsole.Out.WriteLine("Hello, "+Name+"!")
      }
    }
  }
}

Hello world will have its requirements described at following configuration file. Launcher will interpret this configuration file and run hello world application.

Example 4. HelloWorld.cfg


<config name="HelloWorld" kind="Excutable">

  <params>
    <param type="System.String" as="NAME" optional="true" default="NULL">
      <description>
        Aplication will count will count what you will supply here as your name.
      </description>
    </param>
  </params>

  <useprofile profile="system/CONSOLE">
    <request name="CONSOLE" as="CONSOLE"/>
      <reason>Writing application output</reason>
    </request>
  </useprofile>
  
  <useassembly file="HelloWorld.sxe" as="HelloWorld"/>

  <make assembly="HelloWorld" class="Test.HelloWorld" as="HelloApp">
    <arg><value ref="NAME"/></arg>
    <property name="AppConsole">
      <value ref="CONSOLE"/>
    </property>
  </make>

  <run obj="HelloApp" method="Run">
  </run>

</config>

The program depends on following classes in standard library.

Example 5. Standard library classes


namespace System.IO {

  class public Stream {
    // other methods
    to public abstract WriteLine(str:String)
  }

}

namespace System.Apps.Console {
  using system.io.Stream
  class final Console {
    to public init(i:Stream, o:Stream, e:Stream) {
      In = i
      Out = o
      Err = e
    }
    slot public final In:Stream 
    slot public final Out:Stream 
    slot public final Err:Stream 
  }

  class public ConsoleApp {
    slot _appConsole:Console
    slot AppConsole:Console {
      set public {
        _appConsole = value
      }
      get protected {
        _appConsole = value
      }
    }
  }
}

To implement those classes there will be following class in platform library.

Example 6. Posix OS classes


namespace Posix {

  class public FileDescriptorStream  {
    extends Stream
    slot final fd : int
    slot final _amplifer:Amplifier 
    to public init(a:Amplifer, fd:int) {
      _amplifer = a
      this.fd = fd
    }                          
    to public WriteLine(s:string) {
      val data = (s+"\n").ToBytes()
      // runtime will check if this method is valid one
      UnsafeWrite(_amplifer, data,0,data.length)
    }

    to unsafe UnsafeWrite(a:Amplifier, data:byte[], start:int, size:int):int {
      val dataPtr = pin(data) 
      try {
        // errno processing is missed here for simplicity
        return NativeWrite(fd, dataPtr, start, size)
      } finally {
        unpin(data)
      }  
    }

    // custom attributes that show linking
    #[LinkInfo("Win32","msvcrt.dll","_write","cdecl"]
    #[LinkInfo("Unix","libc.so","_write","cdecl"]
    #[LinkInfo("SomeSpecific-OS","libgnuc.so","WRITE","PASCAL"]
    to extern NativeWrite(fd:int, ptr:Ptr, start:int, size:int) :int
  }
}

HelloWorld.cfg will depend on system/CONSOLE.cfg. Console.cfg will provide capabilities need for console application. Note that console profiel does not export NATIVE_AMPLIFIER to context that use this profile. So cosole apps could not be configured to run arbitrary code.

Example 7. system/CONSOLE.cfg


<config name="CONSOLE" kind="Profile">

  <useprofile profile="system/NATIVE">
    <request name="NATIVE_AMPLIFIER" as="NATIVE_AMPLIFIER">
      <reason>Implementing posix environment</reason>
    </request>
  </useprofile>
  
  <useassembly file="posixenv.sxl" as="posix"/>
  <useassembly file="corelib.sxl" as="core"/>

  <make assembly="posixenv" class="Posix.FileDescriptorStream" as="SysIn">
    <arg><value ref="NATIVE_AMPLIFIER"/></arg>
    <arg><value type="System.Int32">0</value></arg>
  </make>

  <make assembly="posixenv" class="Posix.FileDescriptorStream" as="SysOut">
    <arg><value ref="NATIVE_AMPLIFIER"/></arg>
    <arg><value type="System.Int32">1</value></arg>
  </make>

  <make assembly="posixenv" class="Posix.FileDescriptorStream" as="SysErr">
    <arg><value ref="NATIVE_AMPLIFIER"/></arg>
    <arg><value type="System.Int32">2</value></arg>
  </make>

  <make assembly="corelib" class="System.Apps.Console" as="CONSOLE">
    <arg><value ref="SysIn"/></arg>
    <arg><value ref="SysOut"/></arg>
    <arg><value ref="SysErr"/></arg>
  </make>

  <provide name="CONSOLE">
    <value ref="CONSOLE"/>
  </provide>

</config>