| // SPDX-License-Identifier: GPL-2.0-or-later | |
| /* | |
| * KUnit tests for ksmbd security descriptor (DACL) handling. | |
| * | |
| * Copyright (C) 2026 Hang Nan <nanx95726@gmail.com> | |
| * | |
| * The tests pin the DACL declared-size boundary in smb_check_perm_dacl(): | |
| * | |
| * - ksmbd_dacl_walk_must_stop_at_declared_size: a pure semantic harness | |
| * that models the ACE walk. Walking to the end of the enclosing | |
| * security descriptor (the pre-fix behaviour) selects an ACE that | |
| * sits beyond struct smb_acl::size; stopping at the declared DACL | |
| * size (the fixed behaviour) rejects it. | |
| * | |
| * - ksmbd_smb_check_perm_dacl_boundary and | |
| * ksmbd_smb_check_perm_dacl_maximal_boundary: drive the real | |
| * smb_check_perm_dacl() with a descriptor stored through ksmbd's own | |
| * NTACL xattr path on a tmpfs file, and assert that a post-boundary | |
| * ACE is not selected for either a regular or maximal access check. | |
| */ | |
| #include <kunit/test.h> | |
| #include <linux/fs.h> | |
| #include <linux/mm.h> | |
| #include <linux/shmem_fs.h> | |
| #include <linux/slab.h> | |
| #include "../smbacl.h" | |
| #include "../smb_common.h" | |
| #include "../vfs.h" | |
| struct ksmbd_acl_walk_result { | |
| bool found; | |
| bool allowed; | |
| const struct smb_ace *selected; | |
| }; | |
| static const struct smb_sid test_nonmatching_sid = { | |
| 1, 5, {0, 0, 0, 0, 0, 5}, | |
| { cpu_to_le32(21), cpu_to_le32(1), cpu_to_le32(2), | |
| cpu_to_le32(3), cpu_to_le32(9999) } | |
| }; | |
| /* | |
| * S-1-22-1-0: the SID id_to_sid(0, SIDUNIX_USER) resolves to, i.e. what | |
| * smb_check_perm_dacl() looks for when called with uid == 0. | |
| */ | |
| static const struct smb_sid test_owner_sid = { | |
| 1, 2, {0, 0, 0, 0, 0, 22}, | |
| { cpu_to_le32(1), cpu_to_le32(0) } | |
| }; | |
| static int test_compare_sids(const struct smb_sid *a, const struct smb_sid *b) | |
| { | |
| int i; | |
| if (a->revision != b->revision || a->num_subauth != b->num_subauth) | |
| return 1; | |
| for (i = 0; i < NUM_AUTHS; i++) { | |
| if (a->authority[i] != b->authority[i]) | |
| return 1; | |
| } | |
| for (i = 0; i < a->num_subauth; i++) { | |
| if (a->sub_auth[i] != b->sub_auth[i]) | |
| return 1; | |
| } | |
| return 0; | |
| } | |
| static u16 test_ace_size(const struct smb_sid *sid) | |
| { | |
| return offsetof(struct smb_ace, sid) + CIFS_SID_BASE_SIZE + | |
| sid->num_subauth * sizeof(__le32); | |
| } | |
| static u16 fill_test_ace(struct smb_ace *ace, const struct smb_sid *sid, | |
| u32 access_req) | |
| { | |
| u16 size = test_ace_size(sid); | |
| ace->type = ACCESS_ALLOWED_ACE_TYPE; | |
| ace->flags = 0; | |
| ace->size = cpu_to_le16(size); | |
| ace->access_req = cpu_to_le32(access_req); | |
| memcpy(&ace->sid, sid, size - offsetof(struct smb_ace, sid)); | |
| return size; | |
| } | |
| static struct ksmbd_acl_walk_result test_walk_dacl(struct smb_acl *pdacl, | |
| int walk_boundary, | |
| const struct smb_sid *target, | |
| u32 requested) | |
| { | |
| struct ksmbd_acl_walk_result result = {}; | |
| struct smb_ace *ace; | |
| int aces_size; | |
| int i; | |
| ace = (struct smb_ace *)((char *)pdacl + sizeof(struct smb_acl)); | |
| aces_size = walk_boundary - sizeof(struct smb_acl); | |
| for (i = 0; i < le16_to_cpu(pdacl->num_aces); i++) { | |
| u16 ace_size; | |
| if (aces_size < offsetof(struct smb_ace, sid) + CIFS_SID_BASE_SIZE) | |
| break; | |
| ace_size = le16_to_cpu(ace->size); | |
| if (ace_size > aces_size || | |
| ace_size < offsetof(struct smb_ace, sid) + CIFS_SID_BASE_SIZE) | |
| break; | |
| aces_size -= ace_size; | |
| if (ace->sid.num_subauth > SID_MAX_SUB_AUTHORITIES || | |
| ace_size < offsetof(struct smb_ace, sid) + CIFS_SID_BASE_SIZE + | |
| sizeof(__le32) * ace->sid.num_subauth) | |
| break; | |
| if (!test_compare_sids(target, &ace->sid)) { | |
| result.found = true; | |
| result.selected = ace; | |
| result.allowed = !(requested & ~le32_to_cpu(ace->access_req)); | |
| return result; | |
| } | |
| ace = (struct smb_ace *)((char *)ace + ace_size); | |
| } | |
| return result; | |
| } | |
| static void ksmbd_dacl_walk_must_stop_at_declared_size(struct kunit *test) | |
| { | |
| struct ksmbd_acl_walk_result declared, enclosing; | |
| struct smb_acl *acl; | |
| struct smb_ace *ace1, *fake; | |
| u16 ace1_size, fake_size; | |
| u16 pdacl_size; | |
| u16 acl_size; | |
| acl = kunit_kzalloc(test, 128, GFP_KERNEL); | |
| KUNIT_ASSERT_NOT_NULL(test, acl); | |
| acl->revision = cpu_to_le16(2); | |
| acl->num_aces = cpu_to_le16(2); | |
| ace1 = (struct smb_ace *)((char *)acl + sizeof(*acl)); | |
| ace1_size = fill_test_ace(ace1, &test_nonmatching_sid, 0); | |
| fake = (struct smb_ace *)((char *)ace1 + ace1_size); | |
| fake_size = fill_test_ace(fake, &test_owner_sid, FILE_READ_DATA); | |
| pdacl_size = sizeof(*acl) + ace1_size; | |
| acl_size = pdacl_size + fake_size; | |
| acl->size = cpu_to_le16(pdacl_size); | |
| declared = test_walk_dacl(acl, pdacl_size, &test_owner_sid, | |
| FILE_READ_DATA); | |
| enclosing = test_walk_dacl(acl, acl_size, &test_owner_sid, | |
| FILE_READ_DATA); | |
| KUNIT_EXPECT_FALSE(test, declared.found); | |
| KUNIT_EXPECT_FALSE(test, declared.allowed); | |
| /* Demonstrates that the buggy acl_size boundary selects fake ACE #2. */ | |
| KUNIT_EXPECT_TRUE(test, enclosing.found); | |
| KUNIT_EXPECT_TRUE(test, enclosing.allowed); | |
| } | |
| /* | |
| * Build an NTSD whose DACL declares one ACE (pdacl->size) but actually | |
| * contains two: the second ACE sits beyond the declared DACL boundary | |
| * yet inside the enclosing security descriptor. The trailing ACE applies | |
| * to S-1-22-1-0, which smb_check_perm_dacl() looks for when uid is zero. | |
| */ | |
| static struct smb_ntsd *build_boundary_ntsd(struct kunit *test, | |
| const struct smb_sid *first_sid, | |
| u32 first_access, | |
| u32 trailing_access, | |
| int *ntsd_size) | |
| { | |
| struct smb_ntsd *pntsd; | |
| struct smb_acl *pdacl; | |
| struct smb_ace *ace; | |
| u16 first_size = test_ace_size(first_sid); | |
| u16 trailing_size = test_ace_size(&test_owner_sid); | |
| *ntsd_size = sizeof(struct smb_ntsd) + sizeof(struct smb_acl) + | |
| first_size + trailing_size; | |
| pntsd = kunit_kzalloc(test, *ntsd_size, GFP_KERNEL); | |
| if (!pntsd) | |
| return NULL; | |
| pntsd->revision = cpu_to_le16(SD_REVISION); | |
| pntsd->type = cpu_to_le16(DACL_PRESENT); | |
| pntsd->dacloffset = cpu_to_le32(sizeof(struct smb_ntsd)); | |
| pdacl = (struct smb_acl *)((char *)pntsd + sizeof(struct smb_ntsd)); | |
| pdacl->revision = cpu_to_le16(2); | |
| pdacl->num_aces = cpu_to_le16(2); | |
| pdacl->size = cpu_to_le16(sizeof(struct smb_acl) + first_size); | |
| ace = (struct smb_ace *)((char *)pdacl + sizeof(struct smb_acl)); | |
| fill_test_ace(ace, first_sid, first_access); | |
| ace = (struct smb_ace *)((char *)ace + first_size); | |
| fill_test_ace(ace, &test_owner_sid, trailing_access); | |
| return pntsd; | |
| } | |
| static void ksmbd_smb_check_perm_dacl_boundary_test(struct kunit *test) | |
| { | |
| struct file *file; | |
| struct smb_ntsd *pntsd; | |
| __le32 daccess = cpu_to_le32(FILE_READ_DATA); | |
| int ntsd_size, rc; | |
| pntsd = build_boundary_ntsd(test, &test_nonmatching_sid, 0, | |
| FILE_READ_DATA, &ntsd_size); | |
| KUNIT_ASSERT_NOT_NULL(test, pntsd); | |
| file = shmem_file_setup("ksmbd-kunit-dacl", 0, | |
| mk_vma_flags(VMA_NORESERVE_BIT)); | |
| KUNIT_ASSERT_NOT_ERR_OR_NULL(test, file); | |
| rc = ksmbd_vfs_set_sd_xattr(NULL, mnt_idmap(file->f_path.mnt), | |
| &file->f_path, pntsd, ntsd_size, | |
| false); | |
| KUNIT_EXPECT_EQ(test, 0, rc); | |
| if (rc) | |
| goto out; | |
| rc = smb_check_perm_dacl(NULL, &file->f_path, &daccess, | |
| cpu_to_le32(FILE_READ_DATA), 0, false); | |
| /* | |
| * The post-boundary ACE (ACE #2, beyond pdacl->size) grants | |
| * FILE_READ_DATA to the caller's SID, but it must not be | |
| * selected: the walk stops at the declared DACL size and access | |
| * is denied. Before the fix the walk used the enclosing | |
| * descriptor length, selected ACE #2 and returned 0. | |
| */ | |
| KUNIT_EXPECT_EQ(test, -EACCES, rc); | |
| out: | |
| fput(file); | |
| } | |
| static void | |
| ksmbd_smb_check_perm_dacl_maximal_boundary_test(struct kunit *test) | |
| { | |
| struct file *file; | |
| struct smb_ntsd *pntsd; | |
| __le32 daccess = FILE_MAXIMAL_ACCESS_LE; | |
| int ntsd_size, rc; | |
| /* | |
| * The in-boundary ACE grants read access. The trailing ACE grants | |
| * write access, which must not be included in the maximal access mask. | |
| */ | |
| pntsd = build_boundary_ntsd(test, &test_owner_sid, FILE_READ_DATA, | |
| FILE_WRITE_DATA, &ntsd_size); | |
| KUNIT_ASSERT_NOT_NULL(test, pntsd); | |
| file = shmem_file_setup("ksmbd-kunit-dacl-maximal", 0, | |
| mk_vma_flags(VMA_NORESERVE_BIT)); | |
| KUNIT_ASSERT_NOT_ERR_OR_NULL(test, file); | |
| rc = ksmbd_vfs_set_sd_xattr(NULL, mnt_idmap(file->f_path.mnt), | |
| &file->f_path, pntsd, ntsd_size, | |
| false); | |
| KUNIT_EXPECT_EQ(test, 0, rc); | |
| if (rc) | |
| goto out; | |
| rc = smb_check_perm_dacl(NULL, &file->f_path, &daccess, | |
| FILE_MAXIMAL_ACCESS_LE, 0, false); | |
| KUNIT_EXPECT_EQ(test, 0, rc); | |
| if (rc) | |
| goto out; | |
| KUNIT_EXPECT_TRUE(test, le32_to_cpu(daccess) & FILE_READ_DATA); | |
| KUNIT_EXPECT_FALSE(test, le32_to_cpu(daccess) & FILE_WRITE_DATA); | |
| out: | |
| fput(file); | |
| } | |
| static struct kunit_case ksmbd_smbacl_test_cases[] = { | |
| KUNIT_CASE(ksmbd_dacl_walk_must_stop_at_declared_size), | |
| KUNIT_CASE(ksmbd_smb_check_perm_dacl_boundary_test), | |
| KUNIT_CASE(ksmbd_smb_check_perm_dacl_maximal_boundary_test), | |
| {} | |
| }; | |
| static struct kunit_suite ksmbd_smbacl_test_suite = { | |
| .name = "ksmbd-smbacl", | |
| .test_cases = ksmbd_smbacl_test_cases, | |
| }; | |
| kunit_test_suite(ksmbd_smbacl_test_suite); | |
| MODULE_DESCRIPTION("KUnit tests for ksmbd smbacl helpers"); | |
| MODULE_LICENSE("GPL"); | |
| MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING"); |