rpodgorny / unionfs-fuse

union filesystem using fuse
Other
300 stars 76 forks source link

UnionFS CoW permissions issue, doesn't create path predicate before target file if lower path permissions are to be honored. #102

Closed M3TIOR closed 2 years ago

M3TIOR commented 2 years ago

Say I have an empty root CoW target branch, / and a populated lower branch full of directories, /etc/ /dev/ /bin/ /proc/

Creating the file, /test.txt works, /etc/test.txt doesn't, But if I add the directory manually, mkdir /etc/ /etc/test.txt now it does.

rpodgorny commented 2 years ago

thanks for the report! ...just to be sure - what should be the expected behaviour for you?

M3TIOR commented 2 years ago

When writing to file in the top most CoW target, if the path of the file already exists in a lower branch, the top branch should create directories to match the lower path so the file can be created without fail.

Because otherwise it looks like there's a path that I could write to in the mounted folder, but the unionfs won't allow me to do so. It's very intuitive. I thought it was a bug and not an actual feature. Is this supposed to happen by default? If this is a breaking change, could it instead be implemented as a mount option?

# Just as an example if this script were to take the CoW target as input
mkdir -p "$(dirname "$1")"; #... after script exits, finish opening file and writing to it.
bsbernd commented 2 years ago

This is how it is supposed to work.

Shell 1

bernd@t420-work-1 ~>mkdir -p /tmp/unionfs/lower/etc
bernd@t420-work-1 ~>mkdir -p /tmp/unionfs/upper
bernd@t420-work-1 ~>mkdir /tmp/unionfs/union
bernd@t420-work-1 ~>unionfs -d -ocow /tmp/unionfs/lower=RO:/tmp/unionfs/upper=RW /tmp/unionfs/union
FUSE library version: 2.9.9
nullpath_ok: 0
....

Shell 2

bernd@t420-work-1 ~>echo abc >/tmp/unionfs/union/etc/testfile

bernd@t420-work-1 ~>find /tmp/unionfs/
/tmp/unionfs/
/tmp/unionfs/upper
/tmp/unionfs/upper/etc
/tmp/unionfs/upper/etc/testfile
/tmp/unionfs/union
/tmp/unionfs/lower
/tmp/unionfs/lower/etc

This does not work for you?

bsbernd commented 2 years ago

Hmm, as side mark, correct syntax is /tmp/unionfs/upper=RW:/tmp/unionfs/lower=RO(upper is on top of lower). The current code also does a path copy from upper to lower - debatable if this is right. And totally the opposite of your report.

M3TIOR commented 2 years ago

I'm double checking everything one more time. I think I misread the version last time I read unionfs-fuse --version I'm apparently on 1.0 not 2.0 like I thought. It may just be a problem with this version, I'll replicate the test you did...

M3TIOR commented 2 years ago

Okay, so it seems to work either way if you only have two branches, one RW and the other one RW. The problem I seem to be having may be a permissions problem, as the issue rears it's head again when the RW directory is user owned, and the RO directory is /

unionfs -d -ocow /=RO:/tmp/unionfs/upper=RW /tmp/unionfs/union;
M3TIOR commented 2 years ago

I'm gonna try and replicate this on the latest version

M3TIOR commented 2 years ago

Shell 1:

(base) m3tior@chromebook_flip_CP311-HN-C2DV:/tmp/unionfstest$ ~/.local/bin/unionfs --version
unionfs-fuse version: 2.3
(compiled with xattr support)
FUSE library version: 2.9.7
fusermount version: 2.9.7
using FUSE kernel interface version 7.19
(base) m3tior@chromebook_flip_CP311-HN-C2DV:/tmp/unionfstest$ ~/.local/bin/unionfs -d -ocow ./upper=RW:/=RO ./union
FUSE library version: 2.9.7
nullpath_ok: 0
nopath: 0
utime_omit_ok: 0
unique: 1, opcode: INIT (26), nodeid: 0, insize: 56, pid: 0
INIT: 7.26
flags=0x001ffffb
max_readahead=0x00020000
   INIT: 7.19
   flags=0x00000031
   max_readahead=0x00020000
   max_write=0x00020000
   max_background=0
   congestion_threshold=0
   unique: 1, success, outsize: 40
unique: 2, opcode: GETATTR (3), nodeid: 1, insize: 56, pid: 17633
getattr /
   unique: 2, success, outsize: 120
unique: 3, opcode: LOOKUP (1), nodeid: 1, insize: 44, pid: 17633
LOOKUP /etc
getattr /etc
   NODEID: 2
   unique: 3, success, outsize: 144
unique: 4, opcode: GETATTR (3), nodeid: 2, insize: 56, pid: 17633
getattr /etc
   unique: 4, success, outsize: 120
unique: 5, opcode: LOOKUP (1), nodeid: 2, insize: 49, pid: 17633
LOOKUP /etc/testfile
getattr /etc/testfile
   unique: 5, error: -2 (No such file or directory), outsize: 16
unique: 6, opcode: LOOKUP (1), nodeid: 2, insize: 49, pid: 17633
LOOKUP /etc/testfile
getattr /etc/testfile
   unique: 6, error: -2 (No such file or directory), outsize: 16

(base) m3tior@chromebook_flip_CP311-HN-C2DV:/tmp/unionfstest$ ~/.local/bin/unionfs -d -ocow /=RO:./upper=RW ./union
FUSE library version: 2.9.7
nullpath_ok: 0
nopath: 0
utime_omit_ok: 0
unique: 1, opcode: INIT (26), nodeid: 0, insize: 56, pid: 0
INIT: 7.26
flags=0x001ffffb
max_readahead=0x00020000
   INIT: 7.19
   flags=0x00000031
   max_readahead=0x00020000
   max_write=0x00020000
   max_background=0
   congestion_threshold=0
   unique: 1, success, outsize: 40
unique: 2, opcode: GETATTR (3), nodeid: 1, insize: 56, pid: 17807
getattr /
   unique: 2, success, outsize: 120
unique: 3, opcode: LOOKUP (1), nodeid: 1, insize: 44, pid: 17807
LOOKUP /etc
getattr /etc
   NODEID: 2
   unique: 3, success, outsize: 144
unique: 4, opcode: GETATTR (3), nodeid: 2, insize: 56, pid: 17807
getattr /etc
   unique: 4, success, outsize: 120
unique: 5, opcode: LOOKUP (1), nodeid: 2, insize: 49, pid: 17807
LOOKUP /etc/testfile
getattr /etc/testfile
   unique: 5, error: -2 (No such file or directory), outsize: 16
unique: 6, opcode: LOOKUP (1), nodeid: 2, insize: 49, pid: 17807
LOOKUP /etc/testfile
getattr /etc/testfile
   unique: 6, error: -2 (No such file or directory), outsize: 16

Shell 2:

(base) m3tior@chromebook_flip_CP311-HN-C2DV:/tmp/unionfstest$ touch ./union/etc/testfile
touch: cannot touch './union/etc/testfile': Permission denied
(base) m3tior@chromebook_flip_CP311-HN-C2DV:/tmp/unionfstest$ tree
.
├── lower
│   └── etc
├── union
└── upper

4 directories, 0 files
(base) m3tior@chromebook_flip_CP311-HN-C2DV:/tmp/unionfstest$ touch ./union/etc/testfile
touch: cannot touch './union/etc/testfile': Permission denied
M3TIOR commented 2 years ago

@rpodgorny @aakefbs I've narrowed it down to a Linux Native Filesystem permissions issue. It seems like UnionFS-fuse is trying to honor the lowest branch's folder permissions when a directory doesn't exist in the highest branch, but the expected behavior is that it disregard the lower branch's folder permissions because unionfs should be generating paths with new permissions to predicate the target file. This script should reproduce the issue on the latest version.

TMP="${XDG_RUNTIME_DIR:-/tmp}";
TEMPDIR="$(mktemp -p "$TMP" -d unionfstest.XXX)";

trap 0 "rm -rf $TEMPDIR; test -n \"\$UNIONFS\" && kill -9 \$UNIONFS";

# Root fetching command (doas is more secure than sudo)
# The reason I'm using root is because I don't know if there's a second login user on your machine handy to test this.
rooter="$(command -v doas || command -v sudo)";

cd "$TEMPDIR"; mkdir -p union upper lower/good lower/bad;
chmod 775 lower/good;
chmod 755 lower/bad;
$rooter chown "0:$(id -u 2>/dev/null || echo "$UID")" lower/*;

unionfs -d -ocow ./upper=RW:./lower=RO ./union > "$TEMPDIR/unionfs.log" & UNIONFS="$!";
tree;
touch union/good/testfile;
touch union/bad/testfile;
tree;

# To fix the problem, you have to introduce a file with good permissions on the upper branch
mkdir upper/bad;
touch union/bad/testfile;
tree;

echo; # space out the logging;
cat "$TEMPDIR/unionfs.log";
exit;
bsbernd commented 2 years ago

Can you add -o relaxed_permissions please?

M3TIOR commented 2 years ago

Thanks @aakefbs I don't know how I missed that option before. This does fix the problem.

It doesn't help my use-case however because I'm using unionfs-fuse to implement a chroot for hosting the APT suite, which needs to fake UID 0 in order to execute effectively. I guess the workaround I have in place for now will have to do. Why does relaxed permissions only apply outside of UID and GID 0? If the upper COW directory is user owned anyway and that's where the changes will be written to with the initial GID and UID changed back to the correct UID, does that produce a security breach or something I'm not seeing? I feel like there's a good reason what I'm trying to do is unallowed.