├── LICENSE ├── README.md └── scratch /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Victor Michel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scratch 2 | 3 | `scratch` is a tool that creates a shell environment where most filesystem modifications are not persisted. 4 | 5 | It leverages [overlayfs](https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html) to redirect all filesystem changes to a temporary filesystem in `/tmp/scratch-envs`. 6 | 7 | ## Why? 8 | 9 | Routine actions sometimes require too high of a commitment: 10 | * Afraid a system update may screw up the package manager, and want get a feel of things first? 11 | * Want to temporarily install something that brings a million dependencies, and want a way to completely clean it up without having to remember how? 12 | * Want to do `rm -rf *` for therapy or for fun, but without living the consequences? 13 | * Afraid of the impact of a `git` command, and want to try it first to see its effects? 14 | 15 | In all of these cases, `scratch` can help. Actions perpetrated inside the scratch session will not visible outside. 16 | 17 | ## How to use `scratch`? 18 | 19 | Simply do `sudo scratch `, where `` is a name of your choice to identify the new environment. A bash session inside that scratch environment will be started, and can be exited at any moment (with `exit`, CTRL-D, etc). 20 | 21 | Changes made inside a scratch environment are "persisted" at `/tmp/scratch-envs/`. From outside the environment, changes are visible in `/tmp/scratch-envs//top/`. 22 | 23 | It is possible to exit and re-enter the environment, resuming where things were left off. But because `/tmp/scratch-envs` will likely be erased on reboot, the environments won't survive a reboot. Though you are free to change the location to a persistent filesystem (not in `/tmp`, which is usually cleared on reboot). 24 | 25 | ## Warning! Warning! 26 | 27 | * Use at your own risk! 28 | * It is truly disgusting code. The extent of my testing is "it works on my laptop". 29 | * You may forget that you are inside (or outside) a scratch environment. Which means you may accidentally lose data (and time). 30 | * This is especially problematic if you do not have a way to see if you're inside or outside a scratch environment. You may feel emboldened enough to do `rm -rf /` outside a scratch environment, making your system unusable. Similarly, you may do all sort of nice things inside a scratch environment, only to realize you have to do it all over again outside. 31 | * If you're modifying files inside the environment, things may be slow. Touching one byte of a single file requires copying and rewriting the whole file in the overlay upper directory. 32 | * Do not expect scratch environments to stay stable over time. Because the base filesystem can also change underneath, the overlay filesystem will eventually be unusable due to inconsistencies. Scratch environments are meant to be relatively short-lived, they're not something you can rely upon for long-term stability. 33 | * Everything may not work. Some software is smart enough to realize the treachery (`systemd`, for example) and will refuse to run. Some will just be confused (like software installed by `snap`). Your mileage may vary. 34 | * `scratch` may fail to create an overlay on top of some mountpoints (due to weird mount topologies or overlayfs limitations). It should say so when it does, but you need to be extra-careful. Before doing anything destructive, make sure the overlay functionality works in the location you want to make changes in. 35 | 36 | It is strongly recommended to display scratch information in your shell. For example, you can add something like this in your `.bashrc` to modify the prompt: 37 | ``` 38 | if [[ -f /etc/scratch-environment ]]; then 39 | PS1='\[\033[01;32m\]\u@\[\033[01;31m\]$(cat /etc/scratch-environment 2>/dev/null)\[\033[00m\]: \[\033[01;34m\]\w\[\033[00m\]\$ ' 40 | else 41 | PS1='\[\033[01;32m\]\u@\[\033[01;31m\]\h\[\033[00m\]: \[\033[01;34m\]\w\[\033[00m\]\$ ' 42 | fi 43 | ``` 44 | 45 | This makes the shell prompt display the hostname when outside a scratch environment, and display the name of the scratch environment when inside. It works by reading `/etc/scratch-environment`, which is populated by `scratch` in every scratch environment. This file contains the name of the current environment (and should not exist outside scratch environments). 46 | 47 | 48 | ## Example 49 | 50 | Let's look at some files (`test1`, `test2`), enter a scratch environment and modify/delete them, and crerate a new one. 51 | 52 | After exiting the environment, all changes are gone. 53 | 54 | ``` 55 | vic@lapdog: ~/test$ cat test1 56 | test1 57 | vic@lapdog: ~/test$ cat test2 58 | test2 59 | vic@lapdog: ~/test$ sudo scratch noconsequences 60 | [...] 61 | Entering scratch environment noconsequences 62 | ----------------------------- 63 | vic@noconsequences: ~/test$ cat test1 64 | test1 65 | vic@noconsequences: ~/test$ cat test2 66 | test2 67 | vic@noconsequences: ~/test$ rm test1 68 | vic@noconsequences: ~/test$ echo abcd > test2 69 | vic@noconsequences: ~/test$ touch test3 70 | vic@noconsequences: ~/test$ ls 71 | test2 test3 72 | vic@noconsequences: ~/test$ cat test2 73 | abcd 74 | vic@noconsequences: ~/test$ 75 | exit 76 | ----------------------------- 77 | Leaving scratch environment noconsequences 78 | vic@lapdog: ~/test$ ls 79 | test1 test2 80 | vic@lapdog: ~/test$ cat test1 81 | test1 82 | vic@lapdog: ~/test$ cat test2 83 | test2 84 | vic@lapdog: ~/test$ ls -al /tmp/scratch-envs/noconsequences/top/home/vic/test/ 85 | total 4 86 | drwxr-xr-x 2 vic vic 100 Sep 10 19:06 . 87 | drwxr-xr-x 4 vic vic 140 Sep 10 19:43 .. 88 | c--------- 1 root root 0, 0 Sep 10 19:06 test1 # The "deleted" marker for test1 89 | -rw-r--r-- 1 vic vic 5 Sep 10 19:06 test2 90 | -rw-r--r-- 1 vic vic 0 Sep 10 19:06 test3 91 | ``` 92 | 93 | ## How does it work? 94 | 95 | What `scratch` does is: 96 | 1. Inside a new child mount namespace, create an overlay filesystem for every mount point (including the root filesystem), with the upper dir in `/tmp/scratch-envs`. 97 | There are some exceptions - notably, `/dev`, `/proc`, `/sys` and `/run` are bind mounts, not overlay mounts. This is to ensure the system stays usable in common cases. 98 | 2. Create and enter another child mount namespace, where the root is the root of the overlay filesystem for `/`. Think of it as a `chroot` step. 99 | 100 | -------------------------------------------------------------------------------- /scratch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | die() { 4 | echo "$1" >&2 5 | exit 1 6 | } 7 | 8 | die_usage() { 9 | die "Usage: scratch " 10 | } 11 | 12 | check_requirement() { 13 | local command="$1" 14 | type -P "$command" >/dev/null || die "This script requires the command '$command'" 15 | } 16 | 17 | check_requirement unshare 18 | check_requirement sha256sum 19 | check_requirement flock 20 | check_requirement id 21 | check_requirement findmnt 22 | check_requirement mount 23 | check_requirement bash 24 | check_requirement awk 25 | 26 | 27 | [[ $# == 1 ]] || die_usage 28 | SCRATCH_NAME="$1" 29 | [[ -z "${SCRATCH_NAME}" ]] && die_usage 30 | [[ $(id -u) == 0 ]] || die "Must be root" 31 | 32 | # By default, all writes will go to the following dir. Feel free to change the location. 33 | SCRATCH_ROOT="/tmp/scratch-envs" 34 | mkdir -p $SCRATCH_ROOT 35 | 36 | LOGIN_AS=${SUDO_USER-root} 37 | CWD=$(/bin/pwd -P) 38 | SCRATCH_MARKER="/etc/scratch-environment" 39 | SCRATCH_ENV_ROOT="${SCRATCH_ROOT}/${SCRATCH_NAME}" 40 | 41 | [[ -f ${SCRATCH_MARKER} ]] && die "Cannot enter a scratch environment from another one" 42 | 43 | mkdir -p ${SCRATCH_ENV_ROOT} 44 | cd ${SCRATCH_ENV_ROOT} 45 | 46 | # Can't open 2 sessions to the same env. Otherwise it opens the doot to very confusing and unexpected behaviors. 47 | ( 48 | flock -nx 42 || die "Scratch environment ${SCRATCH_NAME} already in use" 49 | 50 | # Don't mess with the current mount namespace. Do all our hacks inside an expendable mount namespace, 51 | # so all mounts will be automatically cleaned up when the user exits the shell session. 52 | unshare -m bash << EOF 53 | # Horrible heredoc. Most '$' need to be escaped. Terribad! 54 | mkdir -p top/etc merge work 55 | 56 | # Write marker so it can be read by the shell 57 | echo ${SCRATCH_NAME} > top${SCRATCH_MARKER} 58 | 59 | # Overlay rootfs 60 | echo "Adding overlay for /" 61 | mount -t overlay overlay -o lowerdir=/,upperdir=top,workdir=work merge 62 | 63 | # Overlay all other mountpoints 64 | while read line; do 65 | eval "\$line" 66 | [[ \$TARGET == "/" ]] && continue 67 | [[ \$FSTYPE == "nsfs" ]] && { echo "Ignoring \$TARGET of type nsfs"; continue; } 68 | [[ \$FSTYPE == "vfat" ]] && { echo "Ignoring \$TARGET of type vfat"; continue; } 69 | [[ \$FSTYPE == "overlay" ]] && { echo "Ignoring \$TARGET of type overlay"; continue; } 70 | [[ \$TARGET == ${SCRATCH_ROOT}* ]] && { echo "Skipping \$TARGET inside scratch fs root"; continue; } 71 | if [[ \$TARGET =~ ^/sys ]] || [[ \$TARGET =~ ^/proc ]] || [[ \$TARGET =~ ^/dev ]] || [[ \$TARGET =~ ^/run ]]; then 72 | echo "Not overlaying \$TARGET, will bind mount" 73 | continue 74 | fi 75 | # Flatten overlay space. 76 | # Generate predictable IDs so the scratch environment can be exited and re-entered later without loss of state. 77 | id=\$(echo "\$TARGET" | sha256sum | awk '{print \$1}' | head -c 32) 78 | mkdir -p top-\${id} work-\${id} 79 | echo "Adding overlay for \${TARGET}" 80 | mount -t overlay overlay -o lowerdir=\${TARGET},upperdir=top-\${id},workdir=work-\${id} merge\${TARGET} 81 | done < <(findmnt -P) 82 | 83 | echo "Bind-mounting /sys" 84 | mount --rbind /sys merge/sys 85 | echo "Bind-mounting /dev" 86 | mount --rbind /dev merge/dev 87 | echo "Bind-mounting /proc" 88 | mount --rbind /proc merge/proc 89 | echo "Bind-mounting /run" 90 | mount --rbind /run merge/run 91 | 92 | CWD="${CWD}" # More heredoc madness 93 | if ! [[ -d merge\${CWD} ]]; then 94 | # Fallback to home directory if the current directory does not exist in the overlay. 95 | CWD=$(echo ~${LOGIN_AS}) 96 | fi 97 | 98 | echo 99 | echo "Entering scratch environment ${SCRATCH_NAME}" 100 | echo "-----------------------------" 101 | unshare -m -R merge -w merge\${CWD} /usr/bin/env bash -c "sudo -u ${LOGIN_AS} bash < /dev/tty" 102 | 103 | echo "-----------------------------" 104 | echo "Leaving scratch environment ${SCRATCH_NAME}" 105 | EOF 106 | ) 42>/var/lock/scratch-${SCRATCH_NAME}.lock 107 | 108 | --------------------------------------------------------------------------------