| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * KUnit tests for ext4 directory hash computation. |
| */ |
| |
| #include <kunit/test.h> |
| #include <kunit/resource.h> |
| #include <linux/fs.h> |
| #include <linux/stddef.h> |
| #include <linux/string.h> |
| #include <linux/unicode.h> |
| #include "ext4.h" |
| |
| static void ext4_hash_init_fake_dir(struct inode *dir, struct super_block *sb) |
| { |
| memset(sb, 0, sizeof(*sb)); |
| memset(dir, 0, sizeof(*dir)); |
| dir->i_sb = sb; |
| strscpy(sb->s_id, "kunit-ext4", sizeof(sb->s_id)); |
| } |
| |
| static void ext4_hash_init_fake_dir_with_sbi(struct inode *dir, |
| struct super_block *sb, |
| struct ext4_sb_info *sbi) |
| { |
| ext4_hash_init_fake_dir(dir, sb); |
| memset(sbi, 0, sizeof(*sbi)); |
| sb->s_fs_info = sbi; |
| sbi->s_sb = sb; |
| } |
| |
| #ifdef CONFIG_FS_ENCRYPTION |
| static const struct fscrypt_operations ext4_hash_test_cryptops = { |
| .inode_info_offs = |
| (int)offsetof(struct ext4_inode_info, i_crypt_info) - |
| (int)offsetof(struct ext4_inode_info, vfs_inode), |
| }; |
| #endif |
| |
| static void ext4_hash_init_fake_ext4_dir(struct ext4_inode_info *ei, |
| struct super_block *sb, |
| struct ext4_sb_info *sbi) |
| { |
| struct inode *dir = &ei->vfs_inode; |
| |
| memset(sb, 0, sizeof(*sb)); |
| memset(ei, 0, sizeof(*ei)); |
| memset(sbi, 0, sizeof(*sbi)); |
| |
| strscpy(sb->s_id, "kunit-ext4", sizeof(sb->s_id)); |
| sb->s_fs_info = sbi; |
| sbi->s_sb = sb; |
| |
| dir->i_sb = sb; |
| dir->i_mode = S_IFDIR; |
| |
| #ifdef CONFIG_FS_ENCRYPTION |
| fscrypt_set_ops(sb, &ext4_hash_test_cryptops); |
| #endif |
| } |
| |
| struct ext4_dirhash_test_case { |
| const char *name; |
| u32 hash_version; |
| const char *input; |
| int len; |
| u32 seed[4]; |
| bool use_seed; |
| u32 expected_hash; |
| u32 expected_minor_hash; |
| }; |
| |
| static const struct ext4_dirhash_test_case ext4_dirhash_test_cases[] = { |
| { |
| .name = "legacy_abc", |
| .hash_version = DX_HASH_LEGACY, |
| .input = "abc", |
| .len = 3, |
| .use_seed = false, |
| .expected_hash = 0x75afd992, |
| .expected_minor_hash = 0x00000000, |
| }, |
| { |
| .name = "legacy_unsigned_abc", |
| .hash_version = DX_HASH_LEGACY_UNSIGNED, |
| .input = "abc", |
| .len = 3, |
| .use_seed = false, |
| .expected_hash = 0x75afd992, |
| .expected_minor_hash = 0x00000000, |
| }, |
| { |
| .name = "half_md4_abc", |
| .hash_version = DX_HASH_HALF_MD4, |
| .input = "abc", |
| .len = 3, |
| .use_seed = false, |
| .expected_hash = 0xd196a868, |
| .expected_minor_hash = 0xc420eb28, |
| }, |
| { |
| .name = "half_md4_unsigned_abc", |
| .hash_version = DX_HASH_HALF_MD4_UNSIGNED, |
| .input = "abc", |
| .len = 3, |
| .use_seed = false, |
| .expected_hash = 0xd196a868, |
| .expected_minor_hash = 0xc420eb28, |
| }, |
| { |
| .name = "tea_abc", |
| .hash_version = DX_HASH_TEA, |
| .input = "abc", |
| .len = 3, |
| .use_seed = false, |
| .expected_hash = 0xb1435ec4, |
| .expected_minor_hash = 0x3f7eaa0e, |
| }, |
| { |
| .name = "tea_unsigned_abc", |
| .hash_version = DX_HASH_TEA_UNSIGNED, |
| .input = "abc", |
| .len = 3, |
| .use_seed = false, |
| .expected_hash = 0xb1435ec4, |
| .expected_minor_hash = 0x3f7eaa0e, |
| }, |
| { |
| .name = "empty_half_md4", |
| .hash_version = DX_HASH_HALF_MD4, |
| .input = "", |
| .len = 0, |
| .use_seed = false, |
| .expected_hash = 0xefcdab88, |
| .expected_minor_hash = 0x98badcfe, |
| }, |
| { |
| .name = "half_md4_31bytes", |
| .hash_version = DX_HASH_HALF_MD4, |
| .input = "1234567890123456789012345678901", |
| .len = 31, |
| .use_seed = false, |
| .expected_hash = 0xc4db1f78, |
| .expected_minor_hash = 0xea23921b, |
| }, |
| { |
| .name = "half_md4_32bytes", |
| .hash_version = DX_HASH_HALF_MD4, |
| .input = "12345678901234567890123456789012", |
| .len = 32, |
| .use_seed = false, |
| .expected_hash = 0xfa6cc63e, |
| .expected_minor_hash = 0x2f77bd1c, |
| }, |
| { |
| .name = "half_md4_33bytes", |
| .hash_version = DX_HASH_HALF_MD4, |
| .input = "123456789012345678901234567890123", |
| .len = 33, |
| .use_seed = false, |
| .expected_hash = 0xdc0c2dec, |
| .expected_minor_hash = 0x5ca23365, |
| }, |
| { |
| .name = "half_md4_unsigned_31bytes", |
| .hash_version = DX_HASH_HALF_MD4_UNSIGNED, |
| .input = "1234567890123456789012345678901", |
| .len = 31, |
| .use_seed = false, |
| .expected_hash = 0xc4db1f78, |
| .expected_minor_hash = 0xea23921b, |
| }, |
| { |
| .name = "half_md4_unsigned_32bytes", |
| .hash_version = DX_HASH_HALF_MD4_UNSIGNED, |
| .input = "12345678901234567890123456789012", |
| .len = 32, |
| .use_seed = false, |
| .expected_hash = 0xfa6cc63e, |
| .expected_minor_hash = 0x2f77bd1c, |
| }, |
| { |
| .name = "half_md4_unsigned_33bytes", |
| .hash_version = DX_HASH_HALF_MD4_UNSIGNED, |
| .input = "123456789012345678901234567890123", |
| .len = 33, |
| .use_seed = false, |
| .expected_hash = 0xdc0c2dec, |
| .expected_minor_hash = 0x5ca23365, |
| }, |
| { |
| .name = "tea_15bytes", |
| .hash_version = DX_HASH_TEA, |
| .input = "123456789abcdef", |
| .len = 15, |
| .use_seed = false, |
| .expected_hash = 0xa562903a, |
| .expected_minor_hash = 0x6174a00f, |
| }, |
| { |
| .name = "tea_16bytes", |
| .hash_version = DX_HASH_TEA, |
| .input = "1234567890abcdef", |
| .len = 16, |
| .use_seed = false, |
| .expected_hash = 0x8449f258, |
| .expected_minor_hash = 0x49a16d46, |
| }, |
| { |
| .name = "tea_17bytes", |
| .hash_version = DX_HASH_TEA, |
| .input = "123456789abcdefgh", |
| .len = 17, |
| .use_seed = false, |
| .expected_hash = 0xf32ec10c, |
| .expected_minor_hash = 0x58ceae61, |
| }, |
| { |
| .name = "half_md4_seeded", |
| .hash_version = DX_HASH_HALF_MD4, |
| .input = "same-name", |
| .len = 9, |
| .seed = { 0x11111111, 0x22222222, 0x33333333, 0x44444444 }, |
| .use_seed = true, |
| .expected_hash = 0x8aebf604, |
| .expected_minor_hash = 0x66ce48fe, |
| }, |
| { |
| .name = "half_md4_non_ascii_signed", |
| .hash_version = DX_HASH_HALF_MD4, |
| .input = "\x80\x81\x82\x83\x84", |
| .len = 5, |
| .use_seed = false, |
| .expected_hash = 0x8bab0498, |
| .expected_minor_hash = 0xc326632d, |
| }, |
| { |
| .name = "half_md4_non_ascii_unsigned", |
| .hash_version = DX_HASH_HALF_MD4_UNSIGNED, |
| .input = "\x80\x81\x82\x83\x84", |
| .len = 5, |
| .use_seed = false, |
| .expected_hash = 0xbc48596e, |
| .expected_minor_hash = 0xde0fad41, |
| }, |
| { |
| .name = "tea_non_ascii_signed", |
| .hash_version = DX_HASH_TEA, |
| .input = "\x80\x81\x82\x83\x84", |
| .len = 5, |
| .use_seed = false, |
| .expected_hash = 0x21e3a154, |
| .expected_minor_hash = 0x90112c3d, |
| }, |
| { |
| .name = "tea_non_ascii_unsigned", |
| .hash_version = DX_HASH_TEA_UNSIGNED, |
| .input = "\x80\x81\x82\x83\x84", |
| .len = 5, |
| .use_seed = false, |
| .expected_hash = 0x9b648616, |
| .expected_minor_hash = 0x011dd507, |
| }, |
| }; |
| |
| static void test_ext4fs_dirhash_vectors(struct kunit *test) |
| { |
| struct super_block *sb; |
| struct inode *dir; |
| int i; |
| |
| sb = kunit_kzalloc(test, sizeof(*sb), GFP_KERNEL); |
| dir = kunit_kzalloc(test, sizeof(*dir), GFP_KERNEL); |
| KUNIT_ASSERT_NOT_NULL(test, sb); |
| KUNIT_ASSERT_NOT_NULL(test, dir); |
| |
| ext4_hash_init_fake_dir(dir, sb); |
| |
| for (i = 0; i < ARRAY_SIZE(ext4_dirhash_test_cases); i++) { |
| const struct ext4_dirhash_test_case *tc = |
| &ext4_dirhash_test_cases[i]; |
| struct dx_hash_info hinfo; |
| int ret; |
| |
| memset(&hinfo, 0, sizeof(hinfo)); |
| hinfo.hash_version = tc->hash_version; |
| hinfo.seed = tc->use_seed ? (u32 *)tc->seed : NULL; |
| |
| ret = ext4fs_dirhash(dir, tc->input, tc->len, &hinfo); |
| |
| KUNIT_ASSERT_EQ_MSG(test, ret, 0, "case=%s", tc->name); |
| KUNIT_EXPECT_EQ_MSG(test, hinfo.hash, tc->expected_hash, |
| "case=%s", tc->name); |
| KUNIT_EXPECT_EQ_MSG(test, hinfo.minor_hash, |
| tc->expected_minor_hash, |
| "case=%s", tc->name); |
| } |
| } |
| |
| static void test_ext4fs_dirhash_seed_changes_result(struct kunit *test) |
| { |
| struct super_block *sb; |
| struct inode *dir; |
| u32 seed[4] = { 0x11111111, 0x22222222, 0x33333333, 0x44444444 }; |
| struct dx_hash_info plain = { |
| .hash_version = DX_HASH_HALF_MD4, |
| }; |
| struct dx_hash_info seeded = { |
| .hash_version = DX_HASH_HALF_MD4, |
| .seed = seed, |
| }; |
| int ret_plain, ret_seeded; |
| |
| sb = kunit_kzalloc(test, sizeof(*sb), GFP_KERNEL); |
| dir = kunit_kzalloc(test, sizeof(*dir), GFP_KERNEL); |
| KUNIT_ASSERT_NOT_NULL(test, sb); |
| KUNIT_ASSERT_NOT_NULL(test, dir); |
| |
| ext4_hash_init_fake_dir(dir, sb); |
| |
| ret_plain = ext4fs_dirhash(dir, "same-name", 9, &plain); |
| ret_seeded = ext4fs_dirhash(dir, "same-name", 9, &seeded); |
| |
| KUNIT_ASSERT_EQ(test, ret_plain, 0); |
| KUNIT_ASSERT_EQ(test, ret_seeded, 0); |
| |
| KUNIT_EXPECT_TRUE(test, |
| plain.hash != seeded.hash || |
| plain.minor_hash != seeded.minor_hash); |
| } |
| |
| static void test_ext4fs_dirhash_invalid_version_returns_einval(struct kunit *test) |
| { |
| struct super_block *sb; |
| struct inode *dir; |
| struct ext4_sb_info *sbi; |
| struct dx_hash_info hinfo = { |
| .hash = 0xdeadbeef, |
| .minor_hash = 0xcafebabe, |
| .hash_version = DX_HASH_LAST + 1, |
| }; |
| int ret; |
| |
| sb = kunit_kzalloc(test, sizeof(*sb), GFP_KERNEL); |
| dir = kunit_kzalloc(test, sizeof(*dir), GFP_KERNEL); |
| sbi = kunit_kzalloc(test, sizeof(*sbi), GFP_KERNEL); |
| KUNIT_ASSERT_NOT_NULL(test, sb); |
| KUNIT_ASSERT_NOT_NULL(test, dir); |
| KUNIT_ASSERT_NOT_NULL(test, sbi); |
| |
| ext4_hash_init_fake_dir_with_sbi(dir, sb, sbi); |
| |
| ret = ext4fs_dirhash(dir, "abc", 3, &hinfo); |
| |
| KUNIT_EXPECT_EQ(test, ret, -EINVAL); |
| KUNIT_EXPECT_EQ(test, hinfo.hash, 0); |
| KUNIT_EXPECT_EQ(test, hinfo.minor_hash, 0); |
| } |
| |
| static void test_ext4fs_dirhash_siphash_without_key_returns_einval(struct kunit *test) |
| { |
| struct super_block *sb; |
| struct ext4_inode_info *ei; |
| struct inode *dir; |
| struct ext4_sb_info *sbi; |
| struct dx_hash_info hinfo = { |
| .hash_version = DX_HASH_SIPHASH, |
| }; |
| int ret; |
| |
| sb = kunit_kzalloc(test, sizeof(*sb), GFP_KERNEL); |
| ei = kunit_kzalloc(test, sizeof(*ei), GFP_KERNEL); |
| sbi = kunit_kzalloc(test, sizeof(*sbi), GFP_KERNEL); |
| KUNIT_ASSERT_NOT_NULL(test, sb); |
| KUNIT_ASSERT_NOT_NULL(test, ei); |
| KUNIT_ASSERT_NOT_NULL(test, sbi); |
| |
| ext4_hash_init_fake_ext4_dir(ei, sb, sbi); |
| dir = &ei->vfs_inode; |
| |
| ret = ext4fs_dirhash(dir, "name", strlen("name"), &hinfo); |
| |
| KUNIT_EXPECT_EQ(test, ret, -EINVAL); |
| } |
| |
| static void test_ext4fs_dirhash_signed_unsigned_differ_on_nonascii(struct kunit *test) |
| { |
| struct super_block *sb; |
| struct inode *dir; |
| static const char input[] = "\x80\xff\x81\xfe\101bc"; |
| struct dx_hash_info legacy_signed = { |
| .hash_version = DX_HASH_LEGACY, |
| }; |
| struct dx_hash_info legacy_unsigned = { |
| .hash_version = DX_HASH_LEGACY_UNSIGNED, |
| }; |
| struct dx_hash_info md4_signed = { |
| .hash_version = DX_HASH_HALF_MD4, |
| }; |
| struct dx_hash_info md4_unsigned = { |
| .hash_version = DX_HASH_HALF_MD4_UNSIGNED, |
| }; |
| struct dx_hash_info tea_signed = { |
| .hash_version = DX_HASH_TEA, |
| }; |
| struct dx_hash_info tea_unsigned = { |
| .hash_version = DX_HASH_TEA_UNSIGNED, |
| }; |
| int ret; |
| |
| sb = kunit_kzalloc(test, sizeof(*sb), GFP_KERNEL); |
| dir = kunit_kzalloc(test, sizeof(*dir), GFP_KERNEL); |
| KUNIT_ASSERT_NOT_NULL(test, sb); |
| KUNIT_ASSERT_NOT_NULL(test, dir); |
| |
| ext4_hash_init_fake_dir(dir, sb); |
| |
| ret = ext4fs_dirhash(dir, input, sizeof(input) - 1, &legacy_signed); |
| KUNIT_ASSERT_EQ(test, ret, 0); |
| ret = ext4fs_dirhash(dir, input, sizeof(input) - 1, &legacy_unsigned); |
| KUNIT_ASSERT_EQ(test, ret, 0); |
| KUNIT_EXPECT_NE(test, legacy_signed.hash, legacy_unsigned.hash); |
| |
| ret = ext4fs_dirhash(dir, input, sizeof(input) - 1, &md4_signed); |
| KUNIT_ASSERT_EQ(test, ret, 0); |
| ret = ext4fs_dirhash(dir, input, sizeof(input) - 1, &md4_unsigned); |
| KUNIT_ASSERT_EQ(test, ret, 0); |
| KUNIT_EXPECT_TRUE(test, |
| md4_signed.hash != md4_unsigned.hash || |
| md4_signed.minor_hash != md4_unsigned.minor_hash); |
| |
| ret = ext4fs_dirhash(dir, input, sizeof(input) - 1, &tea_signed); |
| KUNIT_ASSERT_EQ(test, ret, 0); |
| ret = ext4fs_dirhash(dir, input, sizeof(input) - 1, &tea_unsigned); |
| KUNIT_ASSERT_EQ(test, ret, 0); |
| KUNIT_EXPECT_TRUE(test, |
| tea_signed.hash != tea_unsigned.hash || |
| tea_signed.minor_hash != tea_unsigned.minor_hash); |
| } |
| |
| #if IS_ENABLED(CONFIG_UNICODE) |
| KUNIT_DEFINE_ACTION_WRAPPER(utf8_unload_action, utf8_unload, |
| struct unicode_map *); |
| static void test_ext4fs_dirhash_casefolded_names_hash_consistently(struct kunit *test) |
| { |
| struct super_block *sb; |
| struct ext4_inode_info *ei; |
| struct ext4_sb_info *sbi; |
| struct unicode_map *um; |
| struct dx_hash_info h1 = { |
| .hash_version = DX_HASH_HALF_MD4, |
| }; |
| struct dx_hash_info h2 = { |
| .hash_version = DX_HASH_HALF_MD4, |
| }; |
| int ret, ret1, ret2; |
| |
| sb = kunit_kzalloc(test, sizeof(*sb), GFP_KERNEL); |
| ei = kunit_kzalloc(test, sizeof(*ei), GFP_KERNEL); |
| sbi = kunit_kzalloc(test, sizeof(*sbi), GFP_KERNEL); |
| KUNIT_ASSERT_NOT_NULL(test, sb); |
| KUNIT_ASSERT_NOT_NULL(test, ei); |
| KUNIT_ASSERT_NOT_NULL(test, sbi); |
| |
| um = utf8_load(UTF8_LATEST); |
| if (IS_ERR(um)) { |
| kunit_skip(test, "utf8_load(UTF8_LATEST) failed: %pe", |
| um); |
| return; |
| } |
| |
| ret = kunit_add_action_or_reset(test, utf8_unload_action, um); |
| KUNIT_ASSERT_EQ(test, ret, 0); |
| |
| ext4_hash_init_fake_ext4_dir(ei, sb, sbi); |
| sb->s_encoding = um; |
| ei->vfs_inode.i_flags |= S_CASEFOLD; |
| |
| KUNIT_ASSERT_TRUE(test, IS_CASEFOLDED(&ei->vfs_inode)); |
| |
| ret1 = ext4fs_dirhash(&ei->vfs_inode, "Alpha", 5, &h1); |
| ret2 = ext4fs_dirhash(&ei->vfs_inode, "aLPHa", 5, &h2); |
| |
| KUNIT_ASSERT_EQ(test, ret1, 0); |
| KUNIT_ASSERT_EQ(test, ret2, 0); |
| KUNIT_EXPECT_EQ(test, h1.hash, h2.hash); |
| KUNIT_EXPECT_EQ(test, h1.minor_hash, h2.minor_hash); |
| } |
| |
| static void test_ext4fs_dirhash_casefold_fallback(struct kunit *test) |
| { |
| struct super_block *sb_cf, *sb_plain; |
| struct ext4_inode_info *ei; |
| struct ext4_sb_info *sbi; |
| struct inode *plain_dir; |
| struct unicode_map *um; |
| static const char invalid_utf8[] = "\xc3\x28"; |
| struct dx_hash_info folded_dir = { |
| .hash_version = DX_HASH_HALF_MD4, |
| }; |
| struct dx_hash_info plain = { |
| .hash_version = DX_HASH_HALF_MD4, |
| }; |
| int ret, ret_cf, ret_plain; |
| |
| sb_cf = kunit_kzalloc(test, sizeof(*sb_cf), GFP_KERNEL); |
| sb_plain = kunit_kzalloc(test, sizeof(*sb_plain), GFP_KERNEL); |
| ei = kunit_kzalloc(test, sizeof(*ei), GFP_KERNEL); |
| sbi = kunit_kzalloc(test, sizeof(*sbi), GFP_KERNEL); |
| plain_dir = kunit_kzalloc(test, sizeof(*plain_dir), GFP_KERNEL); |
| KUNIT_ASSERT_NOT_NULL(test, sb_cf); |
| KUNIT_ASSERT_NOT_NULL(test, sb_plain); |
| KUNIT_ASSERT_NOT_NULL(test, ei); |
| KUNIT_ASSERT_NOT_NULL(test, sbi); |
| KUNIT_ASSERT_NOT_NULL(test, plain_dir); |
| |
| um = utf8_load(UTF8_LATEST); |
| if (IS_ERR(um)) { |
| kunit_skip(test, "utf8_load(UTF8_LATEST) failed: %pe", |
| um); |
| return; |
| } |
| |
| ret = kunit_add_action_or_reset(test, utf8_unload_action, um); |
| KUNIT_ASSERT_EQ(test, ret, 0); |
| |
| ext4_hash_init_fake_ext4_dir(ei, sb_cf, sbi); |
| sb_cf->s_encoding = um; |
| ei->vfs_inode.i_flags |= S_CASEFOLD; |
| |
| KUNIT_ASSERT_TRUE(test, IS_CASEFOLDED(&ei->vfs_inode)); |
| |
| ext4_hash_init_fake_dir(plain_dir, sb_plain); |
| |
| ret_cf = ext4fs_dirhash(&ei->vfs_inode, invalid_utf8, |
| sizeof(invalid_utf8) - 1, &folded_dir); |
| ret_plain = ext4fs_dirhash(plain_dir, invalid_utf8, |
| sizeof(invalid_utf8) - 1, &plain); |
| |
| KUNIT_ASSERT_EQ(test, ret_cf, 0); |
| KUNIT_ASSERT_EQ(test, ret_plain, 0); |
| KUNIT_EXPECT_EQ(test, folded_dir.hash, plain.hash); |
| KUNIT_EXPECT_EQ(test, folded_dir.minor_hash, plain.minor_hash); |
| } |
| #endif |
| |
| static struct kunit_case ext4_hash_test_cases[] = { |
| KUNIT_CASE(test_ext4fs_dirhash_vectors), |
| KUNIT_CASE(test_ext4fs_dirhash_seed_changes_result), |
| KUNIT_CASE(test_ext4fs_dirhash_invalid_version_returns_einval), |
| KUNIT_CASE(test_ext4fs_dirhash_siphash_without_key_returns_einval), |
| KUNIT_CASE(test_ext4fs_dirhash_signed_unsigned_differ_on_nonascii), |
| #if IS_ENABLED(CONFIG_UNICODE) |
| KUNIT_CASE(test_ext4fs_dirhash_casefolded_names_hash_consistently), |
| KUNIT_CASE(test_ext4fs_dirhash_casefold_fallback), |
| #endif |
| {} |
| }; |
| |
| static struct kunit_suite ext4_hash_test_suite = { |
| .name = "ext4_hash", |
| .test_cases = ext4_hash_test_cases, |
| }; |
| |
| kunit_test_suites(&ext4_hash_test_suite); |
| |
| MODULE_LICENSE("GPL"); |