Friday, March 6, 2009

Capabilities for Python?

I received an email recently from Mark Miller, quoting a post from Zooko to the cap-talk mailing list (which I do not read). Mark asked me to clarify my position about capabilities (in Python, presumably). Since the last thing I need is another mailing list subscription, I'm posting my clarification here. I'm sure that through the magic of search engines it will find its way to the relevant places.

In his post, Zooko seems to believe that I am hostile to the very idea of capabilities, and seems to draw a link between this assumed attitude and my experience with the use of password-based capabilities in Amoeba. This is odd for several reasons. First, the way I remember it, Amoeba's capabilities weren't based on passwords, but on one-way functions and random numbers (and secure Ethernet wall-sockets, which is perhaps why the idea didn't catch on :-). Second, I don't believe my experience with capabilities in Amoeba made a difference in how I think about capabilities being offered by some modern programming languages like E, or about the various proposals over the years to add capabilities to Python, perhaps starting with an old proposal by Ka-Ping Yee and Ben Laurie. (It would be better to think of this as a subtraction rather than an addition, since such proposals invariably end up limiting the user to a substantially reduced subset of Python. More about that below.)

But the biggest surprise to me is that people are reading so much in my words. I'm not the Pope! I'm a hacker who likes to think aloud about design problems. Often enough I get it wrong. If you think you disagree with me, or have a question about what I said, just respond in the forum where I post (e.g. python-dev or python-ideas, or this blog), but please don't go forwarding my messages to lists I don't read and speculate about them.

With that off my mind, and with the caveat that this entire post is thinking alound, let me try to expose some of my current thoughts about capabilities and Python.

Note that I'm trying to limit myself to Python. Languages specifically written to support capabilities exist (e.g. E) and may well become successful, though I expect they will have a hard time gaining popularity until they also sprout some other highly attractive features: most developers see security as a necessary evil.

This attitude, of course, is the reason why the idea of adding security features to an existing language keeps coming back: it's assumed to be much more likely to convince the "unwashed masses" to switch to a slightly different version of a language they already know, than to get them to even try (let alone adopt) a wholly new language. This argument not limited to security zealots of course. The same reasoning is common in the larger world of "language marketing": C++ made compatibility with C a principle overruling all others, Java chose to resemble C or C++ for ease of adoption, and it is well known that Larry Wall picked many of Perl's syntactic quirks because the initial target audience was already using sed and sh.

I'll be the first to admit that wasn't completely free of this attitude for Python's design, although I didn't do it with the intent of gaining popularity: whenever I borrowed from another language, I did so either because I recognized a good idea, or because I didn't think I had anything to add to current practice, but not because I was concerned about market share. (If I had been, I wouldn't have used indentation for grouping. :-)

Anyway, regardless of the merits of this idea, it keeps coming back. A recent incarnation is Mark Seaborn's CapPython. Skimming through this wiki page it seems that Mark is well aware of the limitations: the section labeled "problem areas" takes up more than half of the page. And the most recent discussion (which also triggered Zooko's post I believe) started with a blog post by Tav where he proposes (with my encouragement) some modest additions to CPython's existing restricted execution mode and challenges the world to break into it. In a follow-up post, Tav provides a better history of this topic than I could provide myself.

And yet, I remain extremely skeptical of this whole area. The various attacks on Tav's supervisor code show how incredibly subtle it is to write a secure supervisor. CPython's restricted execution model lets sandboxed (= untrusted) code call into the supervisor, where the supervisor's Python code runs with full permissions. In Tav's version, the sandbox is given access to the supervisor only through a small collection of function objects which the supervisor passes into the sandbox. Tav's proposed changes remove some introspection attributes from function and class objects that would otherwise give the sandboxed code access to data or functions that the supervisor is trying to hide from the sandbox. This basic idea works well and nobody has yet found a way to break out of the sandbox directly -- so far it looks like no other attributes need to be removed in order to secure the sandbox.

However, several attacks found non-obvious weaknesses in Tav's supervisor code itself: it is deceptively easy to trick the supervisor into calling seemingly safe built-in functions with arguments carefully crafted by the code inside the sandbox so as to make it reveal a secret. This uses an approach that was devised years ago by Samuele Pedroni to dispell doubt that restricted execution was unsafe in Python 2.2 and beyond.

Samuele's approach combines two properties of (C)Python: built-ins invoked by the supervisor run with the supervisor's permissions, and there are many places in Python where implicit conversions attempt to call various specially-named attributes on objects given to them. The sandboxed exploit defines a class with one of these "magic" attributes set to some built-in, and voila, the built-in is called with the supervisor's permissions. It takes some added cleverness to pass an interesting argument to the built-in and to get the result back, but it can be done: for details, see Tav's blog.

My worry about this approach is that a supervisor that provides a reasonably large subset of Python will have to implement some pretty complex functionality: for example, you'll have to support a secure way to import modules. My confidence in the security of the supervisor goes down exponentially as the its complexity goes up. In other words, while Tav may be able to evolve the toy supervisor in "safelite.py" into an impenetrable bastion after enough iterations of exploit-and-patch, I don't think this approach will converge in a realistic timeframe (e.g. decades) for a more fully-featured supervisor.

This lets me segue into another, perhaps more generic, concern with the idea of providing a secure subset of Python, whether it's based on restricted execution, capabilities, or restricting attribute references (like CapPython and Zope's RestrictedPython). Python's claim to fame comes largely from its standard library. People's proficiency with the language is not just measured by how well they can construct efficient algorithm implementations using lists and dicts: to a large extent it depends on how much of the standard library they master as well. Python's standard library is large compared to many other languages. Only Java seems to have more stuff that's assumed to be "always there" (except in certain embedded environments).

For a "secure" version of Python to succeed, it will need to support most of the standard library APIs. I'm distinguishing between the implementations and APIs here, for it is likely that many standard library modules use features of the language that aren't available by the secure subset under consideration. This doesn't have to be a show-stopper as long as an alternate implementation can be provided that uses only the secure subset.

Unfortunately, I expect that, due to a combination of factors, it will be impractical to provide a sufficiently large subset of the standard library for a sufficiently secure subset of Python. One problem is that Python, being a highly dynamic language, it supports introspection at many levels, including some implementation-specific levels, like access to bytecode in CPython, which has no equivalent in Jython, IronPython or other implementations. Because of the language's dynamic and introspective features, there is often no real distinction between a module's API and its implementation. While this is an occasional source of frustration for Python users (see e.g. the recent discussion about asyncore on python-dev), in most cases it works quite well, and often APIs can be simpler because of certain dynamic features of the language. For example, there are several ways that dynamic attribute lookup can enhance an API: automatic delegation is just one of the common patterns that it enables; command dispatch is another. All this leads me to think that a secure version of Python is unlikely to become complete enough to attract enough users to become viable. I'd be happy to be proven wrong, but it seems that the people most attracted to the idea are hoping that adding capabilities Python will somehow provide a shortcut to success. Unfortunately, I don't think it's a shortcut at all.

I should mention that I have some experience in this area: Google's App Engine (to which I currently contribute most of my time) provides a "secure" variant of Python that supports a subset of the standard library. I'm putting "secure" in scare quotes here, because App Engine's security needs are a bit different than those typically proposed by the capability community: an entire Python application is a single security domain, and security is provided by successively harder barriers at the C/Python boundary, the user/kernel boundary, and the virtual machine boundary. There is no support for secure communication between mutually distrusting processes, and the supervisor is implemented in C++ (crucial parts of it live in a different process).

In the App Engine case, the dialect of the Python language supported is completely identical to that implemented by CPython. The only differences are at the library level: you cannot write to the filesystem, you cannot create sockets or pipes, you cannot create threads or processes, and certain built-in modules that would support backdoors have been disabled (in a few cases, only the insecure APIs of a module have been disabled, retaining some useful APIs that are deemed safe). All these are eminently reasonable constraints given the goal of App Engine. And yet almost every one of these restrictions has caused severe pain for some of our users.

Securing App Engine has required a significant use of internal resources, and yet the result is still quite limiting. Now consider that App Engine's security model is much simpler than that preferred by capability enthusiasts: it's an all-or-nothing model that pretty much only protects Google from being attacked by rogue developers (though it also helps to prevent developers from attacking each other). Extrapolating, I expect that a serious capability-based Python would require much more effort to secure, and yet would place many more constraints on developers. It would have to have a very attractive "killer feature" to make developers want to use it...Link

16 comments:

  1. I myself have toyed around with running python code in a sandbox, so I know how hard it is.

    I am thinking about a possible solution to the restricted Python issue.

    Looking at other languages that have been traditionally used in restricted environments, like JavaScript, I can see that importing modules and safe code almost seem to be mutually exclusive features. I can't think of any "safe" language providing a large standard library. It just introduces all the issues you mentioned above.

    I also see a problem with Pythons release cycle. The more security relevant features Python introduces, the more releases you will need to do, the earlier you will have to respond. Think XMLRPC issue * 100.

    Therefore I think that a proper solution would be a Python-like language (just like JavaScript is Java-like, syntax wise) packaged as a distribution, perhaps (if really needed) with a zero-day high frequency release scheme. It would not have, neither ever provide a standard library, and have restricted execution as its primary goal.

    I think it's illusory to believe that "safe" Python code would ever be easy to port to a restricted environment. A separate language ensures that this doesn't seem like a short cut.

    Yet I'm very fond of the Python syntax, and I'd like to see it repeated in future languages (like Boo and IronPython).

    Hope that made any sense.

    ReplyDelete
  2. I think the more important question going unasked here is: why is Python's standard library using so many unsafe features that it's this hard to secure it? Do these libraries really need to use such unsafe primitives?

    If that problem were fixed, the security would just naturally fall into place, without the attendent complexity and subsequent anxiety of trying ensure ambient, unsafe primitives are properly secured.

    I can't think of any "safe" language providing a large standard library. It just introduces all the issues you mentioned above.

    Because there are very few truly safe languages for mutually suspicious programming. Even still, the above isn't quite true. See the Joe-E language, which tames Java to be capability secure.

    Sure the process of taming Python might be a long and involved, but nothing worth doing is easy.

    ReplyDelete
  3. A little off topic for general security topics, but in the context of appengine I think this more limited proposal would actually work well (at least, it would work well enough for my use cases). Instead of going into and out of secure and insecure contexts, just permanently (at the process level) drop permissions that can't be resumed. Then use some kind of inter-process communication (which could be as simple as HTTP) to have those contexts interact.

    Of course, if there were support for some kind of light-weight process in Python (i.e., multiple interpreters per OS process) that'd also work (and be generally useful outside of security).

    ReplyDelete
  4. @Ian: I'm confused which specific proposal you're referring to. Your link points to comment #5, which doesn't seem to describe "a more limited proposal" but rather a possible application of the proposal. I've starred that issue, so it's probably better if we have that discussion there: http://code.google.com/p/googleappengine/issues/detail?id=671

    ReplyDelete
  5. This matter is covered on the Python Wiki:

    http://wiki.python.org/moin/SandboxedPython

    Surely the easiest cross-platform solution would be to link Python against libraries which effectively virtualise I/O and interactions with the system, although you'd then need to have a nice way of controlling the confined process, and I suppose people would want to do that using Python as well. Of course, going all the way down that road leads to more and more comprehensive operating system virtualisation solutions starting with jails and heading towards User Mode Linux, VirtualBox, VMware and Xen.

    ReplyDelete
  6. @Paul Boddie: that wiki page doesn't address capabilities, which typically are about different authorization domains within one process (IIUC).

    ReplyDelete
  7. Hey Guido (and others), I posted an update for those interested: http://tav.espians.com/update-on-securing-the-python-interpreter.html

    -- Cheers, tav

    ReplyDelete
  8. Your post is a good one. You correctly identify some of the most important issues, although I think you're probably somewhat wrong about what the actual usability costs would be. We all are somewhat wrong about that -- it is very hard to predict from first principles. Fortunately, Mark Seaborn has implemented a working version of CapPython so people can learn from experience.

    Also, of course, your GAE work yields useful data points, as you mentioned, although data points from a "sandboxing" system like GAE might or might not apply to a capability system. Capability systems hold out the hope of actually being *easier* to use than our current systems, instead of being harder to use, the way sandboxing systems are. I'm not asserting that it would necessarily work in practice, but most people misunderstand what it is intended to accomplish.

    You hit at least two nails on the head by naming module import and safe re-use of extant library code. Those are definitely hard. The capability community has been gaining some practical experience in safe re-use of extant library code over the last few years. They call it "taming", and have done quite a lot of it for Java (e.g. in the Joe-E project http://code.google.com/p/joe-e/ ) and JavaScript (e.g. in the Caja project http://code.google.com/p/google-caja/ ). Note that in principle you *can* continue to use standard library code which uses powerful authorities such as direct access to the local filesystem. The question is whether the way that that code behaves is going to lead to other code being able to get access to those authorities when it shouldn't -- that's "taming".

    My intuition is that it's not impossible to securely support the dynamic language features that you mention -- dynamic attribute lookup, delegation, command dispatch -- but I haven't thought it all the way through. The capability folks who've been doing that work on Java and JavaScript say that code which an object-oriented programmer would like is easy to tame, and that code which an object-oriented programmer would hate is hard to tame. For example, I would guess that Twisted, with its very stable, carefully specified API, doesn't tend to cause the kinds of problems that asyncore is causing in that python-dev thread you cite, and that Twisted would be correspondingly easy to tame.

    Okay, that's about all I have to say about that. I'm not asking you, or the Python team, to invest a lot of effort in this direction. I do want to see Mark Seaborn's CapPython project get some love, though. It is a working prototype -- perfect for banging on! But what do we do with it? Is it good for anything besides finding holes in it? Maybe tav has an idea.

    (It *would* have been nice if Python 3.0 hadn't made CapPython's job significantly harder for, as far as I could tell, no particular reason, but I guess it is too late to fix that now. http://lackingrhoticity.blogspot.com/2008/09/cappython-unbound-methods-and-python-30.html )

    ReplyDelete
  9. Maybe the people posting saying that this would be possible could indicate how much Python programming they have done?

    I find it very worrying when someone starts extrapolating from what "good OO code" would be, because if you took the kind of code I write in Python and moved it to Java (some of) it would be a nightmare. They are very different languages.

    ReplyDelete
  10. Guido: "that wiki page doesn't address capabilities, which typically are about different authorization domains within one process (IIUC)."

    I realised this after posting. Still, a lot of restricted execution efforts usually boil down to doing stuff that Tav put forward in his challenge - protecting external resources - because the capabilities stuff usually expires the patience of most people (who just want the simple stuff). Witness the introduction of SELinux and the widespread apathy that resulted, although I readily anticipate and accept any observations or criticism that SELinux isn't about capabilities as such, either. ;-)

    ReplyDelete
  11. Zooko, Mark Seaborn: would you mind having a discussion about unbound methods and their importance for CapPython on python-ideas@python.org? I'd love to tease out the significance of unbound methods for your approach (which currently baffles me), but not via blog comments.

    ReplyDelete
  12. Paul Boddie:

    Yes, access control schemes which require the user to do "double entry book-keeping" on everything -- first specifying what they want to do, and then specifying that they should be allowed to do it -- definitely get harder to use when they enforce finer-grained access controls. SELinux is a good example. I think Microsoft Vista's User Account Control is another example.

    However, the capability community has for many years been developing strong, fine-grained access control schemes which do *not* have this problem. They just published their latest research in this direction -- a working prototype of a peer-to-peer filesharing and file-syncing tool. The name of their paper about it is "Not One Click For Security":

    http://www.hpl.hp.com/techreports/2009/HPL-2009-53.html?mtxs=rss-hpl-tr

    This is why capability security might be a good fit for a nice simple dynamic language like Python -- Python programmers know (better than Java programmers, for example) that they don't need to do a lot of "double entry book-keeping" to get their work done.

    Andrew Cooke: You wrote "Maybe the people posting saying that this would be possible could indicate how much Python programming they have done?".

    I've done about a bazillion hours of Python programming. But Guido best argument isn't that a secure Python interpreter is impossible -- tav and Mark Seaborn are busily attempting to disprove that argument by example -- his best argument is that reducing usability (in terms of forbidding language features, especially module import) and reducing the usefulness of extant library code would make it "too heavy to take off". That may well turn out to be true.

    ReplyDelete
  13. At risk of adding gasoline to a raging fire, I'm just going to put this out there and see what happens... (FWIW this approach would probably NOT work in a hosted environment like appengine where performance is important.)

    Why not abandon the security issues of the interpreter *entirely* and instead consider a different solution: write a Python-to-Python compiler that instruments code by inserting checks - for example checking that only certain functions are called.

    This would substantially slow down code (maybe by a factor or 5x - 20x) but it has three key advantages:

    * All security issues are confined to the compiler - the security concern is centralized

    * Avoids the "supervisor mode / user mode" paradigm; instead untrusted code is "padded" and then run in supervisor mode, so the magic-method-called-in-supervisor-mode exploit does not apply.

    * A configurable "securing compiler" could be adapted to the different Python implementations: CPython 2.x, CPython 3.x, Jython, Ironpython, etc... without forking any of the original projects.

    ReplyDelete
  14. Great post Guido, and it's always great to see your thinking on language design issues.

    Small stupid thing I saw, a typo in your post: "All this lease me" probably meant to be "All this leads me".

    huge Python fan

    ReplyDelete
  15. what about this branch http://svn.python.org/view/python/branches/bcannon-objcap/ and the work beyond it?

    ReplyDelete
  16. Don't get suckered into reinventing the wheel. Linux capabilities are fine grained enough to get a security literate administrator to create a security template that would do everything you need without writing a line of code. Combined with python's exceptional exception reporting any hole's and incompatible restrictions to the capabilities can easily be tweaked to perfection.

    http://en.wikipedia.org/wiki/Security-Enhanced_Linux

    http://www.gentoo.org/proj/en/hardened/rsbac/overview.xml?style=printable

    http://www.ibm.com/developerworks/library/l-posixcap.html?ca=dgr-lnxw01POSIX-capabilities

    It's the administrator's job to set security policy. Not the programming language or command.

    ReplyDelete

Note: Only a member of this blog may post a comment.