#! /bin/bash # SPDX-License-Identifier: GPL-2.0 # Copyright 2020 Google LLC # # FS QA Test No. 621 # # Test for a race condition where a duplicate filename could be created in an # encrypted directory while the directory's encryption key was being added # concurrently. This is a regression test for the following kernel commits: # # 968dd6d0c6d6 ("fscrypt: fix race allowing rename() and link() of ciphertext dentries") # 75d18cd1868c ("ext4: prevent creating duplicate encrypted filenames") # bfc2b7e85189 ("f2fs: prevent creating duplicate encrypted filenames") # 76786a0f0834 ("ubifs: prevent creating duplicate encrypted filenames") # # The first commit fixed the bug for the rename() and link() syscalls. # The others fixed the bug for the other syscalls that create new filenames. # # Note, the bug wasn't actually reproducible on f2fs. # # The race condition worked as follows: # 1. Initial state: an encrypted directory "dir" contains a file "foo", # but the directory's key hasn't been added yet so 'ls dir' shows an # encoded no-key name rather than "foo". # 2. The key is added concurrently with mkdir("dir/foo") or another syscall # that creates a new filename and should fail if it already exists. # a. The syscall looks up "dir/foo", creating a negative no-key dentry # for "foo" since the directory's key hasn't been added yet. # b. The directory's key is added. # c. The syscall does the actual fs-level operation to create the # filename. With the bug, the filesystem failed to detect that the # dentry was created without the key, potentially causing the # operation to unexpectedly succeed and add a duplicate filename. # # To test this, we try to reproduce the above race. Afterwards we check for # duplicate filenames, plus a few other things. # seq=`basename $0` seqres=$RESULT_DIR/$seq echo "QA output created by $seq" here=`pwd` tmp=/tmp/$$ status=1 # failure is the default! trap "_cleanup; exit \$status" 0 1 2 3 15 _cleanup() { touch $tmp.done wait rm -f $tmp.* } . ./common/rc . ./common/filter . ./common/encrypt . ./common/renameat2 rm -f $seqres.full _supported_fs generic _require_scratch_encryption -v 2 _require_renameat2 noreplace _scratch_mkfs_encrypted &>> $seqres.full _scratch_mount runtime=$((5 * TIME_FACTOR)) dir=$SCRATCH_MNT/dir echo -e "\n# Creating encrypted directory containing files" mkdir $dir _add_enckey $SCRATCH_MNT "$TEST_RAW_KEY" _set_encpolicy $dir $TEST_KEY_IDENTIFIER for i in {1..100}; do touch $dir/$i done # This is the filename which we'll try to duplicate. inode=$(stat -c %i $dir/100) # ext4 checks for duplicate dentries when inserting one, which can hide the bug # this test is testing for. However, ext4 stops checking for duplicates once it # finds space for the new dentry. Therefore, we can circumvent ext4's duplicate # checking by creating space at the beginning of the directory block. rm $dir/1 echo -e "\n# Starting duplicate filename creator process" ( # Repeatedly try to create the filename $dir/100 (which already exists) # using syscalls that should fail if the file already exists: mkdir(), # mknod(), symlink(), link(), and renameat2(RENAME_NOREPLACE). This # hopefully detects any one of them having the bug. TODO: we should # also try open(O_EXCL|O_CREAT), but it needs a command-line tool. while [ ! -e $tmp.done ]; do if mkdir $dir/100 &> /dev/null; then touch $tmp.mkdir_succeeded fi if mknod $dir/100 c 5 5 &> /dev/null; then touch $tmp.mknod_succeeded fi if ln -s target $dir/100 &> /dev/null; then touch $tmp.symlink_succeeded fi if ln $dir/50 $dir/100 &> /dev/null; then touch $tmp.link_succeeded fi if $here/src/renameat2 -n $dir/50 $dir/100 &> /dev/null; then touch $tmp.rename_noreplace_succeeded fi done ) & echo -e "\n# Starting add/remove enckey process" ( # Repeatedly add and remove the encryption key for $dir. The actual # race this test is trying to reproduce occurs when adding the key. while [ ! -e $tmp.done ]; do _add_enckey $SCRATCH_MNT "$TEST_RAW_KEY" > /dev/null _rm_enckey $SCRATCH_MNT $TEST_KEY_IDENTIFIER > /dev/null done ) & echo -e "\n# Running for a few seconds..." sleep $runtime echo -e "\n# Stopping subprocesses" touch $tmp.done wait _add_enckey $SCRATCH_MNT "$TEST_RAW_KEY" > /dev/null # Check for failure in several different ways, since different ways work on # different filesystems. E.g. ext4 shows duplicate filenames but ubifs doesn't. echo -e "\n# Checking for duplicate filenames via readdir" ls $dir | grep 100 echo -e "\n# Checking for unexpected change in inode number" new_inode=$(stat -c %i $dir/100) if [ $new_inode != $inode ]; then echo "Dentry changed inode number $inode => $new_inode!" fi echo -e "\n# Checking for operations that unexpectedly succeeded on an existing filename" for op in "mkdir" "mknod" "symlink" "link" "rename_noreplace"; do if [ -e $tmp.${op}_succeeded ]; then echo "$op operation(s) on existing filename unexpectedly succeeded!" fi done # Also check that the fsck program can't find any duplicate filenames. # For ext4, override _check_scratch_fs() so that we can specify -D (optimize # directories); otherwise e2fsck doesn't check for duplicate filenames. echo -e "\n# Checking for duplicate filenames via fsck" _scratch_unmount if [ "$FSTYP" = ext4 ]; then if ! e2fsck -f -y -D $SCRATCH_DEV &>> $seqres.full; then _log_err "filesystem on $SCRATCH_DEV is inconsistent" fi else _check_scratch_fs fi # success, all done status=0 exit