How to prevent a process from writing files

7,547

Solution 1

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 <[email protected]>
 *
 * Based on code Copyright (C) 2013 Bastian Blank <[email protected]>
 *
 * 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.

Solution 2

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.

Solution 3

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

Solution 4

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.

Solution 5

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...

Share:
7,547

Related videos on Youtube

Joachim Breitner
Author by

Joachim Breitner

Updated on September 18, 2022

Comments

  • Joachim Breitner
    Joachim Breitner almost 2 years

    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.

    • vonbrand
      vonbrand over 11 years
      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.
    • Joachim Breitner
      Joachim Breitner over 11 years
      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.
    • ctrl-alt-delor
      ctrl-alt-delor about 9 years
      You could add a new user, to run the app in.
  • Joachim Breitner
    Joachim Breitner over 11 years
    I thought about this. But is this possible without being root? Is there a ready made script/program for that available?
  • Joachim Breitner
    Joachim Breitner over 11 years
    You probably mean open... Well, I would consider using an existing solution that uses this approach, yes.
  • Joe Conlin
    Joe Conlin over 11 years
    Yes, it seems that you do need to be root, at least with the 3.7 kernel.
  • Joachim Breitner
    Joachim Breitner over 11 years
    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.
  • Joachim Breitner
    Joachim Breitner over 11 years
    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.
  • Leonid
    Leonid over 11 years
    Yes, sorry, my mistake, updating the answer… But it seems to me you will have to substitute one for the write(…) as well.
  • Joachim Breitner
    Joachim Breitner over 11 years
    Again, needs root privileges and other ”global setup”. But an option, yes.
  • Joachim Breitner
    Joachim Breitner over 11 years
    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.
  • Vi.
    Vi. over 11 years
    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.
  • Wayne Conrad
    Wayne Conrad almost 7 years
    Is /foo/ the path to the main file system?