generic/563: use a loop device to avoid partition incompatibility
[xfstests-dev.git] / tests / f2fs / 002
1 #!/bin/bash
2 # SPDX-License-Identifier: GPL-2.0-only
3 # Copyright 2020 Google LLC
4 #
5 # FS QA Test No. f2fs/002
6 #
7 # Test that when a file is both compressed and encrypted, the encryption is done
8 # correctly.  I.e., the correct ciphertext is written to disk.
9 #
10 # f2fs compression behaves as follows: the original data of a compressed file is
11 # divided into equal-sized clusters.  The cluster size is configurable, but it
12 # must be a power-of-2 multiple of the filesystem block size.  If the file size
13 # isn't a multiple of the cluster size, then the final cluster is "partial" and
14 # holds the remainder modulo the cluster size.  Each cluster is compressed
15 # independently.  Each cluster is stored compressed if it isn't partial and
16 # compression would save at least 1 block, otherwise it is stored uncompressed.
17 #
18 # If the file is also encrypted, then the data is encrypted after compression
19 # (or decrypted before decompression).  In a compressed cluster, the block
20 # numbers used in the IVs for encryption start at logical_block_number + 1 and
21 # increment from there.  E.g. if the first three clusters each compressed 8
22 # blocks to 6 blocks, then the IVs used will be 1..6, 9..14, 17..22.
23 # In comparison, uncompressed clusters would use 0..7, 8..15, 16..23.
24 #
25 # This test verifies that the encryption is actually being done in the expected
26 # way.  This is critical, since the f2fs filesystem implementation uses
27 # significantly different code for I/O to/from compressed files, and bugs (say,
28 # a bug that caused the encryption to be skipped) may not otherwise be detected.
29 #
30 # To do this test, we create a file that is both compressed and encrypted,
31 # retrieve its raw data from disk, decrypt it, decompress it, and compare the
32 # result to the original file.  We can't do it the other way around (compress
33 # and encrypt the original data, and compare it to the on-disk data) because
34 # compression can produce many different outputs from the same input.  E.g. the
35 # lz4 command-line tool may not give the same output as the kernel's lz4
36 # implementation, even though both outputs will decompress to the original data.
37 #
38 # f2fs supports multiple compression algorithms, but the choice of compression
39 # algorithm shouldn't make a difference for the purpose of this test.  So we
40 # just test LZ4.
41
42 seq=`basename $0`
43 seqres=$RESULT_DIR/$seq
44 echo "QA output created by $seq"
45
46 here=`pwd`
47 tmp=/tmp/$$
48 status=1        # failure is the default!
49 trap "_cleanup; exit \$status" 0 1 2 3 15
50
51 _cleanup()
52 {
53         cd /
54         rm -f $tmp.*
55 }
56
57 . ./common/rc
58 . ./common/filter
59 . ./common/f2fs
60 . ./common/encrypt
61
62 rm -f $seqres.full
63
64 _supported_fs f2fs
65
66 # Prerequisites to create a file that is both encrypted and LZ4-compressed
67 _require_scratch_encryption -v 2
68 _require_scratch_f2fs_compression lz4
69 _require_command "$CHATTR_PROG" chattr
70
71 # Prerequisites to verify the ciphertext of the file
72 _require_get_encryption_nonce_support
73 _require_xfs_io_command "fiemap" # for _get_ciphertext_block_list()
74 _require_test_program "fscrypt-crypt-util"
75 _require_command "$LZ4_PROG" lz4
76
77 # Test parameters
78 compress_log_size=4
79 num_compressible_clusters=5
80 num_incompressible_clusters=2
81
82 echo -e "\n# Creating filesystem that supports encryption and compression"
83 _scratch_mkfs -O encrypt,compression,extra_attr >> $seqres.full
84 _scratch_mount "-o compress_algorithm=lz4,compress_log_size=$compress_log_size"
85
86 dir=$SCRATCH_MNT/dir
87 file=$dir/file
88 block_size=$(_get_block_size $SCRATCH_MNT)
89 cluster_blocks=$((1 << compress_log_size))
90 cluster_bytes=$((cluster_blocks * block_size))
91 num_compressible_blocks=$((num_compressible_clusters * cluster_blocks))
92 num_compressible_bytes=$((num_compressible_clusters * cluster_bytes))
93
94 echo -e "\n# Creating directory"
95 mkdir $dir
96
97 echo -e "\n# Enabling encryption on the directory"
98 _add_enckey $SCRATCH_MNT "$TEST_RAW_KEY" >> $seqres.full
99 _set_encpolicy $dir $TEST_KEY_IDENTIFIER
100
101 echo -e "\n# Enabling compression on the directory"
102 $CHATTR_PROG +c $dir
103
104 echo -e "\n# Creating compressed+encrypted file"
105 for (( i = 0; i < num_compressible_clusters; i++ )); do
106         # Fill each compressible cluster with 2 blocks of zeroes, then the rest
107         # random data.  This should make the compression save 1 block.  (Not 2,
108         # due to overhead.)  We don't want the data to be *too* compressible,
109         # since we want to see the encryption IVs increment within each cluster.
110         head -c $(( 2 * block_size )) /dev/zero
111         head -c $(( (cluster_blocks - 2) * block_size )) /dev/urandom
112 done > $tmp.orig_data
113 # Also append some incompressible clusters, just in case there is some problem
114 # that affects only uncompressed data in a compressed file.
115 head -c $(( num_incompressible_clusters * cluster_bytes )) /dev/urandom \
116         >> $tmp.orig_data
117 # Also append a compressible partial cluster at the end, just in case there is
118 # some problem specific to partial clusters at EOF.  However, the current
119 # behavior of f2fs compression is that partial clusters are never compressed.
120 head -c $(( cluster_bytes - block_size )) /dev/zero >> $tmp.orig_data
121
122 cp $tmp.orig_data $file
123 inode=$(stat -c %i $file)
124
125 # Get the list of blocks that contain the file's raw data.
126 #
127 # This is a hack, because the only API to get this information is fiemap, which
128 # doesn't directly support compression as it assumes a 1:1 mapping between
129 # logical blocks and physical blocks.
130 #
131 # But as we have no other option, we use fiemap anyway.  We rely on some f2fs
132 # implementation details which make it work well enough in practice for the
133 # purpose of this test:
134 #
135 #   - f2fs writes the blocks of each compressed cluster contiguously.
136 #   - fiemap on a f2fs file gives an extent for each compressed cluster,
137 #     with length equal to its uncompressed size.
138 #
139 # Note that for each compressed cluster, there will be some extra blocks
140 # appended which aren't actually part of the file.  But it's simplest to just
141 # read these anyway and ignore them when actually doing the decompression.
142 blocklist=$(_get_ciphertext_block_list $file)
143
144 _scratch_unmount
145
146 echo -e "\n# Getting file's encryption nonce"
147 nonce=$(_get_encryption_nonce $SCRATCH_DEV $inode)
148
149 echo -e "\n# Dumping the file's raw data"
150 _dump_ciphertext_blocks $SCRATCH_DEV $blocklist > $tmp.raw
151
152 echo -e "\n# Decrypting the file's data"
153 TEST_RAW_KEY_HEX=$(echo "$TEST_RAW_KEY" | tr -d '\\x')
154 decrypt_blocks()
155 {
156         $here/src/fscrypt-crypt-util "$@"                       \
157                 --decrypt                                       \
158                 --block-size=$block_size                        \
159                 --file-nonce=$nonce                             \
160                 --kdf=HKDF-SHA512                               \
161                 AES-256-XTS                                     \
162                 $TEST_RAW_KEY_HEX
163 }
164 head -c $num_compressible_bytes $tmp.raw \
165         | decrypt_blocks --block-number=1 > $tmp.decrypted
166 dd if=$tmp.raw bs=$cluster_bytes skip=$num_compressible_clusters status=none \
167         | decrypt_blocks --block-number=$num_compressible_blocks \
168         >> $tmp.decrypted
169
170 # Decompress the compressed clusters using the lz4 command-line tool.
171 #
172 # Each f2fs compressed cluster begins with a 24-byte header, starting with the
173 # compressed size in bytes (excluding the header) as a __le32.  The header is
174 # followed by the actual compressed data; for LZ4, that means an LZ4 block.
175 #
176 # Unfortunately, the lz4 command-line tool only deals with LZ4 *frames*
177 # (https://github.com/lz4/lz4/blob/master/doc/lz4_Frame_format.md) and can't
178 # decompress LZ4 blocks directly.  So we have to extract the LZ4 block, then
179 # wrap it with a minimal LZ4 frame.
180
181 decompress_cluster()
182 {
183         if (( $(stat -c %s "$1") < 24 )); then
184                 _fail "Invalid compressed cluster (truncated)"
185         fi
186         compressed_size=$(od -td4 -N4 -An --endian=little $1 | awk '{print $1}')
187         if (( compressed_size <= 0 )); then
188                 _fail "Invalid compressed cluster (bad compressed size)"
189         fi
190         (
191                 echo -e -n '\x04\x22\x4d\x18' # LZ4 frame magic number
192                 echo -e -n '\x40\x70\xdf'     # LZ4 frame descriptor
193                 head -c 4 "$1"                # Compressed block size
194                 dd if="$1" skip=24 iflag=skip_bytes bs=$compressed_size \
195                         count=1 status=none
196                 echo -e -n '\x00\x00\x00\x00' # Next block size (none)
197         ) | $LZ4_PROG -d
198 }
199
200 echo -e "\n# Decompressing the file's data"
201 for (( i = 0; i < num_compressible_clusters; i++ )); do
202         dd if=$tmp.decrypted bs=$cluster_bytes skip=$i count=1 status=none \
203                 of=$tmp.cluster
204         decompress_cluster $tmp.cluster >> $tmp.uncompressed_data
205 done
206 # Append the incompressible clusters and the final partial cluster,
207 # neither of which should have been compressed.
208 dd if=$tmp.decrypted bs=$cluster_bytes skip=$num_compressible_clusters \
209         status=none >> $tmp.uncompressed_data
210
211 # Finally do the actual test.  The data we got after decryption+decompression
212 # should match the original file contents.
213 echo -e "\n# Comparing to original data"
214 cmp $tmp.uncompressed_data $tmp.orig_data
215
216 status=0
217 exit