2 # SPDX-License-Identifier: GPL-2.0-only
3 # Copyright 2020 Google LLC
5 # FS QA Test No. f2fs/002
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.
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.
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.
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.
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.
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
43 seqres=$RESULT_DIR/$seq
44 echo "QA output created by $seq"
48 status=1 # failure is the default!
49 trap "_cleanup; exit \$status" 0 1 2 3 15
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
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
79 num_compressible_clusters=5
80 num_incompressible_clusters=2
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"
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))
94 echo -e "\n# Creating directory"
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
101 echo -e "\n# Enabling compression on the directory"
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 \
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
122 cp $tmp.orig_data $file
123 inode=$(stat -c %i $file)
125 # Get the list of blocks that contain the file's raw data.
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.
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:
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.
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)
146 echo -e "\n# Getting file's encryption nonce"
147 nonce=$(_get_encryption_nonce $SCRATCH_DEV $inode)
149 echo -e "\n# Dumping the file's raw data"
150 _dump_ciphertext_blocks $SCRATCH_DEV $blocklist > $tmp.raw
152 echo -e "\n# Decrypting the file's data"
153 TEST_RAW_KEY_HEX=$(echo "$TEST_RAW_KEY" | tr -d '\\x')
156 $here/src/fscrypt-crypt-util "$@" \
158 --block-size=$block_size \
159 --file-nonce=$nonce \
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 \
170 # Decompress the compressed clusters using the lz4 command-line tool.
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.
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.
183 if (( $(stat -c %s "$1") < 24 )); then
184 _fail "Invalid compressed cluster (truncated)"
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)"
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 \
196 echo -e -n '\x00\x00\x00\x00' # Next block size (none)
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 \
204 decompress_cluster $tmp.cluster >> $tmp.uncompressed_data
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
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