l4-hurd
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Translucent storage: design, pros, and cons


From: Jonathan S. Shapiro
Subject: Translucent storage: design, pros, and cons
Date: Wed, 10 Jan 2007 06:39:07 -0500

This is NOT quite the model that Marcus has. Since I do not fully
understand his design, I have difficulty trying to speak about it. I
believe that this design satisfies all of his translucency objectives as
I understand them.

Marcus: if I make a mistake, please correct me, but please note that I
am trying very hard to capture your intentions as faithfully as I can
here, so please be patient with me.


I believe that the following modification to the EROS/Coyotos space bank
is sufficient to guarantee completely translucent storage. It does not
precisely follow Marcus's definition of parent and child (at least: not
as I understand it), but I think it is equivalent. Here is why:

  In order to create a process, I must supply the storage allocator
  from which the process is allocated. This requires that I hold a
  capability to the storage allocator. If the allocator is translucent,
  this guarantees that I can inspect the newly created process.

It also has the following property:

  If I hold a storage allocator, and I give you the right to use it
  (by transferring a capability) then *both* of us can inspect anything
  that you fabricate from it.

I am not sure if this property exists in Marcus's proposal.

The key point here is that if I "supply" resource I can inspect it.

I assume the following precondition: when a user account is created, the
initial capability to the per-user storage allocator is translucent.


DESIGN

The key change is a simple modification to the space bank logic:


1. There is a tree of translucent storage allocators. The root of this
   tree is TranslucentPrimeStore.

2. If I have a translucent storage allocator capability, I can allocate
   primitive objects from it (pages, nodes, things at that level).

   Conceptually, every allocation from a translucent store TSA implies
   an allocation from parent(TSA) as well. A parent allocator cannot
   give its children more storage than the parent has.

   Allocators can optionally implement limits (quotas). This does not
   concern us here.

3. A translucent storage allocator keeps track of its allocations. If a
   translucent storage allocator is destroyed, all storage that has been
   allocated by that allocator **and its children** is revoked.

[So far this is just the space bank. Here is the change for Marcus:]

4. The translucent storage allocator maintains some stable indexing
   scheme from 0 to n, where n is the number of objects that have been
   allocated from that storage allocator. There are methods:

     getNumAllocs();
     reallocate(index) => capability
       Given and index to a previous allocation, returns the same
       capability that was previously allocated (unless the capability
       has been revoked already).
     amplify(c1) => c2
       Takes a potentially restricted capability c1 and returns the
       corresponding "full strength" capability c2

   that allow the caller to obtain a capability to any object that has
   been allocated by this allocator (or its children).

   NOTE that this is sufficient to ensure that memory is fully
   transparent at all times, both for read and for write.

   NOTE I reserve the right to use a different indexing mechanism
   with equivalent properties if it makes the implementation easier.

Obviously this could be optimized to facilitate simpler inspection. We
can do that separately. I'm not sure that any optimization is necessary.
The important point right now is that it is *sufficient* to support
inspection.


So given this design, let us consider what I can inspect. Assume
(because it's a fun illustration) that I am a user running a shell, and
I am now starting a copy of DrmPlayer -- a program that wishes to
implement DRM.

To instantiate this process, I hand a storage allocator to a DrmPlayer
constructor. Because I supply the storage allocator, I can examine any
object that is allocated from this allocator. Because I know the
implementation of the constructor, I know what the first few allocated
objects will be. In particular, this means I know the index of the
process object and I can obtain a process capability to the running
instance of DrmPlayer. Given a process capability I can extract the
address space capability (or, for that matter, substitute a new address
space with modified code).

Unless I am missing something, it doesn't get very much more transparent
than that.

Now there are three things to say at this point:

  1. This design change is sufficient to satisfy Marcus's requirements.
     [I think]

  2. This design retains all of the process assembly and confinement
     properties of the constructor. What it changes is what I have
     called the "encapsulation property":

     Definition of the encapsulation property:

        A process only discloses information by sharing writable memory
        or sending messages.

     I call this the encapsulation property because it guarantees that
     clients of this object must respect and use the intended interface
     of the object.

  3. It can be implemented with a very minor change to the EROS/Coyotos
     space bank. I would compile this change out for the Coyotos OS, but
     I think it is enough to allow Hurd to use most of the Coyotos
     infrastructure **if desired**, and I am still trying to make that
     possible.


Okay. So I think I have established that I cannot create a *new* process
that conspires against me. That is: an instance is always translucent to
its creator.

So now let us consider how to build a DRM service in this system. The
answer is simple: install it as a daemon.

A daemon gets its storage allocator from the installer. This storage
allocator is not normally accessible to ordinary users, and IT CANNOT
BE.

If the installer's storage pool were accessible to ordinary users, then
it would be possible for any user to rewrite the address space image of
any system service program, up to and including "login". In consequence
the user could violate boundaries that we would normally prefer to
respect, such as user privacy boundaries when the users do not intend to
share information.

To put this another way: access to the installer's allocator is like
access to /dev/kmem, only more useful because it provides a cleanly
typed interface. In fact, there is so little difference (for purposes of
security) between the installer's allocator and the PrimeStore that we
might as well be making PrimeStore available instead. It has the
advantage that we will not confuse ourselves about what is being given
out.

So we will conclude that access to this store (the installer store or
the PrimeStore) should be restricted to the machine owner. To do this,
we will create a user account that serves as the machine owner identity,
and we will arrange for this account to have access to the installer
allocator.

There is a traditional name for this account: "root". [Though in fact
the powers obtained through the PrimeStore are more than root usually
has.]


There are good points and bad points to this design:

Bad:  The existence of a "root" account is generally viewed as a bad
      thing, because is a universal vulnerability.

      The major hazard of having this account is the hazard of politics
      and invasive law enforcement. In some places that is an acceptable
      risk. Not good in lots of countries I can think of, where the
      wrong data on your machine can get you killed.

Good: This root account is not the same as the UNIX root account. There
      is no equivalent to setuid, and no requirement that any equivalent
      to setuid should exist. This may be sufficient to address the
      concerns about root accounts.

Bad:  There are cases where this design gives too much authority. For
      example, I want the mail subsystem to be able to give me new
      incoming mail, but I probably do not want it to be able to read
      my existing mail (because mail daemons get penetrated). I
      *probably* want my mail to go into my own storage, but the
      design [so far] has no way for me to achieve all of this
      simultaneously.

Good: This can be fixed by introducing a new type of storage allocator
      capability that does not permit inspection. If a translucent
      parent has an opaque child, the holder of the *parent* capability
      can inspect objects allocated from the child, but the holder
      of the child capability cannot inspect objects allocated from the
      child. This permission is irreversible: an opaque allocator cannot
      have a translucent child.

Bad:  The defensible network stack design is completely defeated,
      because the supplier of storage can alter the storage. This
      means that the ethernet stack cannot rely on the data structure
      integrity of these structures.

Good: This can be fixed by changing the specification a bit:

        reallocate() returns full-strength capabilities only for
          process objects. It returns (transitively) read-only
          capabilities for everything else.

      Under this revision:

         + You can still debug any process that you create. Because
           of this, you have write access to anything that the process
           can write.

         + If you hand storage to a process that you did *not* create,
           you can inspect what it stores, but you cannot modify what
           it stores.

      I think that this remains sufficient to achieve Marcus's design
      objective, and it rescues the case where high-performance shared
      buffers are needed.

Risk: There is a risk of peer exposure. If I do not create sufficient
      sub-allocators, then I will end up with multiple processes coming
      from the same allocator that can inspect each other.

      This is unlikely because I will also lose the ability to
      separately destroy these processes. The normal pattern is "one
      process instantiation, one new sub-allocator". Given this,
      I do not consider this risk to be very large, but it needed to
      be identified.


A note on opaque sub-allocators:

This does NOT permit a child to refuse to run, because the creator can
seize control before the first instruction of the child (a small
modification to the constructor is required: "create paused yield", but
this is desirable for debugging in any case).


I have a followup usability question about this design, but before I get
in to that:

  Marcus: does this meet your design objectives?





reply via email to

[Prev in Thread] Current Thread [Next in Thread]