summaryrefslogtreecommitdiff
path: root/tmpoverlay.sh
diff options
context:
space:
mode:
Diffstat (limited to 'tmpoverlay.sh')
-rwxr-xr-xtmpoverlay.sh243
1 files changed, 243 insertions, 0 deletions
diff --git a/tmpoverlay.sh b/tmpoverlay.sh
new file mode 100755
index 0000000..9ca2d00
--- /dev/null
+++ b/tmpoverlay.sh
@@ -0,0 +1,243 @@
+#!/bin/sh
+
+usage() {
+ [ "$1" = 0 ] || exec >&2
+ cat << EOF
+usage: tmpoverlay [OPTIONS] [SOURCE...] DEST
+
+Create a tmpfs-backed overlayfs at DEST starting with SOURCEs. If no SOURCE is
+specified, use DEST as the source. To free the memory, simply umount DEST.
+
+options:
+ -c, --no-canonicalize don't canonicalize paths
+ -h, --help print this help
+ -o, --overlayfs OPTS add overlayfs mount options, e.g. redirect_dir/metacopy
+ -n, --no-mtab don't write to /etc/mtab
+ -N, --mount-name NAME source name for mount (default "overlay")
+ -t, --tmpfs OPTS add tmpfs mount options, e.g. size
+ -v, --verbose verbose mode
+
+examples:
+ tmpoverlay / /new_root # make a thin copy of root
+ tmpoverlay /etc # make read-only /etc writable
+ tmpoverlay /a /b /c /merged # merge /a, /b, /c, and a fresh tmpfs
+ tmpoverlay / # USE WITH CAUTION, see docs
+ unshare -Umc --keep-caps sh -c 'tmpoverlay . && exec \$SHELL' # make cwd writable as non-root
+EOF
+ exit "$1"
+}
+
+log() {
+ # not equivalent to printf "tmpoverlay: $@"
+ printf 'tmpoverlay: ' >&2
+ printf "$@" >&2
+ printf '\n' >&2
+}
+
+logv() {
+ [ -z "$verbose" ] || log "$@"
+}
+
+cmd() {
+ logv '%s ' "$@"
+ "$@"
+}
+
+unset tmpdir
+die() {
+ r=$?
+ [ "$r" != 0 ] || r=1
+ [ "$#" = 0 ] || log "$@"
+ [ -z "$tmpdir" ] || { exec 9>&-; wait; }
+ exit $r
+}
+
+canon() {
+ if [ -n "$no_canon" ]; then
+ printf '%s\n' "$1"
+ else
+ realpath "$1"
+ fi
+}
+
+my_getopt() {
+ getopt \
+ -l no-canonicalize \
+ -l help \
+ -l overlayfs: \
+ -l no-mtab \
+ -l mount-name: \
+ -l tmpfs: \
+ -l verbose \
+ -n tmpoverlay \
+ -- \
+ cho:nN:t:v \
+ "$@"
+}
+
+args=$(my_getopt "$@") || usage 1
+eval set -- "$args"
+unset args
+
+unset no_canon extra_ovl_opts no_mtab mount_name tmpfs_opts verbose
+while true; do
+ case "$1" in
+ -c|--no-canonicalize) no_canon=-c; shift;;
+ -h|--help) usage 0;;
+ -o|--overlayfs)
+ [ -n "$2" ] && extra_ovl_opts="$extra_ovl_opts,$2"
+ shift 2
+ ;;
+ -n|--no-mtab) no_mtab=-n; shift;;
+ -N|--mount-name) mount_name=$2; shift 2;;
+ -t|--tmpfs)
+ [ -n "$2" ] && tmpfs_opts="${tmpfs_opts:+$tmpfs_opts,}$2"
+ shift 2
+ ;;
+ -v|--verbose) verbose=-v; shift;;
+ --) shift; break;;
+ *) die "getopt failure"
+ esac
+done
+
+[ $# != 0 ] || usage 1
+
+unset lowerdir
+while [ "$#" != 1 ]; do
+ d=$(canon "$1")
+ if [ -h "$d" ] || ! [ -d "$d" ]; then
+ die 'source "%s" is not a directory' "$d"
+ fi
+ lowerdir=${lowerdir+:}$d
+ shift
+done
+
+dest=$(canon "$1")
+[ -d "$dest" ] || die 'destination "%s" is not a directory' "$dest"
+[ "$dest" != / ] || log 'overmounting root, use with caution'
+
+[ -n "$lowerdir" ] || lowerdir=$dest
+
+logv 'creating tmpdir'
+tmpdir=$(cmd umask 077; cmd mktemp -dt tmpoverlay.XXXXXXXXXX) || die
+logv 'created tmpdir: %s' "$tmpdir"
+# won't trigger on overmounting /, which is actually correct
+case "$tmpdir" in
+ "$dest"/*) log "warning: tmpdir cannot be cleaned up after overmounting $dest"
+esac
+cmd mount -t tmpfs ${tmpfs_opts:+-o "$tmpfs_opts"} $verbose tmpfs "$tmpdir" || { cmd rmdir "$tmpdir"; die; }
+cmd mkfifo "$tmpdir/fifo" || { cmd umount "$tmpdir"; cmd rmdir "$tmpdir"; die; }
+# subshell allows cleanup after overmount /tmp without using realpath source
+# subshell also avoids trapping and re-raising signals which is annoying in shell
+(
+ cmd cd "$tmpdir" || die
+ trap '' INT || die
+ exec <fifo || die
+ # read returns non-zero when write end is closed
+ read _
+ logv 'unmounting tmpdir'
+ # TODO: this *almost* works except umount insists on canonicalizing .
+ # shellcheck disable=SC2086
+ cmd umount -cil $no_mtab . || die
+ logv 'deleting tmpdir'
+ cmd cd /
+ cmd rmdir "$tmpdir" || die
+) &
+# should be FD_CLOEXEC but can't do in shell
+exec 9>"$tmpdir/fifo"
+# starting from here, exiting will cause cleanup
+
+upperdir="$tmpdir/upper"
+workdir="$tmpdir/work"
+tmpmnt="$tmpdir/tmpmnt"
+cmd mkdir "$upperdir" "$workdir" "$tmpmnt" || die
+
+ovl_opts="lowerdir=$lowerdir,upperdir=$upperdir,workdir=$workdir"
+logv 'testing overlay options'
+cmd mount -n -t overlay -o "$ovl_opts" overlay "$tmpmnt" || die "overlayfs is not supported"
+cmd umount "$tmpmnt"
+if [ -n "$extra_ovl_opts" ]; then
+ ovl_opts="$ovl_opts,$extra_ovl_opts"
+ cmd mount -n -t overlay -o "$ovl_opts" overlay "$tmpmnt" || die "invalid extra overlayfs options"
+ cmd umount "$tmpmnt" || die
+fi
+chk_ovl_opt() {
+ if [ -n "$2" ]; then
+ case ",$ovl_opts," in
+ *",$1=$2,"*) return 0
+ esac
+ [ "$2" = on ] && val=Y || val=N
+ else
+ val=Y
+ fi
+ f=/sys/module/overlay/parameters/$1
+ [ -e "$f" ] && [ "$(cat "$f")" = "$val" ]
+}
+try_ovl_opt() {
+ # returns 0 iff option is/gets enabled
+ case ",$ovl_opts," in
+ *",$1=off,"*) return 1;;
+ *",$1[=,]"*) return 0
+ esac
+ if chk_ovl_opt "$1"; then
+ logv 'skipping %s' "$1${2+=$2}"
+ return
+ fi
+ logv 'trying %s' "$1${2+=$2}"
+ new_ovl_opts="$ovl_opts,$1${2+=$2}"
+ logv "mount -n -t overlay -o '$new_ovl_opts' overlay '$tmpmnt'"
+ if ! mount -n -t overlay -o "$new_ovl_opts" overlay "$tmpmnt" 2>/dev/null; then
+ [ -z "$verbose" ] || echo rejected >&2
+ return
+ fi
+ cmd umount "$tmpmnt" || die
+ # clear out workdir/work/incompat/volatile and upperdir index
+ cmd rm -r "$workdir" "$upperdir" || die
+ cmd mkdir "$upperdir" "$workdir" || die
+ ovl_opts="$new_ovl_opts"
+}
+try_ovl_opt index on
+# redirect_dir and metacopy are unsafe with untrusted non-bottom layers
+# nfs_export conflicts with metacopy
+[ "${lowerdir#*:}" = "$lowerdir" ] && \
+ ! chk_ovl_opt userxattr on && \
+ try_ovl_opt redirect_dir on && \
+ { chk_ovl_opt nfs_export on || try_ovl_opt metacopy on; }
+try_ovl_opt volatile
+
+logv 'copying lowerdir owner/perms to upperdir'
+lastlowerdir=${lowerdir##*:}
+# stat -c isn't posix, but ls is
+ls=$(ls -dn "$lastlowerdir/.") || die
+[ -n "$ls" ] || die 'empty ls output'
+owner=$(printf '%s\n' "$ls" | sed -e 's/^[^ ]* [^ ]* \([^ ]*\) \([^ ]*\).*$/\1:\2/;t;d')
+[ -n "$owner" ] || die 'bad ls owner output'
+mode=$(printf '%s\n' "$ls" | sed -e 's/^d\(...\)\(...\)\(...\).*/u=\1,g=\2,o=\3/;s/-//g;t;d')
+[ -n "$mode" ] || die 'bad ls mode output'
+if ! cmd chown "$owner" "$upperdir"; then
+ # int sysctl can't be read by read
+ [ "$owner" = "$(dd if=/proc/sys/fs/overflowuid bs=16 status=none):$(dd if=/proc/sys/fs/overflowgid bs=16 status=none)" ] || die
+ read uid_old uid_new uid_cnt < /proc/self/uid_map
+ [ "$uid_old $uid_new" != "0 0" ] || die
+ log 'detected user namespace, ignoring chown failure'
+fi
+cmd chmod "$mode" "$upperdir" || die
+# -m - covers ACLs (system.posix_acl_access) and file caps
+# (security.capability). theoretically someone might have get/setcap and/or
+# get/setfacl but not get/setxattr, but this is unlikely since libcap/acl
+# require attr.
+logv 'copying root xattrs'
+if attrs=$(cd "$lastlowerdir" && getfattr -d -m - . 2>/dev/null); then
+ if [ -n "$attrs" ]; then
+ printf '%s\n' "$attrs" | (cd "$upperdir"; setfattr --restore=-) || die
+ fi
+else
+ log 'getfattr failed, skipping xattrs/ACLs'
+fi
+
+logv 'mounting overlay'
+# shellcheck disable=SC2086
+cmd mount $no_canon $no_mtab $verbose -t overlay -o "$ovl_opts" "${mount_name-overlay}" "$dest" || die
+exec 9>&-
+wait || die
+logv 'done'