// SPDX-License-Identifier: GPL-2.0 /* * KUnit tests for ext4 directory hash computation. */ #include #include #include #include #include #include #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");