Creating a capabilities-based restricted execution system

S

Sean R. Lynch

I've been playing around with Zope's RestrictedPython, and I think I'm
on the way to making the modifications necessary to create a
capabilities-based restricted execution system. The idea is to strip out
any part of RestrictedPython that's not necessary for doing capabilities
and do all security using just capabilities.

The basic idea behind capabilities is that you don't give any piece of
code you don't trust a reference to something you don't want it to have
access to. You use proxies instead (E calls them "facets").

In order to be able to allow untrusted code to create proxy objects, I
needed to be able to store a reference to the proxied object in a
private attribute.

To create private attributes, I'm using "name mangling," where names
beginning with X_ within a class definition get changed to
_<uuid>_<name>, where the UUID is the same for that class. The UUIDs
don't need to be secure because it's not actually possible to create
your own name starting with an underscore in RestrictedPython; they just
need to be unique across all compiler invocations.

The nice thing about using this name mangling is that it's only done at
compile time and doesn't affect runtime performance. An interesting side
effect is that code defined on a class can access private attributes on
all descendants of that class, but only ones that are defined by other
code on that class, so this isn't a security issue.

I was thinking I needed read-only attributes to be able to avoid
untrusted code's being able to sabotage the revoke method on a proxy
object, but I'm thinking that just keeping around a reference to the
revoke method in the original code may be enough.

Does anyone think I'm going in completely the wrong direction here? Am I
missing anything obvious?
 
P

Paul Rubin

Sean R. Lynch said:
Does anyone think I'm going in completely the wrong direction here? Am
I missing anything obvious?

Well, I have a dumb question. Have you studied the security failures
of rexec/Bastion and convinced yourself that they don't happen to your
new scheme?

You might look at the PyPy architecture doc if you haven't yet.
Making a separate object space for restricted objects may fit PyPy's
design quite naturally.
 
S

Sean R. Lynch

Paul said:
Well, I have a dumb question. Have you studied the security failures
of rexec/Bastion and convinced yourself that they don't happen to your
new scheme?

If you know of a location where the known shortcomings of rexec are
documented, please let me know. So far I've only seen a couple examples
and a lot of people saying "it's not secure so let's disable it."

My current methodology is to be very careful about adding any privileges
beyond what RestrictedPython allows.
You might look at the PyPy architecture doc if you haven't yet.
Making a separate object space for restricted objects may fit PyPy's
design quite naturally.

I have looked at PyPy. It's very interesting, but RestrictedPython is
already written and in use in Zope.

I think I've figured out a way to use my name mangling scheme to make
attributes only *writable* by code defined on a class from which an
object descends: do writes through a name-mangled method, and have
RestrictedPython output self._mangled_setattr(attr, val) for each
attempted attribute assignment. This will basically make it impossible
to have attributes that are writable from other classes, but I think
it's probably a prerequisite for capabilities. Most other languages
require attributes to be set via methods anyway, right?
 
M

Martin v. Loewis

Sean said:
If you know of a location where the known shortcomings of rexec are
documented, please let me know. So far I've only seen a couple examples
and a lot of people saying "it's not secure so let's disable it."

The biggest problem is that new-style classes are both available through
the type() builtin, and callable to create new instances.

For example, if you have managed to open a file object f, then

type(f)("/etc/passwd").read()

lets you access a different file, bypassing all machinery that may
have been designed to prevent that from happening.

Of course, for the specific case of file objects, there is additional
machinery preventing that from happening, but in the general case,
there might be more problems in that area. For example,
object.__subclasses__() gives you access to quite a lot of stuff.

Regards,
Martin
 
J

John Roth

[...]
Does anyone think I'm going in completely the wrong direction here? Am I
missing anything obvious?

Yes, you're missing something really obvious. Multi-level
security is a real difficult problem if you want to solve it
in a believable (that is, bullet-proof) fashion. The only way
I know of solving it is to provide separate execution
environments for the different privilege domains.
In the current Python structure, that means different
interpreters so that the object structures don't intermix.

If you have separate domains, then the only support
needed is to remove privileged modules from the
built-ins, and virtualize import so that it won't load
modules that aren't on the approved list for that
domain.

You also, of course, need some form of gate between
the untrusted and trusted domains.

Once that's done, there's no reason to layer additional
complexity on top, and there is no reason to restrict
any introspection facilities.

John Roth
 
S

Sean R. Lynch

John said:
Yes, you're missing something really obvious. Multi-level
security is a real difficult problem if you want to solve it
in a believable (that is, bullet-proof) fashion. The only way
I know of solving it is to provide separate execution
environments for the different privilege domains.
In the current Python structure, that means different
interpreters so that the object structures don't intermix.

Hmmm, can you give me an example of a Python application that works this
way? Zope seems to be doing fine using RestrictedPython.
RestrictedPython is, in fact, an attempt to provide different execution
environments within the same memory space, which is the whole point of
my exercise. Now, I know that the lack of an example of insecurity is
not proof of security, but can you think of a way to escape from
RestrictedPython's environment? DoS is still possible, but as I'm not
planning on using this for completely untrusted users, I'm not too
concerned about that.
 
S

Sean R. Lynch

Martin said:
The biggest problem is that new-style classes are both available through
the type() builtin, and callable to create new instances.

For example, if you have managed to open a file object f, then

type(f)("/etc/passwd").read()

lets you access a different file, bypassing all machinery that may
have been designed to prevent that from happening.

Of course, for the specific case of file objects, there is additional
machinery preventing that from happening, but in the general case,
there might be more problems in that area. For example,
object.__subclasses__() gives you access to quite a lot of stuff.

RestrictedPython avoids this by removing the type() builtin from the
restricted __builtins__, and it doesn't allow untrusted code to create
names that start with _. Zope3 has a type() builtin, but it returns a
proxy (written in C) to the type object to prevent access.

Right now I'm providing a same_type function instead to compare types.
Later I'll probably start playing around with C proxies.

I think the main thing that's liable to introduce new security problems
(beyond what RestrictedPython may already have) is the fact that
RestrictedPython is mostly designed to protect the trusted environment
from the untrusted environment, and what I'd really like to do is give
programmers in the untrusted environment a way to create objects and
pass them around to one another; for example, in the original setup,
class statements are allowed but not very useful in the restricted
environment, because objects created from those classes would be
read-only due to the fact that you can't create any special attributes
to tell the system how to handle security from within the restricted
environment, which is why I'm adding private attributes to the system
and figuring out a way to allow methods defined on a class to assign to
attributes on instances of that class without allowing all code to do so.
 
S

Serge Orlov

Sean R. Lynch said:
I've been playing around with Zope's RestrictedPython, and I think I'm
on the way to making the modifications necessary to create a
capabilities-based restricted execution system. The idea is to strip out
any part of RestrictedPython that's not necessary for doing capabilities
and do all security using just capabilities.

The basic idea behind capabilities is that you don't give any piece of
code you don't trust a reference to something you don't want it to have
access to. You use proxies instead (E calls them "facets").

"Don't give" sounds good in theory but fails in practice. You can't prevent
leakage 100%, so any security system _must_ help programmer to keep
trusted data away from untrusted code. Do you know that rexec failed
exactly because it didn't help to prevent leakage?
In order to be able to allow untrusted code to create proxy objects, I
needed to be able to store a reference to the proxied object in a
private attribute.

To create private attributes, I'm using "name mangling," where names
beginning with X_ within a class definition get changed to
_<uuid>_<name>, where the UUID is the same for that class. The UUIDs
don't need to be secure because it's not actually possible to create
your own name starting with an underscore in RestrictedPython; they just
need to be unique across all compiler invocations.

This is a problem: you declare private attributes whereas you should be
declaring public attributes and consider all other attributes private. Otherwise
you don't help prevent leakage. What about doing it this way:

obj.attr means xgetattr(obj,acc_tuple) where acc_tuple = ('attr',UUID)
and xgetattr is
def xgetattr(obj,acc_tuple):
if not has_key(obj.__accdict__,acc_tuple):
raise AccessException
return getattr(obj,acc_tuple[0])

__accdict__ is populated at the time class or its subclasses are created.
If an object without __accdict__ is passed to untrusted code it will
just fail. If new attributes are introduced but not declared in __accdict__
they are also unreachable by default.
The nice thing about using this name mangling is that it's only done at
compile time and doesn't affect runtime performance. An interesting side
effect is that code defined on a class can access private attributes on
all descendants of that class, but only ones that are defined by other
code on that class, so this isn't a security issue.

I was thinking I needed read-only attributes to be able to avoid
untrusted code's being able to sabotage the revoke method on a proxy
object, but I'm thinking that just keeping around a reference to the
revoke method in the original code may be enough.

Does anyone think I'm going in completely the wrong direction here? Am I
missing anything obvious?

It depends on what type of security do you want. Did you think about DOS
and covert channels? If you don't care about that, yeah, you don't miss
anything obvious. <wink> you should worry whether you miss something
non-obvious.

By the way, did you think about str.encode? Or you are not worried about
bugs in zlib too?

-- Serge.
 
S

Sean R. Lynch

I hate replying to myself, but I've written some more code. I hope to
have something posted soon so people can rip it apart without needing to
resort to conjecture :)

I had been considering using a name-mangled setattr for doing attribute
assignment to only allow assignment to attributes on descendants of the
class one was writing methods on, but it occurred to me that I could
probably treat "self" as a special name using only compiler
modifications, so I could eliminate RestrictedPython's need to turn all
Getattrs and AssAttrs (shouldn't it be GetAttr) into method calls. Now,
of course, I'm limited to static checks on names to control access, but
Python already disallows, for example, access to f.func_globals, and
RestrictedPython disallows names that begin with underscore.

Now I need to write a bunch of code that uses this system and attempts
to break it :)
 
S

Sean R. Lynch

Serge said:
"Don't give" sounds good in theory but fails in practice. You can't prevent
leakage 100%, so any security system _must_ help programmer to keep
trusted data away from untrusted code. Do you know that rexec failed
exactly because it didn't help to prevent leakage?

Hmm, this is good information. I think it will probably change the way
I've been looking at this.
In order to be able to allow untrusted code to create proxy objects, I
needed to be able to store a reference to the proxied object in a
private attribute.

To create private attributes, I'm using "name mangling," where names
beginning with X_ within a class definition get changed to
_<uuid>_<name>, where the UUID is the same for that class. The UUIDs
don't need to be secure because it's not actually possible to create
your own name starting with an underscore in RestrictedPython; they just
need to be unique across all compiler invocations.


This is a problem: you declare private attributes whereas you should be
declaring public attributes and consider all other attributes private. Otherwise
you don't help prevent leakage. What about doing it this way:

obj.attr means xgetattr(obj,acc_tuple) where acc_tuple = ('attr',UUID)
and xgetattr is
def xgetattr(obj,acc_tuple):
if not has_key(obj.__accdict__,acc_tuple):
raise AccessException
return getattr(obj,acc_tuple[0])

__accdict__ is populated at the time class or its subclasses are created.
If an object without __accdict__ is passed to untrusted code it will
just fail. If new attributes are introduced but not declared in __accdict__
they are also unreachable by default.

This is very interesting, and you may convince me to use something
similar, but I don't think you're quite correct in saying that the
name-mangling scheme declares private attributes; what is the difference
between saying "not having X_ in front of the attribute makes it public"
and "having X_ in front of the attribute makes it private?"
It depends on what type of security do you want. Did you think about DOS
and covert channels? If you don't care about that, yeah, you don't miss
anything obvious. <wink> you should worry whether you miss something
non-obvious.

I am not (particularly) concerned about DoS because I don't plan to be
running anonymous code and having to restart the server isn't that big
of a deal. I do plan to make it hard to accidentally DoS the server, but
I'm not going to sacrifice a bunch of performance for that purpose. As
for covert channels, can you give me an example of what to look for?

I am certainly worried about non-obvious things, but my intent wasn't to
put up a straw man, because if I ask if I'm missing non-obvious things,
the only possible answer is "of course."
By the way, did you think about str.encode? Or you are not worried about
bugs in zlib too?

Well, it'll only take *one* problem of that nature to force me to go
back to converting all attribute accesses to function calls. On the
other hand, as long as any problem that allows a user to access
protected data is actually a in (zlib, etc), I think I'm not going to
worry about it too much yet. If there is some method somewhere that will
allow a user access to protected data that is not considered a bug in
that particular subsystem, then I have to fix it in my scheme, which
would probably require going back to converting attribute access to
method calls.
 
M

Martin v. =?iso-8859-15?q?L=F6wis?=

Sean R. Lynch said:
RestrictedPython avoids this by removing the type() builtin from the
restricted __builtins__, and it doesn't allow untrusted code to create
names that start with _.

Ah, ok. That might restrict the usefulness of the package (perhaps
that is what "restricted" really means here :).

People would not normally consider the type builtin insecure, and
might expect it to work. If you restrict Python to, say, just integers
(and functions thereof), it may be easy to see it is safe - but it is
also easy to see that it is useless.

The challenge perhaps is to provide the same functionality as rexec,
without the same problems.

Regards,
Martin
 
S

Sean R. Lynch

Martin said:
Ah, ok. That might restrict the usefulness of the package (perhaps
that is what "restricted" really means here :).

People would not normally consider the type builtin insecure, and
might expect it to work. If you restrict Python to, say, just integers
(and functions thereof), it may be easy to see it is safe - but it is
also easy to see that it is useless.

The challenge perhaps is to provide the same functionality as rexec,
without the same problems.

Well, I'm providing a same_type function that compares types. What else
do you want to do with type()? The other option is to go the Zope3 route
and provide proxies to the type objects returned by type().
 
J

John Roth

Sean R. Lynch said:
Hmmm, can you give me an example of a Python application that works this
way? Zope seems to be doing fine using RestrictedPython.
RestrictedPython is, in fact, an attempt to provide different execution
environments within the same memory space, which is the whole point of
my exercise. Now, I know that the lack of an example of insecurity is
not proof of security, but can you think of a way to escape from
RestrictedPython's environment? DoS is still possible, but as I'm not
planning on using this for completely untrusted users, I'm not too
concerned about that.

Restricted Python was withdrawn because of a number of
holes, of which new style classes were the last straw. I don't
know what the exact holes were.

Whether Zope security is subject to those holes is a question
I can't answer (and I don't find it all that interesting, anyway.)
The Restricted Execution environment's disabling access to
__dict__ seems a bit ham-handed, but I suspect that it was
simply the easiest way around one major difficulty. The Bastion
hook (which is what I believe Zope security is built on top of)
seems to be reasonably adequate. The rest of it probably
needs to be rethought.

John Roth
 
S

Serge Orlov

Sean R. Lynch said:
Serge said:
Sean R. Lynch said:
To create private attributes, I'm using "name mangling," where names
beginning with X_ within a class definition get changed to
_<uuid>_<name>, where the UUID is the same for that class. The UUIDs
don't need to be secure because it's not actually possible to create
your own name starting with an underscore in RestrictedPython; they just
need to be unique across all compiler invocations.


This is a problem: you declare private attributes whereas you should be
declaring public attributes and consider all other attributes private. Otherwise
you don't help prevent leakage. What about doing it this way:

obj.attr means xgetattr(obj,acc_tuple) where acc_tuple = ('attr',UUID)
and xgetattr is
def xgetattr(obj,acc_tuple):
if not has_key(obj.__accdict__,acc_tuple):
raise AccessException
return getattr(obj,acc_tuple[0])

__accdict__ is populated at the time class or its subclasses are created.
If an object without __accdict__ is passed to untrusted code it will
just fail. If new attributes are introduced but not declared in __accdict__
they are also unreachable by default.

This is very interesting, and you may convince me to use something
similar, but I don't think you're quite correct in saying that the
name-mangling scheme declares private attributes; what is the difference
between saying "not having X_ in front of the attribute makes it public"
and "having X_ in front of the attribute makes it private?"

You're right, the wording is not quite correct. My point was that it should
take effort to make attributes _public_, for example, going to another
source line and typing attribute name or doing whatever "declaration" means.
This way adding new attribute or leaking "unsecured" object will
raise an exception when untrusted code will try to access it. Otherwise
one day something similar to rexec failure will happen: somebody
added __class__ and __subclasses__ attributes and rexec blindly
allowed to access them. The leading underscore doesn't matter, it
could be name like encode/decode that is troublesome.

By the way, my code above is buggy, it's a good idea that you're
not going to use it :) Let me try it the second time in English words:
If the attribute 'attr' is declared public give it. If the function with
UUID has access to attribute 'attr' on object 'obj' give it. Otherwise fail.

I am not (particularly) concerned about DoS because I don't plan to be
running anonymous code and having to restart the server isn't that big
of a deal. I do plan to make it hard to accidentally DoS the server, but
I'm not going to sacrifice a bunch of performance for that purpose. As
for covert channels, can you give me an example of what to look for?

Nevermind, it's just a scary word :) It can concern you if you worry
about information leaking from one security domain to another. Like
prisoners knocking the wall to pass information between them. In
computers it may look like two plugins, one is processing credit
cards and the other one has capability to make network connections.
If they are written by one evil programmer the first one can "knock
the wall" to pass the information to the second. "knocking the wall"
can be encoded like quick memory allocation up to failure = 1, no
quick memory allocation = 0. Add error correction and check summing
and you've got a reliable leak channel.
I am certainly worried about non-obvious things, but my intent wasn't to
put up a straw man, because if I ask if I'm missing non-obvious things,
the only possible answer is "of course."


Well, it'll only take *one* problem of that nature to force me to go
back to converting all attribute accesses to function calls. On the
other hand, as long as any problem that allows a user to access
protected data is actually a in (zlib, etc), I think I'm not going to
worry about it too much yet. If there is some method somewhere that will
allow a user access to protected data that is not considered a bug in
that particular subsystem, then I have to fix it in my scheme, which
would probably require going back to converting attribute access to
method calls.

I'm not sure how to deal with str.encode too. You don't know what
kind of codecs are registered for that method for sure, one day there
could be registered an unknown codec that does something unknown.
Shouldn't you have two (or several) codecs.py modules(instances?)
for trusted and untrusted code? And str.encode should be transparently
redirected to the proper one?

-- Serge.
 
M

Martin v. Loewis

Sean said:
Well, I'm providing a same_type function that compares types. What else
do you want to do with type()? The other option is to go the Zope3 route
and provide proxies to the type objects returned by type().

I don't know what is needed, but I know that existing code will break
when it did not strictly need to - no existing code uses your function.

If you think your users can accept rewriting their code - fine.

Regards,
Martin
 
J

John Roth

Aahz said:
RestrictedPython was *not* withdrawn; rexec was withdrawn. This is a
difficult enough issue to discuss without confusing different modules. See
http://dev.zope.org/Wikis/DevSite/Projects/SupportPython21/RestrictedPython

I'm not sure what you're trying to say. The Zope page you reference
says that they were (prior to 2.1) doing things like modifying generated
byte code and reworking the AST. That's fun stuff I'm sure, but it
doesn't have anything to do with "Restricted Execution" as defined in the
Python Library Reference, Chapter 17, which covers Restricted Execution,
RExec and Bastion (which was also withdrawn.)

If I confused you with a subtle nomenclature difference, sorry. I don't
care what Zope is doing or not doing, except for the fact that it seems
to come up in this discussion. I'm only concerned with what Python is
(or is not) doing. The approach in the Wiki page you pointed to does,
however, seem to be a substantially more bullet-proof approach than
Python's Restricted Execution.

John Roth
--
Aahz ([email protected]) <*> http://www.pythoncraft.com/

Weinberg's Second Law: If builders built buildings the way programmers wrote
programs, then the first woodpecker that came along would destroy
civilization.
 
S

Sean R. Lynch

Serge said:
You're right, the wording is not quite correct. My point was that it should
take effort to make attributes _public_, for example, going to another
source line and typing attribute name or doing whatever "declaration" means.
This way adding new attribute or leaking "unsecured" object will
raise an exception when untrusted code will try to access it. Otherwise
one day something similar to rexec failure will happen: somebody
added __class__ and __subclasses__ attributes and rexec blindly
allowed to access them. The leading underscore doesn't matter, it
could be name like encode/decode that is troublesome.

By the way, my code above is buggy, it's a good idea that you're
not going to use it :) Let me try it the second time in English words:
If the attribute 'attr' is declared public give it. If the function with
UUID has access to attribute 'attr' on object 'obj' give it. Otherwise fail.

Ok, I think you've pretty much convinced me here. My choices for
protected attributes were to either name them specially and only allow
those attribute accesses on the name "self" (which I treat specially),
or to make everything protected by default, pass all attribute access
through a checker function (which I was hoping to avoid), and check for
a special attribute to define which attributes are supposed to be
public. Do you think it's good enough to make all attributes protected
as opposed to private by default?
Nevermind, it's just a scary word :) It can concern you if you worry
about information leaking from one security domain to another. Like
prisoners knocking the wall to pass information between them. In
computers it may look like two plugins, one is processing credit
cards and the other one has capability to make network connections.
If they are written by one evil programmer the first one can "knock
the wall" to pass the information to the second. "knocking the wall"
can be encoded like quick memory allocation up to failure = 1, no
quick memory allocation = 0. Add error correction and check summing
and you've got a reliable leak channel.

Hmmm, I think this would be even more difficult to protect from than
doing resource checks. Fortunately, I'm not planning on processing any
credit cards with this code. The primary purpose is so that multiple
programmers (possibly thousands) can work in the same memory space
without stepping on one another.
I'm not sure how to deal with str.encode too. You don't know what
kind of codecs are registered for that method for sure, one day there
could be registered an unknown codec that does something unknown.
Shouldn't you have two (or several) codecs.py modules(instances?)
for trusted and untrusted code? And str.encode should be transparently
redirected to the proper one?

I guess I'll just make attributes protected by default, and force the
programmer to go out of their way to make things public. Then I can use
the Zope/RestrictedPython technique of assuming everything is insecure
until proven otherwise, and only expose parts of the interface on
built-in types that have been audited.

Thank you very much for your extremely informative responses!
 
A

Aahz

I'm not sure what you're trying to say. The Zope page you reference
says that they were (prior to 2.1) doing things like modifying generated
byte code and reworking the AST. That's fun stuff I'm sure, but it
doesn't have anything to do with "Restricted Execution" as defined in the
Python Library Reference, Chapter 17, which covers Restricted Execution,
RExec and Bastion (which was also withdrawn.)

If I confused you with a subtle nomenclature difference, sorry. I don't
care what Zope is doing or not doing, except for the fact that it seems
to come up in this discussion. I'm only concerned with what Python is
(or is not) doing. The approach in the Wiki page you pointed to does,
however, seem to be a substantially more bullet-proof approach than
Python's Restricted Execution.

Well, I don't care what you do or don't care about, but I do care that
if you're going to post in a thread that you actually read what you're
responding to and that you post accurate information. If you go back to
the post that started this thread, it's quite clear that the reference
was specifically to Zope's RestrictedPython.
 
J

John Roth

Aahz said:
Well, I don't care what you do or don't care about, but I do care that
if you're going to post in a thread that you actually read what you're
responding to and that you post accurate information. If you go back to
the post that started this thread, it's quite clear that the reference
was specifically to Zope's RestrictedPython.

I beg to differ. That's what the OP said he started with, not what he's
mostly interested in nor where he wants to end up. I'm including
the first paragraph below:

[extract from message at head of thread]
I've been playing around with Zope's RestrictedPython, and I think I'm
on the way to making the modifications necessary to create a
capabilities-based restricted execution system. The idea is to strip out
any part of RestrictedPython that's not necessary for doing capabilities
and do all security using just capabilities.
[end extract]

To me, at least, it's clear that while he's *started* with a consideration
of Zope's RestrictedPython, he's talking about what would be needed
in regular Python. If he was focusing on Zope, this is the wrong forum
for the thread.

My fast scan of Zope yesterday showed that it was quite impressive,
but some of the things they did seem to be quite specific to the Zope
environment, and don't seem (at least to me) to be all that applicable
to a general solution to having some form of restricted execution.

Much of this thread has focused on "capabilities" and the use of
proxies to implement capabilities. AFIAC, that's not only putting
attention on mechanism before policy, but it's putting attention on
mechanism in the wrong place.

What I *haven't* seen in this thread is very much consideration of
what people want from a security implementation. I've seen that in
some other threads, which included some ways of taming exec and
eval when all you want is a data structure that contains nothing but
known kinds of objects. You don't, however, need exec and eval
for that if you're willing to use the compiler tools (and, I presume,
take a substantial performance hit.)

One problem I've been playing around with is: how would you
implement something functionally equivalent to the Unix/Linux
chroot() facility? The boundaries are that it should not require
coding changes to the application that is being restricted, and it
should allow any and all Python extension (not C language
extension) to operate as coded (at least as long as they don't
try to escape the jail!) Oh, yes. It has to work on Windows,
so it's not a legitimate response to say: "use chroot()."

John Roth
--
Aahz ([email protected]) <*> http://www.pythoncraft.com/

Weinberg's Second Law: If builders built buildings the way programmers wrote
programs, then the first woodpecker that came along would destroy
civilization.

Have to be a pretty non-aggressive woodpecker, if it allowed something
that extensive and complicated to happen before attacking it.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,764
Messages
2,569,567
Members
45,041
Latest member
RomeoFarnh

Latest Threads

Top