GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/acl.cc
Date: 2025-08-31 02:39:21
Exec Total Coverage
Lines: 138 151 91.4%
Branches: 104 132 78.8%

Line Branch Exec Source
1 /**
2 * This file is part of the CernVM File System.
3 */
4
5 #include "acl.h"
6
7 #include <string.h>
8
9 #include <algorithm>
10 #include <cassert>
11 #include <cstring>
12 #include <vector>
13
14 #include "util/posix.h"
15
16 using namespace std; // NOLINT
17
18 #ifdef COMPARE_TO_LIBACL
19 #include "acl/libacl.h"
20 #else // COMPARE_TO_LIBACL
21
22 // ACL permission bits
23 #define ACL_READ (0x04)
24 #define ACL_WRITE (0x02)
25 #define ACL_EXECUTE (0x01)
26
27 // ACL tag types
28 #define ACL_UNDEFINED_TAG (0x00)
29 #define ACL_USER_OBJ (0x01)
30 #define ACL_USER (0x02)
31 #define ACL_GROUP_OBJ (0x04)
32 #define ACL_GROUP (0x08)
33 #define ACL_MASK (0x10)
34 #define ACL_OTHER (0x20)
35
36 // ACL qualifier constants
37 #define ACL_UNDEFINED_ID ((id_t) - 1)
38
39 #endif // COMPARE_TO_LIBACL
40
41 #define ACL_EA_VERSION 0x0002
42
43 // ACL data structures
44 struct acl_ea_entry {
45 u_int16_t e_tag;
46 u_int16_t e_perm;
47 u_int32_t e_id;
48
49 // implements sorting compatible with libacl
50 4557 bool operator<(const acl_ea_entry &other) const {
51
2/2
✓ Branch 0 taken 4263 times.
✓ Branch 1 taken 294 times.
4557 if (e_tag != other.e_tag) {
52 4263 return e_tag < other.e_tag;
53 }
54 294 return e_id < other.e_id;
55 }
56 };
57
58 struct acl_ea_header {
59 u_int32_t a_version;
60 acl_ea_entry a_entries[0];
61 };
62
63 1078 static int acl_from_text_to_string_entries(const string &acl_string,
64 vector<string> &string_entries) {
65 1078 std::size_t entry_pos = 0;
66
2/2
✓ Branch 0 taken 4263 times.
✓ Branch 1 taken 686 times.
4949 while (entry_pos != string::npos) {
67 size_t entry_length;
68 size_t next_pos;
69 4263 size_t const sep_pos = acl_string.find_first_of(",\n", entry_pos);
70
2/2
✓ Branch 0 taken 1078 times.
✓ Branch 1 taken 3185 times.
4263 if (sep_pos == string::npos) {
71
2/2
✓ Branch 1 taken 686 times.
✓ Branch 2 taken 392 times.
1078 if (acl_string.length() > entry_pos) {
72 686 entry_length = acl_string.length() - entry_pos;
73 686 next_pos = string::npos;
74 } else {
75 // we've just looked past a trailing delimiter
76 392 break;
77 }
78 } else {
79
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3185 times.
3185 assert(sep_pos >= entry_pos);
80 3185 entry_length = sep_pos - entry_pos;
81 3185 next_pos = sep_pos + 1;
82 }
83
2/2
✓ Branch 0 taken 539 times.
✓ Branch 1 taken 3332 times.
3871 if (entry_length == 0) {
84 // libacl tolerates excessive whitespace but not excessive delimiters.
85 // It's simpler for us to treat whitespace as delimiters.
86 539 entry_pos = next_pos;
87 539 continue;
88 }
89
1/2
✓ Branch 1 taken 3332 times.
✗ Branch 2 not taken.
3332 string entry(acl_string, entry_pos, entry_length);
90 3332 entry_pos = next_pos;
91
92 // search for '#'-starting comment, discard if found
93 3332 size_t const comment_pos = entry.find('#');
94
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3332 times.
3332 if (comment_pos != string::npos) {
95 entry = string(entry, 0, comment_pos);
96 }
97
98 // TODO(autkin): trim whitespace on both ends
99
100 // discard empty lines
101
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 3332 times.
3332 if (entry.length() == 0) {
102 continue;
103 }
104
105
1/2
✓ Branch 1 taken 3332 times.
✗ Branch 2 not taken.
3332 string_entries.push_back(entry);
106
1/2
✓ Branch 1 taken 3332 times.
✗ Branch 2 not taken.
3332 }
107 1078 return 0;
108 }
109
110 3038 static int acl_parms_from_text(const string &str, u_int16_t *perms) {
111 // Currently unsupported syntax features found in setfacl:
112 // - X (capital x)
113 // - numeric syntax
114 // See "man 1 setfacl", "The perms field is..."
115
116 3038 *perms = 0;
117
2/2
✓ Branch 5 taken 5488 times.
✓ Branch 6 taken 3038 times.
8526 for (const char &c : str) {
118
4/5
✓ Branch 0 taken 2009 times.
✓ Branch 1 taken 1225 times.
✓ Branch 2 taken 588 times.
✓ Branch 3 taken 1666 times.
✗ Branch 4 not taken.
5488 switch (c) {
119 2009 case 'r':
120 2009 *perms |= ACL_READ;
121 2009 break;
122 1225 case 'w':
123 1225 *perms |= ACL_WRITE;
124 1225 break;
125 588 case 'x':
126 588 *perms |= ACL_EXECUTE;
127 588 break;
128 1666 case '-':
129 1666 break;
130 default:
131 return EINVAL;
132 }
133 }
134 3038 return 0;
135 }
136
137 3087 static int acl_entry_from_text(const string &str, acl_ea_entry &entry) {
138 // break down to 3 fields by ':'
139 // type:qualifier:permissions according to terminology
140 // e_tag:e_id:e_perm are acl_ea_entry field names
141 3087 size_t sep_pos = str.find(':');
142
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3087 times.
3087 if (sep_pos == string::npos) {
143 return EINVAL;
144 }
145
1/2
✓ Branch 1 taken 3087 times.
✗ Branch 2 not taken.
3087 string const type(str, 0, sep_pos);
146 3087 size_t next_field_pos = sep_pos + 1;
147 3087 sep_pos = str.find(':', next_field_pos);
148
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3087 times.
3087 if (sep_pos == string::npos) {
149 return EINVAL;
150 }
151
1/2
✓ Branch 1 taken 3087 times.
✗ Branch 2 not taken.
3087 string const qualifier(str, next_field_pos, sep_pos - next_field_pos);
152 3087 next_field_pos = sep_pos + 1;
153
1/2
✓ Branch 2 taken 3087 times.
✗ Branch 3 not taken.
3087 string const permissions(str, next_field_pos);
154
155
6/6
✓ Branch 1 taken 3038 times.
✓ Branch 2 taken 49 times.
✓ Branch 4 taken 1176 times.
✓ Branch 5 taken 1862 times.
✓ Branch 6 taken 1225 times.
✓ Branch 7 taken 1862 times.
3087 if (!type.compare("user") || !type.compare("u")) {
156
2/2
✓ Branch 1 taken 735 times.
✓ Branch 2 taken 490 times.
1225 entry.e_tag = qualifier.empty() ? ACL_USER_OBJ : ACL_USER;
157
6/6
✓ Branch 1 taken 1715 times.
✓ Branch 2 taken 147 times.
✓ Branch 4 taken 735 times.
✓ Branch 5 taken 980 times.
✓ Branch 6 taken 882 times.
✓ Branch 7 taken 980 times.
1862 } else if (!type.compare("group") || !type.compare("g")) {
158
2/2
✓ Branch 1 taken 686 times.
✓ Branch 2 taken 196 times.
882 entry.e_tag = qualifier.empty() ? ACL_GROUP_OBJ : ACL_GROUP;
159
6/6
✓ Branch 1 taken 931 times.
✓ Branch 2 taken 49 times.
✓ Branch 4 taken 637 times.
✓ Branch 5 taken 294 times.
✓ Branch 6 taken 686 times.
✓ Branch 7 taken 294 times.
980 } else if (!type.compare("other") || !type.compare("o")) {
160 686 entry.e_tag = ACL_OTHER;
161
6/6
✓ Branch 1 taken 147 times.
✓ Branch 2 taken 147 times.
✓ Branch 4 taken 98 times.
✓ Branch 5 taken 49 times.
✓ Branch 6 taken 245 times.
✓ Branch 7 taken 49 times.
294 } else if (!type.compare("mask") || !type.compare("m")) {
162 245 entry.e_tag = ACL_MASK;
163 } else {
164 49 return EINVAL;
165 }
166 3038 entry.e_tag = htole16(entry.e_tag);
167
168
2/2
✓ Branch 1 taken 2352 times.
✓ Branch 2 taken 686 times.
3038 if (qualifier.empty()) {
169 2352 entry.e_id = ACL_UNDEFINED_ID;
170 } else {
171 char *at_null_terminator_if_number;
172 686 long number = strtol(qualifier.c_str(), &at_null_terminator_if_number, 10);
173
2/2
✓ Branch 0 taken 343 times.
✓ Branch 1 taken 343 times.
686 if (*at_null_terminator_if_number != '\0') {
174 bool ok;
175
2/2
✓ Branch 1 taken 245 times.
✓ Branch 2 taken 98 times.
343 if (entry.e_tag == htole16(ACL_USER)) {
176 [[maybe_unused]] gid_t main_gid;
177 uid_t uid;
178
1/2
✓ Branch 1 taken 245 times.
✗ Branch 2 not taken.
245 ok = GetUidOf(qualifier, &uid, &main_gid);
179 245 number = uid;
180
1/2
✓ Branch 1 taken 98 times.
✗ Branch 2 not taken.
98 } else if (entry.e_tag == htole16(ACL_GROUP)) {
181 gid_t gid;
182
1/2
✓ Branch 1 taken 98 times.
✗ Branch 2 not taken.
98 ok = GetGidOf(qualifier, &gid);
183 98 number = gid;
184 } else {
185 assert(false);
186 }
187
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 343 times.
343 if (!ok) {
188 return EINVAL;
189 }
190 }
191 686 entry.e_id = htole32(number);
192 }
193
194 // parse perms
195 u_int16_t host_byteorder_perms;
196 int ret;
197 3038 ret = acl_parms_from_text(permissions, &host_byteorder_perms);
198
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3038 times.
3038 if (ret) {
199 return ret;
200 }
201 3038 entry.e_perm = htole16(host_byteorder_perms);
202
203 3038 return 0;
204 3087 }
205
206 1029 static bool acl_valid_builtin(const vector<acl_ea_entry> &entries) {
207 // From man acl_valid:
208 // The three required entries ACL_USER_OBJ, ACL_GROUP_OBJ, and ACL_OTHER must
209 // exist exactly once in the ACL.
210 //
211 // If the ACL contains any ACL_USER or ACL_GROUP entries, then an ACL_MASK
212 // entry is also required.
213 //
214 // The ACL may contain at most one ACL_MASK entry.
215 //
216 // The user identifiers must be unique among all entries of type ACL_USER.
217 // The group identifiers must be unique among all entries of type ACL_GROUP.
218
219 1029 bool types_met[ACL_OTHER + 1] = {
220 false,
221 };
222
223
2/2
✓ Branch 4 taken 2842 times.
✓ Branch 5 taken 980 times.
3822 for (auto entry_it = entries.begin(); entry_it != entries.end(); ++entry_it) {
224 2842 const acl_ea_entry &e = *entry_it;
225
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2842 times.
2842 assert(e.e_tag <= ACL_OTHER);
226 2842 bool &type_met = types_met[e.e_tag];
227
2/3
✓ Branch 0 taken 2254 times.
✓ Branch 1 taken 588 times.
✗ Branch 2 not taken.
2842 switch (e.e_tag) {
228 // at most one of these types
229 2254 case ACL_USER_OBJ:
230 case ACL_GROUP_OBJ:
231 case ACL_OTHER:
232 case ACL_MASK:
233
2/2
✓ Branch 0 taken 49 times.
✓ Branch 1 taken 2205 times.
2254 if (type_met) {
234 49 return false;
235 } else {
236 2205 type_met = true;
237 }
238 2205 break;
239 588 case ACL_USER:
240 case ACL_GROUP:
241 588 type_met = true;
242 588 break;
243 default:
244 assert(false);
245 }
246 }
247
4/4
✓ Branch 0 taken 637 times.
✓ Branch 1 taken 343 times.
✓ Branch 2 taken 588 times.
✓ Branch 3 taken 49 times.
980 if (!(types_met[ACL_USER_OBJ] && types_met[ACL_GROUP_OBJ]
248
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 588 times.
588 && types_met[ACL_OTHER])) {
249 392 return false;
250 }
251
6/6
✓ Branch 0 taken 343 times.
✓ Branch 1 taken 245 times.
✓ Branch 2 taken 49 times.
✓ Branch 3 taken 294 times.
✓ Branch 4 taken 49 times.
✓ Branch 5 taken 245 times.
588 if ((types_met[ACL_USER] || types_met[ACL_GROUP]) && !types_met[ACL_MASK]) {
252 49 return false;
253 }
254 // TODO(autkin): ACL_USER, ACL_GROUP uniqueness checks. Not a pressing issue.
255 539 return true;
256 }
257
258 1078 int acl_from_text_to_xattr_value(const string &textual_acl, char *&o_binary_acl,
259 size_t &o_size, bool &o_equiv_mode) {
260 int ret;
261
262 1078 o_equiv_mode = true;
263
264 // get individual textual entries from one big text
265 1078 vector<string> string_entries;
266
1/2
✓ Branch 1 taken 1078 times.
✗ Branch 2 not taken.
1078 ret = acl_from_text_to_string_entries(textual_acl, string_entries);
267
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1078 times.
1078 if (ret) {
268 return ret;
269 }
270
271 // get individual entries in structural form
272 1078 vector<acl_ea_entry> entries;
273 1078 for (auto string_it = string_entries.begin();
274
2/2
✓ Branch 2 taken 3087 times.
✓ Branch 3 taken 1029 times.
4116 string_it != string_entries.end();
275 3038 ++string_it) {
276 acl_ea_entry entry;
277
1/2
✓ Branch 2 taken 3087 times.
✗ Branch 3 not taken.
3087 ret = acl_entry_from_text(*string_it, entry);
278
2/2
✓ Branch 0 taken 49 times.
✓ Branch 1 taken 3038 times.
3087 if (ret) {
279 49 return ret;
280 }
281
4/4
✓ Branch 0 taken 2842 times.
✓ Branch 1 taken 196 times.
✓ Branch 2 taken 490 times.
✓ Branch 3 taken 2352 times.
3038 if (entry.e_tag == ACL_GROUP || entry.e_tag == ACL_USER) {
282 686 o_equiv_mode = false;
283 }
284
1/2
✓ Branch 1 taken 3038 times.
✗ Branch 2 not taken.
3038 entries.push_back(entry);
285 }
286
287 // sort entries as libacl does, to be able to use it in testing as a reference
288
1/2
✓ Branch 3 taken 1029 times.
✗ Branch 4 not taken.
1029 sort(entries.begin(), entries.end());
289
290 // reject what acl_valid() rejects, to be able to use it in testing
291
2/2
✓ Branch 1 taken 490 times.
✓ Branch 2 taken 539 times.
1029 if (!acl_valid_builtin(entries)) {
292 490 return EINVAL;
293 }
294
295 // if nothing but usual u,g,o bits, don't produce a binary. Mimicking libacl.
296
2/2
✓ Branch 0 taken 294 times.
✓ Branch 1 taken 245 times.
539 if (o_equiv_mode) {
297 294 o_binary_acl = NULL;
298 294 o_size = 0;
299 294 return 0;
300 }
301
302 // get one big buffer with all the entries in the "on-disk" xattr format
303 245 size_t const acl_entry_count = entries.size();
304 245 size_t const buf_size = sizeof(acl_ea_header)
305 245 + (acl_entry_count * sizeof(acl_ea_entry));
306 245 char *buf = static_cast<char *>(malloc(buf_size));
307
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 245 times.
245 if (!buf) {
308 return ENOMEM;
309 }
310 245 acl_ea_header *header = reinterpret_cast<acl_ea_header *>(buf);
311 245 header->a_version = htole32(ACL_EA_VERSION);
312 245 acl_ea_entry *ext_entry = reinterpret_cast<acl_ea_entry *>(header + 1);
313
2/2
✓ Branch 3 taken 1421 times.
✓ Branch 4 taken 245 times.
1666 for (auto entry_it = entries.begin(); entry_it != entries.end(); ++entry_it) {
314 1421 *ext_entry = *entry_it;
315 1421 ext_entry += 1;
316 }
317
318 245 o_binary_acl = buf;
319 245 o_size = buf_size;
320 245 return 0;
321 1078 }
322
323 #ifdef COMPARE_TO_LIBACL
324 int acl_from_text_to_xattr_value_libacl(const string textual_acl,
325 char *&o_binary_acl, size_t &o_size,
326 bool &o_equiv_mode) {
327 acl_t acl = acl_from_text(
328 textual_acl.c_str()); // Convert ACL string to acl_t object
329 if (!acl) {
330 return EINVAL;
331 }
332 if (acl_valid(acl) != 0) {
333 acl_free(acl);
334 return EINVAL;
335 }
336
337 // check if the ACL string contains more than the synthetic ACLs
338 int equiv = acl_equiv_mode(acl, NULL);
339 assert(equiv != -1);
340
341 o_equiv_mode = equiv == 0;
342 if (!o_equiv_mode) {
343 o_binary_acl = (char *)acl_to_xattr(acl, &o_size);
344 } else {
345 o_binary_acl = NULL;
346 o_size = 0;
347 }
348 acl_free(acl);
349 return 0;
350 }
351
352 int acl_from_text_to_xattr_value_both_impl(const string textual_acl,
353 char *&o_binary_acl, size_t &o_size,
354 bool &o_equiv_mode) {
355 struct impl_result {
356 int ret;
357 size_t binary_size;
358 char *binary_acl;
359 bool equiv_mode;
360 } b, l;
361 b.ret = acl_from_text_to_xattr_value(textual_acl, b.binary_acl, b.binary_size,
362 b.equiv_mode);
363 l.ret = acl_from_text_to_xattr_value_libacl(textual_acl, l.binary_acl,
364 l.binary_size, l.equiv_mode);
365 assert(b.ret == l.ret);
366 if (!l.ret) {
367 assert(b.binary_size == l.binary_size);
368 assert(0 == memcmp(b.binary_acl, l.binary_acl, b.binary_size));
369 assert(b.equiv_mode == l.equiv_mode);
370 free(l.binary_acl);
371 }
372 o_binary_acl = b.binary_acl;
373 o_size = b.binary_size;
374 o_equiv_mode = b.equiv_mode;
375 return b.ret;
376 }
377 #endif // COMPARE_TO_LIBACL
378