Skip to content

Instantly share code, notes, and snippets.

@mid-kid
Last active May 18, 2026 19:18
Show Gist options
  • Select an option

  • Save mid-kid/9293f4f0617052b9c3aa45422fb89f90 to your computer and use it in GitHub Desktop.

Select an option

Save mid-kid/9293f4f0617052b9c3aa45422fb89f90 to your computer and use it in GitHub Desktop.
Create and enter a chroot without root permissions, using unshare + pivot_root
#!/usr/bin/env -S unshare -fpmr --map-auto sh
# vim:ft=sh
set -eu
# Create and enter a chroot, without root permissions, using only programs
# available in coreutils and util-linux.
# In order to make sure UIDs and GIDs are mapped correctly, the rootfs has to
# be extracted from within an unshare environment. Executing this script
# without parameters will aid with this.
#
# Setting up a gentoo chroot:
# $ wget https://distfiles.gentoo.org/releases/amd64/autobuilds/XXXXXXXXXXXXXXXZ/stage3-amd64-openrc-XXXXXXXXXXXXXXXZ.tar.xz
# $ SHELL=bash chrootless
# $ mkdir -p rootfs
# $ tar xpvf stage3-*.tar.xz --xattrs-include='*.*' --numeric-owner -C rootfs
# $ exit
# $ chrootless rootfs
# $ emerge-webrsync
# $ emerge -auDU @world
# Unshare without chroot
test -n "${1:-}" || exec "${SHELL:-bash}"
umask 022
cd "$1"; shift
# Common mounts as suggested by the gentoo handbook
mount --rbind /dev dev
mount --make-rslave dev
mount --rbind /sys sys
mount --make-rslave sys
mount -t proc proc proc
mount --make-slave proc
# Not bind-mounting /run, we cannot control the host's init
# Allow the processes inside the chroot to create new PTYs
# required for emerge to work
mount -t devpts devpts dev/pts
# Many container technologies will create a tmpfs on /dev, and only bind-mount
# some of the nodes. This has the side-effect of solving some permission
# issues. In the interest of keeping the script simple, we --rbind it, but
# still override the dev/pts mount.
# Make sure /etc/resolv.conf is updated alongside the host's
mkdir -p etc
test -f etc/resolv.conf || touch etc/resolv.conf
mount --bind /etc/resolv.conf etc/resolv.conf
# Roughly replicate what bubblewrap, systemd-nspawn, etc do
# This allows for recursive "unshare" inside the chroot to work correctly
# https://github.com/containers/bubblewrap/blob/main/bubblewrap.c
# https://bugs.gentoo.org/922960
# Assuming /mnt exists inside the chroot, and the umount command is available
# in the chroot's $PATH
mount --rbind . .
cd .
exec env -i TERM="$TERM" HOME=/root sh -ec 'pivot_root="$1"; shift
"$pivot_root" . mnt
exec /bin/sh -l -ec "umount -l /mnt
exec \"\$@\"" -- "$@"' -- "$(which pivot_root)" "${@:-bash}"
# Alternative, simpler chroot invocation
# "env -i" allows us to clear the environment, so we don't pollute it with host
# variables. Combining this with "/bin/sh -l", we read /etc/profile and
# populate the chroot's $PATH and other variables, before executing an
# arbitrary command.
exec env -i TERM="$TERM" HOME=/root "$(which chroot)" . /bin/sh -l -c 'exec "$@"' -- "${@:-bash}"
# Alternative, direct-syscall implementation of the pivot_root method above
# Doesn't rely on /mnt or "umount" inside the chroot, and doesn't leave the
# original rootfs mounted while /etc/profile is being evaluated. Instead relies
# on the host's C compiler and /tmp.
mount -t tmpfs tmpfs /tmp
printf '_(%s,(%s))' \
mount '".",".",0,MS_BIND|MS_REC,0' \
chdir 'getcwd((char[PATH_MAX]){0},PATH_MAX)' \
pivot_root '".","."' \
umount2 '"/",MNT_DETACH' \
execv '*++v,v' | \
xargs -0 printf 'int main(int _,char**v){%s}' | cc -xc - -o /tmp/pivot_root \
-D'_(a,b)=if(a b<0)write(2,#a" failed\n",sizeof(#a)+8),_exit(1);' \
-D'pivot_root(a,b)=syscall(SYS_pivot_root,a,b)' \
$(printf ' -include %s.h' unistd sys/mount sys/syscall linux/limits)
exec env -i TERM="$TERM" HOME=/root /tmp/pivot_root /bin/sh -l -c 'exec "$@"' -- "${@:-bash}"
#!/usr/bin/env -S unshare -fpmr --map-auto sh
# vim:ft=sh
set -eu
test -n "${1:-}" || exec "${SHELL:-bash}"
umask 022
cd "$1"; shift
mount --rbind /dev dev
mount --rbind /sys sys
mount -t proc proc proc
mount -t devpts devpts dev/pts
mkdir -p etc
test -f etc/resolv.conf || touch etc/resolv.conf
mount --bind /etc/resolv.conf etc/resolv.conf
mount --rbind . .
cd .
exec env -i TERM="$TERM" HOME=/root sh -ec 'pivot_root="$1"; shift
"$pivot_root" . mnt
exec /bin/sh -l -ec "umount -l /mnt
exec \"\$@\"" -- "$@"' -- "$(which pivot_root)" "${@:-bash}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment