Skip to content

Commit abe88f3

Browse files
ezrizhuEric Zhugliargovasangelhofmgree
authored
Nested mount support (#67)
* Allow for nested mount by using mergerfs * Remove debug bash and echo * mount /run with merger aswell * refactor and use overlayfs via mergerfs if regular overlayfs fails * Only mount /dev/{tty null zero full random urandom} * improve docs, refactor from top_dir to mountpoint * Fix mergerfs failing not showing mount log path * Add support for unionfs, allow user to specify unionfs helper path * Write mountpoint on unionhelper not found message * exit if findmnt not installed * nested mount docs * add newlines to readme * grammar fix * Add -U option description to manpages * Add shell completion for -U option * Change -U flag autocompletion to only suggest executables * Install mergerfs in ci * Try reading from /run directory before testing * Refactor and unmount devices for tests to pass * Add a device test * Some comments and redirect a test to /dev/null Fixed #56 #45 #38 #20 #19 --------- Co-authored-by: Eric Zhu <eric@debian-BULLSEYE-live-builder-AMD64> Co-authored-by: gliargovas <gliargovas@aueb.gr> Co-authored-by: Konstantinos Kallas <konstantinos.kallas@hotmail.com> Co-authored-by: Michael Greenberg <michael@greenberg.science>
1 parent 0b4c678 commit abe88f3

File tree

6 files changed

+177
-39
lines changed

6 files changed

+177
-39
lines changed

.github/workflows/test.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
run: |
2020
uname -a
2121
sudo apt-get update
22-
sudo apt-get install strace expect
22+
sudo apt-get install expect mergerfs
2323
2424
- name: Checkout
2525
uses: actions/checkout@v2

README.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ disks.
2525

2626
### Dependencies
2727

28+
`try` relies on the following dependencies
29+
30+
* `util-linux`
31+
32+
In cases where overlayfs doesn't work on nested mounts, you will need either
33+
[mergerfs](https://github.com/trapexit/mergerfs) or [unionfs](https://github.com/rpodgorny/unionfs-fuse). `try` should be able to autodetect them, but you can specify the path to mergerfs or unionfs with -U (e.g. `try -U ~/.local/bin/unionfs`)
34+
2835
Has been tested on the following distributions:
2936
* `Ubuntu 20.04 LTS` or later
3037
* `Debian 12`
@@ -44,7 +51,7 @@ $ git clone https://github.com/binpash/try.git
4451

4552
#### Arch Linux
4653

47-
`Try` is present in [AUR](https://aur.archlinux.org/packages/try), you can install it with your preferred AUR helper:
54+
`try` is present in [AUR](https://aur.archlinux.org/packages/try), you can install it with your preferred AUR helper:
4855

4956
```shellsession
5057
yay -S try

completions/try.bash

+5-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ _try() {
2020

2121
case "${cmd}" in
2222
try)
23-
opts="-n -y -v -h -D summary commit explore"
23+
opts="-n -y -v -h -D -U summary commit explore"
2424
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
2525
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
2626
return 0
@@ -31,7 +31,10 @@ _try() {
3131
COMPREPLY=($(compgen -d "${cur}"))
3232
return 0
3333
;;
34-
34+
-U)
35+
COMPREPLY=($(compgen -c "${cur}"))
36+
return 0
37+
;;
3538
commit)
3639
COMPREPLY=($(compgen -d "${cur}"))
3740
return 0

docs/try.1.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,14 @@ While using *try* you can choose to commit the result to the filesystem or compl
4444

4545
: Specifies DIR as the overlay directory (implies -n). The use of -D also implies that *DIR* already exists.
4646

47+
-U *PATH*
48+
49+
: Use the unionfs helper implementation defined in the *PATH* (e.g., mergerfs, unionfs-fuse) instead of the default.
50+
This option is recommended in case OverlayFS fails.
51+
4752
## Subcommands
4853

49-
try summary *DIR*
54+
try summary *DIR*
5055

5156
: Show the summary for the overlay in DIR
5257

test/run_tests.sh

+25-2
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,27 @@ cleanup()
3434
mkdir "$try_workspace"
3535
}
3636

37+
test_read_from_run_dir()
38+
{
39+
ls /run/systemd > /dev/null
40+
if [ $? -ne 0 ]; then
41+
echo "Cannot read from /run/systemd."
42+
return 1
43+
fi
44+
}
45+
3746
run_test()
3847
{
3948
cleanup
4049
local test=$1
41-
50+
4251
if [ "$(type -t $test)" != "function" ]; then
4352
echo "$test is not a function! FAIL"
4453
return 1
4554
fi
55+
56+
# Check if we can read from /run dir
57+
test_read_from_run_dir
4658

4759
echo -n "Running $test..."
4860

@@ -165,7 +177,7 @@ test_reuse_problematic_sandbox()
165177
## but it doesn't seem to both overlayfs at all.
166178
## TODO: Extend this with more problematic overlayfs modifications.
167179
touch "$try_example_dir/temproot/bin/foo"
168-
! "$try" -D $try_example_dir "rm file_1.txt; echo test2 >> file_2.txt; touch file.txt.gz"
180+
! "$try" -D $try_example_dir "rm file_1.txt; echo test2 >> file_2.txt; touch file.txt.gz" 2> /dev/null
169181
}
170182

171183
test_non_existent_sandbox()
@@ -305,6 +317,16 @@ test_mkdir_on_file()
305317
diff -qr expected target
306318
}
307319

320+
test_dev()
321+
{
322+
local try_workspace=$1
323+
cp $RESOURCE_DIR/file.txt.gz "$try_workspace/"
324+
cd "$try_workspace/"
325+
326+
"$try" -y "head -c 5 /dev/urandom > target"
327+
[ -s target ]
328+
}
329+
308330
# a test that deliberately fails (for testing CI changes)
309331
test_fail()
310332
{
@@ -331,6 +353,7 @@ if [ "$#" -eq 0 ]; then
331353
run_test test_explore
332354
run_test test_empty_summary
333355
run_test test_mkdir_on_file
356+
run_test test_dev
334357

335358
# uncomment this to force a failure
336359
# run_test test_fail

try

+132-32
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ TRY_VERSION="0.1.0"
2222
try() {
2323
START_DIR="$PWD"
2424

25+
if ! command -v findmnt > /dev/null
26+
then
27+
printf "%s: findmnt not found, please install util-linux\n" "$(basename $0)" >&2
28+
exit 1
29+
fi
30+
2531
if [ "$SANDBOX_DIR" ]
2632
then
2733
## If the name of a sandbox is given then we need to exit prematurely if its directory doesn't exist
@@ -45,11 +51,21 @@ try() {
4551

4652
# we will overlay-mount each root directory separately (instead of all at once) because some directories cannot be overlayed
4753
# so we set up the mount points now
48-
for top_dir in /*
54+
#
55+
# KK 2023-06-29 This approach (of mounting each root directory separately) was necessary because we could not mount `/` in an overlay.
56+
# However, this might be solveable using mergerfs/unionfs, allowing us to mount an overlay on a unionfs of the `/` once.
57+
#
58+
# findmnt
59+
# --real: only list real filesystems
60+
# -n: no header
61+
# -r: raw output
62+
# -o target: only print the mount target
63+
# then we want to exclude the root partition "/"
64+
for mountpoint in /* $(findmnt --real -r -o target -n | grep -v "^/$")
4965
do
5066
## Only make the directory if the original is a directory too
51-
if [ -d "$top_dir" ]; then
52-
mkdir -p "$SANDBOX_DIR"/upperdir/"$top_dir" "$SANDBOX_DIR"/workdir"/$top_dir" "$SANDBOX_DIR"/temproot/"$top_dir"
67+
if [ -d "$mountpoint" ]; then
68+
mkdir -p "$SANDBOX_DIR"/upperdir/"$mountpoint" "$SANDBOX_DIR"/workdir"/$mountpoint" "$SANDBOX_DIR"/temproot/"$mountpoint"
5369
fi
5470
done
5571

@@ -60,48 +76,123 @@ try() {
6076
cat >"$mount_and_execute" <<"EOF"
6177
#!/bin/sh
6278
79+
## A wrapper of `mount -t overlay` to have cleaner looking code
80+
make_overlay() {
81+
sandbox_dir="$1"
82+
lowerdir="$2"
83+
mountpoint="$3"
84+
mount -t overlay overlay -o "lowerdir=$lowerdir,upperdir=$sandbox_dir/upperdir/$mountpoint,workdir=$sandbox_dir/workdir/$mountpoint" "$sandbox_dir/temproot/$mountpoint"
85+
}
86+
87+
devices_to_mount="tty null zero full random urandom"
88+
89+
## Mounts and unmounts a few select devices instead of the whole `/dev`
90+
mount_devices() {
91+
sandbox_dir="$1"
92+
for dev in $devices_to_mount
93+
do
94+
touch "$sandbox_dir/temproot/dev/$dev"
95+
mount -o bind /dev/$dev "$sandbox_dir/temproot/dev/$dev"
96+
done
97+
}
98+
99+
unmount_devices() {
100+
sandbox_dir="$1"
101+
for dev in $devices_to_mount
102+
do
103+
umount "$sandbox_dir/temproot/dev/$dev" 2>>"$try_mount_log"
104+
rm -f "$sandbox_dir/temproot/dev/$dev"
105+
done
106+
}
107+
108+
## Try to autodetect union helper: {mergerfs | unionfs}
109+
## Returns an empty string if no union helper is found
110+
autodetect_union_helper() {
111+
if command -v mergerfs > /dev/null; then
112+
echo mergerfs
113+
elif command -v unionfs > /dev/null; then
114+
echo unionfs
115+
fi
116+
}
117+
63118
# actually mount the overlays
64-
for top_dir in /*
119+
for mountpoint in /* $(findmnt --real -r -o target -n)
65120
do
66-
## If the directory is not a mountpoint
67-
if [ -d "$top_dir" ] && ! mountpoint -q "$top_dir"; then
68-
## TODO: The
69-
mount -t overlay overlay -o lowerdir=/"$top_dir",upperdir="$SANDBOX_DIR"/upperdir/"$top_dir",workdir="$SANDBOX_DIR"/workdir/"$top_dir" "$SANDBOX_DIR"/temproot/"$top_dir" 2>> "$try_mount_log" || echo "Warning: Failed mounting $top_dir as an overlay, see "$try_mount_log"" 1>&2
121+
## We are not interested in mounts that are not directories
122+
if [ ! -d "$mountpoint" ]
123+
then
124+
continue
70125
fi
71-
done
72126
73-
# Now we will handle custom mounts, e.g., mounts on /home
74-
# findmnt
75-
# --real: only list real filesystems
76-
# -n: no header
77-
# -r: raw output
78-
# -o target: only print the mount target
79-
# then we want to exclude the root partition "/"
80-
for mount_dir in $(findmnt --real -r -o target -n | grep -v "^/$")
81-
do
82-
mount -t overlay overlay -o lowerdir="$mount_dir",upperdir="$SANDBOX_DIR"/upperdir"$mount_dir",workdir="$SANDBOX_DIR"/workdir"$mount_dir" "$SANDBOX_DIR"/temproot"$mount_dir" 2>> "$try_mount_log" || echo "Warning: Failed mounting $mount_dir as an overlay, see "$try_mount_log"" 1>&2
127+
## Don't do anything for the root
128+
## and skip if it is /dev or /proc, we will mount it later
129+
if [ "$mountpoint" = "/" ] ||
130+
[ "$mountpoint" = "/dev" ] || [ "$mountpoint" = "/proc" ]
131+
then
132+
continue
133+
fi
134+
135+
# Try mounting everything normally
136+
make_overlay "$SANDBOX_DIR" "/$mountpoint" "$mountpoint" 2>> "$try_mount_log"
137+
# If mounting everything normally fails, we try using either using mergerfs or unionfs to mount them.
138+
if [ "$?" -ne 0 ]
139+
then
140+
# Detect if union_helper is set, if not, we try to autodetect them
141+
if [ -z ${union_helper+x} ]
142+
then
143+
union_helper="$(autodetect_union_helper)"
144+
if [ -z "$union_helper" ]
145+
then
146+
printf "%s: Failed to mount overlayfs normally, mergerfs or unionfs not found for $mountpoint, see $try_mount_log\n" "$(basename $0)" >&2
147+
exit 1
148+
fi
149+
fi
150+
151+
152+
## If the overlay failed, it means that there is a nested mount inside the target mount, e.g., both `/home` and `/home/user/mnt` are mounts.
153+
## To address this, we use unionfs/mergerfs (they support the same functionality) to show all mounts under the target mount as normal directories.
154+
## Then we can normally make the overlay on the new union directory.
155+
##
156+
## KK 2023-06-29 Since this uses findmnt, it performs the union+overlay for both the outside and the inside mount.
157+
## In the best case scenario this is only causing extra work (the internal mount is already shown through the unionfs),
158+
## but in the worst case this could lead to bugs due to the extra complexity (e.g., because we are doing mounts on top of each other).
159+
## We should try to investigate either:
160+
## 1. Not doing another overlay if we have done it for a parent directory (we can keep around a list of overlays and skip if we are in a child)
161+
## 2. Do one unionfs+overlay at the root `/` once and be done with it!
162+
merger_dir=$(mktemp -d)
163+
164+
## Create a union directory
165+
"$union_helper" $mountpoint $merger_dir 2>> "$try_mount_log" ||
166+
printf "%s: Warning: Failed to mount $mountpoint via $union_helper, see \"$try_mount_log\"\n" "$(basename $0)" >&2
167+
168+
## Make the overlay on the union directory which works as a lowerdir for overlay
169+
make_overlay "$SANDBOX_DIR" "$merger_dir" "$mountpoint" 2>> "$try_mount_log" ||
170+
printf "%s: Warning: Failed mounting $mountpoint as an overlay via $union_helper, see \"$try_mount_log\"\n" "$(basename $0)" >&2
171+
fi
83172
done
84173
85-
## Bind the udev mount so that the containerized process has access to /dev
86-
## KK 2023-05-06 Are there any security/safety implications by binding the whole /dev?
87-
## Maybe we just want to bind a few files in it like /dev/null, /dev/zero?
88-
mount --rbind /dev "$SANDBOX_DIR/temproot/dev"
89-
## KK 2023-06-20 Redirecting to /dev/null to suppress a yet uninvestigated but
90-
## seemingly not impactful warning.
91-
mount --rbind --read-only /run "$SANDBOX_DIR/temproot/run" 2> /dev/null
174+
## Mount a few select devices in /dev
175+
mount_devices "$SANDBOX_DIR"
92176
93177
## Check if chroot_executable exists, #29
94-
if ! [ -f "$SANDBOX_DIR/temproot/$chroot_executable" ]; then
178+
if ! [ -f "$SANDBOX_DIR/temproot/$chroot_executable" ]
179+
then
95180
cp $chroot_executable "$SANDBOX_DIR/temproot/$chroot_executable"
96181
fi
97182
98-
99183
unshare --root="$SANDBOX_DIR/temproot" /bin/bash "$chroot_executable"
184+
exitcode="$?"
185+
186+
# unmount the devices
187+
sync
188+
unmount_devices "$SANDBOX_DIR"
189+
190+
exit $exitcode
100191
EOF
101192

102193
cat >"$chroot_executable" <<EOF
103194
#!/bin/sh
104-
195+
105196
mount -t proc proc /proc &&
106197
cd $START_DIR &&
107198
source "$script_to_execute"
@@ -165,7 +256,7 @@ summary() {
165256
changed_files=$(find_upperdir_changes "$SANDBOX_DIR")
166257
summary_output=$(process_changes "$SANDBOX_DIR" "$changed_files")
167258

168-
if [ -z "$summary_output" ];
259+
if [ -z "$summary_output" ]
169260
then
170261
return 1
171262
fi
@@ -248,6 +339,7 @@ find_upperdir_changes() {
248339
find "$sandbox_dir/upperdir/" -type f -o \( -type c -size 0 \) -o -type d | ignore_changes
249340
}
250341

342+
251343
## Processes upperdir changes to an internal format that can be processed by summary and commit
252344
## Format:
253345
## XX PATH
@@ -323,11 +415,12 @@ sandbox_valid_or_empty() {
323415
usage() {
324416
cmd="$(basename $0)"
325417
cat >&2 <<EOF
326-
Usage: $cmd [-nvhy] [-D DIR] CMD [ARG ...]
418+
Usage: $cmd [-nvhy] [-D DIR] [-U PATH] CMD [ARG ...]
327419
328420
-n don't prompt for commit
329421
-y assume yes to all prompts (implies -n is not used)
330422
-D DIR work in DIR (implies -n)
423+
-U PATH path to unionfs helper (e.g., mergerfs, unionfs-fuse)
331424
332425
-v show version information (and exit)
333426
-h show this usage message (and exit)
@@ -346,7 +439,7 @@ EOF
346439
# "commit" - commit the result directory automatically when we're done
347440
NO_COMMIT="interactive"
348441

349-
while getopts ":yvnD:" opt
442+
while getopts ":yvnD:U:" opt
350443
do
351444
case "$opt" in
352445
(y) NO_COMMIT="commit";;
@@ -360,6 +453,13 @@ do
360453
NO_COMMIT="quiet"
361454
;;
362455
(v) printf "%s version $TRY_VERSION\n" "$(basename $0)" >&2; exit 0;;
456+
(U) if ! [ -x "$OPTARG" ]
457+
then
458+
printf "%s: no such executable $OPTARG\n" "$(basename $0)" >&2
459+
exit 2
460+
fi
461+
union_helper="$OPTARG"
462+
;;
363463
(h|*) usage; exit 0;;
364464
esac
365465
done

0 commit comments

Comments
 (0)