GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/acl.cc
Date: 2026-04-05 02:35:23
Exec Total Coverage
Lines: 135 151 89.4%
Branches: 101 132 76.5%

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