l4-hurd
[Top][All Lists]
Advanced

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

secure exec


From: Marcus Brinkmann
Subject: secure exec
Date: Thu, 22 May 2003 19:54:29 +0200
User-agent: Mutt/1.5.3i

Hi,

I got a bit confused in the last mails, and I think it is better if I lay
down my proposal clearly from scratch.  I am not convinced by the lastc
ouple of suggestions you made.  Some were not addressing all problems, some
were introducing new concepts.

I will give hints about implementation.  In particular, I think that using
task handles can be light-weight in the task server, because it won't use
the same handle library as all other tasks.  This is because it provides
security to itself, of course.

The task server will receive messages at a fixed thread number, that only
depends on the subsystem and current cpu (its thread number is subsystem
bits bitwise-OR current cpu number, its version ID is 1).  The object
handles it provides are built up like this:

       17 reserved bits - 1 bit control? - 14 bits task ID

The control bit is 0 if this handle is only for task ID reference counting
and other unprivileged operations.  It is 1 if this task handle is the
privileged one (ie, it gives you control over a task).  The task server will
internally make both IDs refer to the same object, and only differentiate
the privilege within this object.

The 17 reserved bits can actually be used internally for reference counting,
for example.  The task object could have a list of unprivileged task handle
owners, and each such element in the list contains 1 word, built up this
way:
      16 bits refcount - 2 unused bits - 14 bits task ID

This is only an implementation hint.  How it is done exactly is not so
important, as long as it looks like object handles to the outside.  The task
server will also provide a handle movement interface like the one needed for
normal handle transfer.  I mean the transaction records.  The transaction
record serves to transfer or expand the control privilege to other tasks.

Note that in the current Hurd design, task handles are also used to
authorize certain signals and other things.  I am not sure I want to keep it
that way, but I don't see a reason to prematurely make the task server very
different from any other server.

The task server creates tasks as empty (ie, no actual address space is
created), and provides a task_revoke call that can be used to revoke access
to a task and only keep access for the caller.  This together with a normal
task status inquiry (ie, something that allows to find out if a task is
still empty) is enough for me to implement the below protocol.  Anything
else, like if tasks live on if the last thread is destroyed I don't want nor
need to decide here.

I only describe secure exec, normal exec is easier.

The code of the user is something like this.  I leave out all parameters etc
that are not required for the protocol.

  new_task = task_create ();
  new_task_id = task_id_get_handle (new_task);
  task_request_transaction_record (new_task, &trans);
  err = file_exec (file, task_server, get_obj_id(new_task), trans, &thread_id);
  if (get_task_id_from_thread (thread_id) != get_task_id (new_task))
    err = Esomethingbadhappened;
  if (err)
    {
       task_destroy_transaction_record (task_server, new_task, trans);
       /* Might already have been done by fileserver, but we don't know.  */
       task_destroy (new_task);
       task_id_release_handle (new_task_id);
       return err;
    }

  /* Blocking send.  */
  send_startup_message (thread_id, inherited_fds, inherited_stuff, ...,
                        some_handle_that_allows_new_task_to_steal_pid);
  /* Crawl into corner and wait for stab of mercy.  */
  while (1)
    l4_wait ();   /* Or halt this thread, if possible to halt yourself. */
  /* Not reached.  */

This has the following properties:
* The filesystem does not get any privilege it shouldn't get.  In fact it
  only gets control over the new_task.
* The accounting ID is correct, in particular in that time frame where the
  filesystem doesn't have a task handle anymore (it gives up control right
  before returning to the old task).
* The new task does not get any extra privilege.  Note that new task does
  not even need the privilege to control old_task, it only needs the privilege
  to steal the PID and other things that are inherited.  The actuall killing
  of the old task should be done by the proc server.  This is important, IMO: 
  if a process running as foo got later some privilege for user bar, and
  then starts a suid executable as baz, then baz should not get any
  privilege from bar that happens to remain in foo at the time of the exec.
  Even if foo is so sloppy that it didn't release this privilege before
  exec().

The filesystem server does this:

error_t
file_exec (file, task_server, new_task_obj_id, trans, &thread_id)
{
  if (task_server != my_trusted_task_server)
    return EINVAL;
  err = task_use_transaction_record (task_server, new_task_obj_id, trans,
                                     &new_task);
  if (!err)
    err = task_revoke (new_task);
  if (!err)
    err = task_get_state (new_task, &state);
  if (state != just_created_and_empty)
    {
      /* We trust the filesystem to do this here.  But if it really wanted
         to steal the the task, it could do it anyway.  This is still better
         than having to wait for the user to accept the task we give to it.  */
      task_destroy (new_task);
      return EINVAL;
    }
  /* The task is all ours now.  */
  thread_id = setup_task (new_task);
  /* thread_id now is the thread that will wait for the user's startup
     message.  */
  return thread_id;
}

The only grief I have with this is that the filesystem can steal the task
the user created and use it for its own purpose, or just leak it.  However,
as the filesystem itself controls the content of the task to be created,
that is the case anyway.  So there is no way out of this.  If you start a
suid exec, you trust the filesystem with the resulting task to some extent.
Considering this, having the task to be accounted with the filesystem would
be more honest to the user, as the user can not control what happens next.

But then, you can also think of suid programs to be a very complex service
provided to the user.  And as such, it should be accounted to the user, and
not to the server (and the issue of trust holds true for all such services).

Note that in the above code, the server does not rely on any interaction
with anything but the task server that it trusts and the new task.
The new task interaction can be secured by inserting startup code into the
new task, that does what the server wants.  So this is entirely secure
and robust (or can be).

Now, what does the new task do, either voluntarily or because the inserted
code was programmed this way?

_secure_startup ()
{
  creator = get_bootstrap_handle ();
  /* This might either just do a simple receive, or it might do a send and
     receive.  This is only dependent on the protocol internal to filesystem
     and new_task and must match what setup_task does in the filesystem
     server.  As a consequence of the protocol, this new_task receives a
     handle to the task id of the old task _before_ the filesystem server can
     release the old_task ID handle it possesses, ie before it returns to
     the old task.
     This will exchange some handles with the filesystem, but only handles
     to trusted servers.  This includes the task server and a trusted proc.  */
  do_startup (creator, &trusted_proc, &old_task_id);
  /* Now wait for the old task's start up message.  This will include
     arbitrary handles and data.  */
  receive_startup_message (old_task_id, &inherited_stuff, &pid_steal_auth);
  /* This will do the magic of killing the old task and replacing it with us
     process-wise.  */
  proc_steal_pid (trusted_proc, pid_steal_auth, &new_pid_port);

  /* Note: If anything above fails, it's too late to do anything about it.
     We will just crash.  We might be able to notify the proc server about
     it to give some status code, but that's all.  */
  /* Now do a normal startup.  */
  _startup ();
}

So that's it.  As you can see, I don't need any new concepts, and it does
exactly what we want.

Thanks,
Marcus

-- 
`Rhubarb is no Egyptian god.' GNU      http://www.gnu.org    address@hidden
Marcus Brinkmann              The Hurd http://www.gnu.org/software/hurd/
address@hidden
http://www.marcus-brinkmann.de/




reply via email to

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