How to prevent a process from writing files

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.

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

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.

Method 2

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 <<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="acc1cdc5c0ecc6c3cdcfc4c5c181cedec9c5d8c2c9de82c8c9">[email protected]</a>>
 *
 * Based on code Copyright (C) 2013 Bastian Blank <<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="15627479717c557170777c747b3b7a6772">[email protected]</a>>
 *
 * 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 filtern", argv[0]);
    return 1;
  }

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

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

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

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

[<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="b2d8ddd8ddf2d9dbc0d9">[email protected]</a>: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.

Method 3

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

Method 4

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…

Method 5

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.

Method 6

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.

Method 7

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

Method 8

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.


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x