summaryrefslogtreecommitdiff
path: root/fs/smb/server/tests/smbacl_kunit.c
blob: 33496b4d31a3ece29b9fc8f3b8b1f67c3d8348fc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
// 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");