On many linux distro's you can already do this with user namespaces:
$ mkdir rootfs
$ docker export $(docker create ubuntu:20.04) | tar -C rootfs -xf -
$ unshare -r chroot rootfs bash
# ls
bin dev home ...
Very often when you use chroot you also want unprivileged mounts, in particular overlay mounts if you don't want to mutate the underlying rootfs. You can do that with mount namespaces: `unshare -rm`, but you need Linux kernel 5.13 (or a distro with a patched kernel like Ubuntu) to allow unpriviliged overlayfs.
An alternative to unshare is also bubblewrap (https://github.com/containers/bubblewrap) which also sets up a new namespace. You can build up your own new filesystem by binding existing paths into the new root and then run a process within it:
$ mkdir -p root/bin
$ cp /bin/busybox root/bin/
$ bwrap --bind root / /bin/busybox sh
BusyBox v1.27.2 (Ubuntu 1:1.27.2-2ubuntu3.3) built-in shell (ash)
Enter 'help' for a list of built-in commands.
/ $ ls -l /
total 0
drwxrwxr-x 2 1000 1000 60 Jul 22 11:07 bin
I used bubblewrap to do a lightweight containers on top of arch + pacman. Basically you could install packages on overlays of the host and do whatever there without affecting the host fs. It was pretty nice.
So how does this work? Can you mount / as the lower layer of the overlayfs? Doesn't that create a weird recursion because the mountpoint is a path inside /?
I used unionfs first to combine the sandbox and / where host / is read-only. Then simply bubblewrap into it. I also mounted / to /host if for some reason you wanted to access host fs from inside the sandbox.