From d571da78eaaa8408b4a6be8d8945df93624f41a9 Mon Sep 17 00:00:00 2001 From: "Alex Xu (Hello71)" Date: Tue, 29 Jun 2021 16:59:52 -0400 Subject: improve userns support, logging, add minification --- .gitignore | 1 + Makefile | 14 ++++ README.rst | 23 +++++- tmpoverlay | 244 ---------------------------------------------------------- tmpoverlay.sh | 243 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 280 insertions(+), 245 deletions(-) create mode 100644 .gitignore create mode 100644 Makefile delete mode 100755 tmpoverlay create mode 100755 tmpoverlay.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..59b3be3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/tmpoverlay diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..aacc7e9 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +prefix ?= /usr/local +bindir ?= $(prefix)/bin + +tmpoverlay: tmpoverlay.sh + sed -e '1p;1t;/^ *#.*/d' $< > $@ + chmod +x $@ + +clean: + rm -f tmpoverlay + +install: + install -Dm755 tmpoverlay $(DESTDIR)$(bindir)/tmpoverlay + +.PHONY: clean install diff --git a/README.rst b/README.rst index ceeaf84..3e8e4c5 100644 --- a/README.rst +++ b/README.rst @@ -8,6 +8,8 @@ Features -------- - minimal requirements (sh, mount, getopt) +- single shell script, no compilation required +- small (7 KB, 2 KB after gzip -9) Benefits over manually calling ``mkdir /tmp/x; mount ...`` @@ -26,10 +28,12 @@ mount point, it continues to access the original directory, not the overlaid one. Each process also has a cached root directory pointer, which can only be modified by chroot (internally) or pivot_root (globally). The pivot_root(2)_ and pivot_root(8)_ man pages should be fully read and understood before using -tmpoverlay to overmount ``/``. +tmpoverlay to overmount ``/``. It is also highly recommended to read `busybox +switch_root comment`_. .. _pivot_root(2): https://man7.org/linux/man-pages/man2/pivot_root.2.html .. _pivot_root(8): https://man7.org/linux/man-pages/man8/pivot_root.8.html +.. _busybox switch_root comment: https://git.busybox.net/busybox/tree/util-linux/switch_root.c?id=3b267e99259191eca0865179a56429c4c441e2b2#n289 Changes to underlying filesystems --------------------------------- @@ -44,6 +48,23 @@ while the overlay is mounted is not supported: .. _the kernel overlayfs documentation: https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html#changes-to-underlying-filesystems +Unprivileged operation using user namespaces +-------------------------------------------- + +Since Linux 5.11, overlayfs can be mounted in unprivileged user namespaces. +This means that it is possible to temporarily place an overlay in a local +context. For example, ``unshare -Umc --keep-caps sh -c 'tmpoverlay . && exec +setpriv --inh-caps=-all $SHELL'`` has a similar effect to ``tmpoverlay .``, but +does not require privileges. In exchange, it only takes effect within the newly +started shell, similar to environment variables. + +Note that tmpfs overlay doesn't work properly with unmapped UIDs. In other +words, after running tmpoverlay, only files owned by the current user can be +modified; modifying other files will have unpredictable results. + +This issue can be mitigated starting with Linux 5.12 using idmap, but this is +not integrated in tmpoverlay due to a lack of standard utilities. + Pseudo-filesystems ------------------ diff --git a/tmpoverlay b/tmpoverlay deleted file mode 100755 index e817e87..0000000 --- a/tmpoverlay +++ /dev/null @@ -1,244 +0,0 @@ -#!/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 -EOF - exit "$1" -} - -log() { - logn "$@" - printf '\n' >&2 -} - -logn() { - # not equivalent to printf "tmpoverlay: $@" - printf 'tmpoverlay: ' >&2 - printf "$@" >&2 -} - -logv() { - [ -z "$verbose" ] || log "$@" -} - -logvn() { - [ -z "$verbose" ] || logn "$@" -} - -cmdv() { - 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=$(umask 077; mktemp -dt tmpoverlay.XXXXXXXXXX) || die -logv 'created tmpdir: %s' "$tmpdir" -case "$tmpdir" in - "$dest"/*) log "warning: tmpdir cannot be cleaned up after overmounting $dest" -esac -mount -t tmpfs ${tmpfs_opts:+-o "$tmpfs_opts"} $verbose tmpfs "$tmpdir" || { rmdir "$tmpdir"; die; } -mkfifo "$tmpdir/fifo" || { umount "$tmpdir"; 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 -( - cd "$tmpdir" || die - trap '' INT || die - exec "$tmpdir/fifo" -# starting from here, exiting will cause cleanup - -upperdir="$tmpdir/upper" -workdir="$tmpdir/work" -tmpmnt="$tmpdir/tmpmnt" -logv 'creating dirs' -mkdir "$upperdir" "$workdir" "$tmpmnt" || die - -ovl_opts="lowerdir=$lowerdir,upperdir=$upperdir,workdir=$workdir" -logv 'testing overlay options' -mount -n -t overlay -o "$ovl_opts" overlay "$tmpmnt" || die "overlayfs is not supported" -umount "$tmpmnt" -if [ -n "$extra_ovl_opts" ]; then - ovl_opts="$ovl_opts,$extra_ovl_opts" - mount -n -t overlay -o "$ovl_opts" overlay "$tmpmnt" || die "invalid extra overlayfs options" - 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 'not checking %s' - return - fi - logvn 'trying %s... ' "$1${2+=$2}" - new_ovl_opts="$ovl_opts,$1${2+=$2}" - if ! cmdv mount -n -t overlay -o "$new_ovl_opts" "overlay" "$tmpmnt" 2>/dev/null; then - [ -z "$verbose" ] || echo rejected >&2 - return - fi - [ -z "$verbose" ] || echo ok >&2 - umount "$tmpmnt" || die - # clear out workdir/work/incompat/volatile and upperdir index - rm -r "$workdir" "$upperdir" || die - 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' -cmdv chown "$owner" "$upperdir" || die -cmdv 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 -cmdv mount $no_canon $no_mtab $verbose -t overlay -o "$ovl_opts" "${mount_name-overlay}" "$dest" || die -exec 9>&- -wait || die -logv 'done' 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 "$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' -- cgit v1.2.3-54-g00ecf