14

I want to run a command on Linux in a way that it cannot create or open any files to write. It should still be able to read files as normal (so an empty chroot is not an option), and still be able to write to files already open (especially stdout).

Bonus points if writing files to certain directories (i.e. the current directory) is still possible.

I’m looking for a solution that is process-local, i.e. does not involve configuring things like AppArmor or SELinux for the whole system, nor root privileges. It may involve installing their kernel modules, though.

I was looking at capabilities and these would have been nice and easy, if there were a capability for creating files. ulimit is another approach that would be convenient, if it covered this use case.

3
  • Too many programs assume that they are able to write files as a matter of course (and fail in strange ways when they can't). strace tells you what files the program is opening. Why do you want to do this? Is it a specific program, or do you want this for testing or something else? Can you run the program as a user/group which doesn't have permission to write almost everywhere except in the current directory? Modern Linux disttributions use the idea of a group for each user, so this should be relatively easy to set up.
    – vonbrand
    Feb 13, 2013 at 11:48
  • It is a special program (Isabelle) that interprets code in a somewhat safe way already (no arbitrary code execution), but still allows the code to create files in arbitrary places. As the code is untrusted, I want to prevent this from happening (by aborting the program). The program already runs as a special user, but I’d feel safer if the code could not clobber up, say, /tmp or similar places. Feb 13, 2013 at 11:50
  • You could add a new user, to run the app in. Apr 16, 2015 at 14:11

8 Answers 8

10

How about creating an empty chroot, then bind-mount the main filesystem as read-only inside the chroot?

Should probably be something like this to create a read-only bind-mount:

mount --bind /foo/ /path/to/chroot/
mount -o remount,ro /path/to/chroot/

You can bind-mount other directories which you want the jail to have write access to as well. Be careful if you need to bind-mount special directories (/dev/, /proc/, /sys/), mounting them as-is may be insecure.

2
  • Again, needs root privileges and other ”global setup”. But an option, yes. Feb 13, 2013 at 17:07
  • Is /foo/ the path to the main file system? Jul 9, 2017 at 14:47
5

It seems that the right tool for this job is fseccomp Based on sync-ignoringf code by Bastian Blank, I came up with this relatively small file that causes all its children to not be able to open a file for writing:

/*
 * Copyright (C) 2013 Joachim Breitner <mail@joachim-breitner.de>
 *
 * Based on code Copyright (C) 2013 Bastian Blank <waldi@debian.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#define _GNU_SOURCE 1
#include <errno.h>
#include <fcntl.h>
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define filter_rule_add(action, syscall, count, ...) \
  if (seccomp_rule_add(filter, action, syscall, count, ##__VA_ARGS__)) abort();

static int filter_init(void)
{
  scmp_filter_ctx filter;

  if (!(filter = seccomp_init(SCMP_ACT_ALLOW))) abort();
  if (seccomp_attr_set(filter, SCMP_FLTATR_CTL_NNP, 1)) abort();
  filter_rule_add(SCMP_ACT_ERRNO(EACCES), SCMP_SYS(open), 1, SCMP_A1(SCMP_CMP_MASKED_EQ, O_WRONLY, O_WRONLY));
  filter_rule_add(SCMP_ACT_ERRNO(EACCES), SCMP_SYS(open), 1, SCMP_A1(SCMP_CMP_MASKED_EQ, O_RDWR, O_RDWR));
  return seccomp_load(filter);
}

int main(__attribute__((unused)) int argc, char *argv[])
{
  if (argc <= 1)
  {
    fprintf(stderr, "usage: %s COMMAND [ARG]...\n", argv[0]);
    return 2;
  }

  if (filter_init())
  {
    fprintf(stderr, "%s: can't initialize seccomp filter\n", argv[0]);
    return 1;
  }

  execvp(argv[1], &argv[1]);

  if (errno == ENOENT)
  {
    fprintf(stderr, "%s: command not found: %s\n", argv[0], argv[1]);
    return 127;
  }

  fprintf(stderr, "%s: failed to execute: %s: %s\n", argv[0], argv[1], strerror(errno));
  return 1;
}

Here you can see that it is still possible to read files:

[jojo@kirk:1] Wed, der 06.03.2013 um 12:58 Uhr Keep Smiling :-)
> ls test
ls: cannot access test: No such file or directory
> echo foo > test
bash: test: Permission denied
> ls test
ls: cannot access test: No such file or directory
> touch test
touch: cannot touch 'test': Permission denied
> head -n 1 no-writes.c # reading still works
/*

It does not prevent deleting files, or moving them, or other file operations besides opening, but that could be added.

A tool that enables this without having to write C code is syscall_limiter.

1
  • 4
    Note that secure approach is to whitelist syscalls, not to blacklist them. If too much is denied, external unsanboxed helpers can be used to assist the program. With LD_PRELOAD such helpers can be made transparent to the program we running.
    – Vi.
    Mar 6, 2013 at 18:08
4

Would you consider writing a substitute to open(…) function, and loading it using LD_PRELOAD?

4
  • 2
    You probably mean open... Well, I would consider using an existing solution that uses this approach, yes. Feb 13, 2013 at 12:18
  • 2
    There is something like this at github.com/certik/restrict, but it is configured by compiling and doesn’t seem to be in widespread use. Feb 13, 2013 at 12:27
  • Yes, sorry, my mistake, updating the answer… But it seems to me you will have to substitute one for the write(…) as well.
    – Leonid
    Feb 13, 2013 at 13:34
  • As for the github.com/certik/restrict, yes, you're completely right.
    – Leonid
    Feb 13, 2013 at 13:37
3

The simplest solution is probably a wrapper program that creates a new filesystem namespace with the relevant filesystems mounted read-only and then execs the program you are trying to restrict.

This is what systemd does when you use ReadOnlyDirectories= to mark certain directories as read-only for a service. There is also an unshare command in util-linux that can do the work of creating a new namespace, so you can do something like:

unshare -m <wrapper>

where wrapper would then just have to remount filesystems as required before starting the real target program.

The only problem is that you need to be root to create the new namespace...

3
  • I thought about this. But is this possible without being root? Is there a ready made script/program for that available? Feb 13, 2013 at 12:13
  • 1
    Yes, it seems that you do need to be root, at least with the 3.7 kernel.
    – TomH
    Feb 13, 2013 at 12:27
  • I was looking further at this solution. It is possible to recursively bind-mount / to a new /, but not and recursivley mark it as read-only. Feb 13, 2013 at 19:28
3

Doing some initial setup as root is really the easiest way. Specifically, a chroot into a read-only bind mount is the path of least resistance.

You can use bindfs instead of mount --bind to create the read-only view without needing to be root. However, you do need to do something as root to prevent access to other files, such as chroot.

Another approach is to LD_PRELOAD a library that hooks into file opening and refuses to allow writing. This requires no special privileges. From a security perspective, this can be bypassed, but it's ok for your use case where you only need to contain a specific feature and not arbitrary native code. I don't know of an existing library for this, however. LD_PRELOAD could also be used to confine the program to the read-only view created with mount --bind or bindfs; again, I don't know of an existing library.

On Debian and derivatives, you can set up a schroot environment. Schroot is setuid root and needs to be configured as root, but can be executed by any authorized user.

A method that doesn't require any cooperation from root is to run the process in a virtual machine. You could set up KVM or VirtualBox, or user-mode Linux. It's a bit heavyweight, and will mean extra memory consumption, but shouldn't affect the speed of raw symbolic computation significantly.

How to "jail" a process without being root? might provide some inspiration.

2

You could run it in a chroot, mounting special versions of /tmp and such inside. Perhaps systemd is of help, and particularly systemd-nspawn(1), which looks just like what you want.

2

A virtual machine would make it possible for the script to write anywhere without affecting the host system, and to inspect where it's actually trying to write to, which seem to be the goals.

For example, you can easily start up Arch Linux with

kvm -boot d -m 512 -cdrom archlinux-*.iso
1
  • 1
    I still want to run the program in the current machine to avoid having to set up a new system, new environment etc. A virtual machine is way too heavyweight for my use case. Feb 13, 2013 at 12:33
1

One way to at least prevent the process from writing the files (but not from creating them) is to call ulimit -f 0 first. This will abort the process as soon as it tries to write to a file, but creating empty files is still possible.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.