Subject: bug#45198: 28.0.50; Sandbox mode

From: Philipp Stephani <[email-masked]>
Date: Mon, 14 Dec 2020 16:37:42 +0100

Am Mo., 14. Dez. 2020 um 15:44 Uhr schrieb Stefan Monnier
<[email-masked]>:
>
> > In some way certainly, but it's not necessarily through stdout. I tend
> > to write potential output into a file whose filename is passed on the
> > command line. That's more robust than stdout (which often contains
> > spurious messages about loading files etc).
>
> Hmm... but that requires write access to some part of the file system,
> so it requires a finer granularity of control.

When using mount namespaces, that comes at a low cost: you mount
exactly those files that you want to write as writable filesystems,
and read-only files as read-only.

> I'd much rather limit
> the output to something equivalent to a pipe (the implementation of the
> sandboxing doesn't have to use stdout for that if that's a problem).

It's definitely true that only accessing open file descriptors is more
secure and simpler ("capability-based security"). However, I assume
that generic Elisp code doesn't work too well if it can't do things
like create temporary files. I've found 211 calls to make-temp-file in
the Emacs codebase alone, and I'd be surprised if none of them were
applicable to sandboxed processes.

>
> >> Also, I think the async option is the most important one.  How 'bout:
> >>     (sandbox-start FUNCTION)
> >>     Lunch a sandboxed Emacs subprocess running FUNCTION.
> > Passing a function here might be confusing because e.g. lexical
> > closures won't work.
>
> What makes you think they don't?

Hmm, you mean printing and reading closure objects? It might work,
though I don't know how portable/robust it is.

>
> > It might be preferable to pass a form and state
> > that both dynamic and lexical bindings are ignored.
>
> If closures turn out to be a problem I'd rather use FUNCTION + VALUES
> than FORM (using FORM implies the use of `eval`, and you have to think
> of all those kitten that'll suffer if we do that).

It is always eval, because that's how Elisp works. How else would you
deserialize and execute code than with read + eval?

>
> >>     Returns a process object.
> > Depending how much we care about forward compatibility, it might be
> > better to return an opaque sandbox object (which will initially wrap a
> > process object).
>
> We always use process objects to represent file-descriptors, so I can't
> find any good reason why this one should be different or why an
> implementation might find it difficult to expose a process object.

That's a fair point, but when thinking about forwards-compatibility we
might want to anticipate reasons that we currently can't think of.

>
> >>     FUNCTION is called with no arguments and it can use `sandbox-read`
> >>     to read the data sent to the process object via `process-send-string`,
> >>     and `sandbox-reply` to send back a reply to the parent process
> >>     (which will receive it via its `process-filter`).
> > That is, sandbox-read and sandbox-reply just read/write stdin/stdout?
>
> While it may use stdin/stdout internally, I can imagine good reasons why
> we'd want to use some other file descriptors.

If the process can write to stdout, then users will do that. Users
would probably just try whether 'print' works, and use it if it does.
So if you want a specialized function, then we'd need to make sure
that 'print' doesn't work (e.g. by having the parent process only read
the new pipe).

>
> > That would certainly work, but (a) it doesn't really have anything to
> > do with sandboxing, so these functions should rather be called
> > stdin-read and stdout-write or similar,
>
> I think "the right thing" would be to represent the parent as a process
> object inside the child.  I proposed dedicated functions only because
> but when it uses stdin/stdout, providing a process object seems awkward
> to implement.

It would be a file descriptor in all cases, so we might as well use a
pipe process (and introduce a function to write to a pipe). Stdout
isn't really different from other open file descriptors.
We'd need some special support for other file descriptors though, as
we'd need to make sure to not open them with O_CLOEXEC.

>
> >>     The sandboxed process has read access to all the local files
> >>     but no write access to them, nor any access to the network or
> >>     the display.
> > This might be a bit too specific. I'd imagine we'd want to restrict
> > reading files to the absolute minimum (files that Emacs itself needs
> > plus a fixed set of input files/directories known in advance), but
> > often allow writing some output files.
>
> I'm trying to design an API which can be made to work in as many
> circumstances as possible without imposing too high a maintenance
> burden.  So while I agree that it'd be better to limit the set of files
> that can be read and to allow writing to some files, I think I'd rather
> start with something more crude.
>
> We can refine it later if/when we have more experience with how it's
> used, and how it's implemented in the various OSes.

Even without specific experience, we can definitely say that
restricting something later is much harder than lifting restrictions.
So if we'd start with a policy that allows full read access, we'd have
a very hard time restricting that later. IOW, I'd be fine with not
providing write access to files (with the exception of maybe a
process-local tmpfs), but I'd try to restrict reading right from the
start to the installation directory and other known sources like the
load path.

>
> >> >> - I suspect we'll still want to use the extra "manual" checks I put in
> >> >>   my code (so as to get clean ELisp errors when bumping against the
> >> >>   walls of the sandbox, and because of the added in-depth security).
> >> > That's reasonable, though I'm worried that it will give users a false
> >> > sense of security.
> >> That would only be the case if we don't additionally use process-level
> >> isolation, right?
> > My worry is that people see a function like enter-sandbox and then
> > assume that Emacs will be secure after calling it, without actually
> > verifying the security implications.
>
> This seems universally true and hence suggests we should just forget
> about this idea of providing a sandbox functionality.  IOW I'm not sure
> what this has to do with the `ensure_no_sandbox` calls I'm suggesting
> we keep.

Seccomp and namespaces are battle-tested kernel-level security
features. If we use them, we'll have a much better chance at providing
an actually secure sandbox.

>
> > I've looked into this, and what I'd suggest for now is:
> > 1. Add a --seccomp=FILE command-line option that loads seccomp filters
> > from FILE and applies them directly after startup (first thing in
> > main). Why do this in Emacs? Because that's the easiest way to prevent
> > execve. When installing a seccomp filter in a separate process, execve
> > needs to be allowed because otherwise there'd be no way to execute the
> > Emacs binary. While there are workarounds (ptrace, LD_PRELOAD), it's
> > easiest to install the seccomp filter directly in the Emacs process.
> > 2. Generate appropriate seccomp filters using libseccomp or similar.
> > 3. In the sandboxing functions, start Emacs with bwrap to set up
> > namespaces and invoke Emacs with the new --seccomp flag.
>
> Sounds OK, tho I must say I don't understand why we care particularly
> about disallowing execve inside the bwrap jail.  AFAIK anything that an
> external process can do can also be done directly by Emacs since ELisp
> is a fairly fully-featured language (since there's nothing like setuid
> inside a bwrap jail).  I mean, I agree that we want to disallow running
> subprocesses, but can't think of a good reason why we would need this to
> be 100%, so we could rely on `ensure_no_sandbox` for that.

It's basically just another way to make the attack surface smaller and
provide defense in depth. If we allow execve, then suddenly the attack
surface increases to include all possible binaries that Emacs might
execute.
I've implemented the --seccomp flag, and it's really benign - mapping
the input file plus one or two syscalls. It's also strictly optional
and doesn't preclude using other technologies.