GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/acl.cc
Date: 2026-05-19 11:45:12
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 2604 bool operator<(const acl_ea_entry &other) const {
57
2/2
✓ Branch 0 taken 2436 times.
✓ Branch 1 taken 168 times.
2604 if (e_tag != other.e_tag) {
58 2436 return e_tag < other.e_tag;
59 }
60 168 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 616 static int acl_from_text_to_string_entries(const string &acl_string,
70 vector<string> &string_entries) {
71 616 std::size_t entry_pos = 0;
72
2/2
✓ Branch 0 taken 2436 times.
✓ Branch 1 taken 392 times.
2828 while (entry_pos != string::npos) {
73 size_t entry_length;
74 size_t next_pos;
75 2436 size_t const sep_pos = acl_string.find_first_of(",\n", entry_pos);
76
2/2
✓ Branch 0 taken 616 times.
✓ Branch 1 taken 1820 times.
2436 if (sep_pos == string::npos) {
77
2/2
✓ Branch 1 taken 392 times.
✓ Branch 2 taken 224 times.
616 if (acl_string.length() > entry_pos) {
78 392 entry_length = acl_string.length() - entry_pos;
79 392 next_pos = string::npos;
80 } else {
81 // we've just looked past a trailing delimiter
82 224 break;
83 }
84 } else {
85
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1820 times.
1820 assert(sep_pos >= entry_pos);
86 1820 entry_length = sep_pos - entry_pos;
87 1820 next_pos = sep_pos + 1;
88 }
89
2/2
✓ Branch 0 taken 308 times.
✓ Branch 1 taken 1904 times.
2212 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 308 entry_pos = next_pos;
93 308 continue;
94 }
95
1/2
✓ Branch 1 taken 1904 times.
✗ Branch 2 not taken.
1904 string entry(acl_string, entry_pos, entry_length);
96 1904 entry_pos = next_pos;
97
98 // search for '#'-starting comment, discard if found
99 1904 size_t const comment_pos = entry.find('#');
100
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1904 times.
1904 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 1904 times.
1904 if (entry.length() == 0) {
108 continue;
109 }
110
111
1/2
✓ Branch 1 taken 1904 times.
✗ Branch 2 not taken.
1904 string_entries.push_back(entry);
112
1/2
✓ Branch 1 taken 1904 times.
✗ Branch 2 not taken.
1904 }
113 616 return 0;
114 }
115
116 1736 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 1736 *perms = 0;
123
2/2
✓ Branch 5 taken 3136 times.
✓ Branch 6 taken 1736 times.
4872 for (const char &c : str) {
124
4/5
✓ Branch 0 taken 1148 times.
✓ Branch 1 taken 700 times.
✓ Branch 2 taken 336 times.
✓ Branch 3 taken 952 times.
✗ Branch 4 not taken.
3136 switch (c) {
125 1148 case 'r':
126 1148 *perms |= ACL_READ;
127 1148 break;
128 700 case 'w':
129 700 *perms |= ACL_WRITE;
130 700 break;
131 336 case 'x':
132 336 *perms |= ACL_EXECUTE;
133 336 break;
134 952 case '-':
135 952 break;
136 default:
137 return EINVAL;
138 }
139 }
140 1736 return 0;
141 }
142
143 1764 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 1764 size_t sep_pos = str.find(':');
148
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1764 times.
1764 if (sep_pos == string::npos) {
149 return EINVAL;
150 }
151
1/2
✓ Branch 1 taken 1764 times.
✗ Branch 2 not taken.
1764 string const type(str, 0, sep_pos);
152 1764 size_t next_field_pos = sep_pos + 1;
153 1764 sep_pos = str.find(':', next_field_pos);
154
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1764 times.
1764 if (sep_pos == string::npos) {
155 return EINVAL;
156 }
157
1/2
✓ Branch 1 taken 1764 times.
✗ Branch 2 not taken.
1764 string const qualifier(str, next_field_pos, sep_pos - next_field_pos);
158 1764 next_field_pos = sep_pos + 1;
159
1/2
✓ Branch 2 taken 1764 times.
✗ Branch 3 not taken.
1764 string const permissions(str, next_field_pos);
160
161
6/6
✓ Branch 1 taken 1736 times.
✓ Branch 2 taken 28 times.
✓ Branch 4 taken 672 times.
✓ Branch 5 taken 1064 times.
✓ Branch 6 taken 700 times.
✓ Branch 7 taken 1064 times.
1764 if (!type.compare("user") || !type.compare("u")) {
162
2/2
✓ Branch 1 taken 420 times.
✓ Branch 2 taken 280 times.
700 entry.e_tag = qualifier.empty() ? ACL_USER_OBJ : ACL_USER;
163
6/6
✓ Branch 1 taken 980 times.
✓ Branch 2 taken 84 times.
✓ Branch 4 taken 420 times.
✓ Branch 5 taken 560 times.
✓ Branch 6 taken 504 times.
✓ Branch 7 taken 560 times.
1064 } else if (!type.compare("group") || !type.compare("g")) {
164
2/2
✓ Branch 1 taken 392 times.
✓ Branch 2 taken 112 times.
504 entry.e_tag = qualifier.empty() ? ACL_GROUP_OBJ : ACL_GROUP;
165
6/6
✓ Branch 1 taken 532 times.
✓ Branch 2 taken 28 times.
✓ Branch 4 taken 364 times.
✓ Branch 5 taken 168 times.
✓ Branch 6 taken 392 times.
✓ Branch 7 taken 168 times.
560 } else if (!type.compare("other") || !type.compare("o")) {
166 392 entry.e_tag = ACL_OTHER;
167
6/6
✓ Branch 1 taken 84 times.
✓ Branch 2 taken 84 times.
✓ Branch 4 taken 56 times.
✓ Branch 5 taken 28 times.
✓ Branch 6 taken 140 times.
✓ Branch 7 taken 28 times.
168 } else if (!type.compare("mask") || !type.compare("m")) {
168 140 entry.e_tag = ACL_MASK;
169 } else {
170 28 return EINVAL;
171 }
172 1736 entry.e_tag = htole16(entry.e_tag);
173
174
2/2
✓ Branch 1 taken 1344 times.
✓ Branch 2 taken 392 times.
1736 if (qualifier.empty()) {
175 1344 entry.e_id = ACL_UNDEFINED_ID;
176 } else {
177 char *at_null_terminator_if_number;
178 392 long number = strtol(qualifier.c_str(), &at_null_terminator_if_number, 10);
179
2/2
✓ Branch 0 taken 140 times.
✓ Branch 1 taken 252 times.
392 if (*at_null_terminator_if_number != '\0') {
180 bool ok;
181
1/2
✓ Branch 1 taken 140 times.
✗ Branch 2 not taken.
140 if (entry.e_tag == htole16(ACL_USER)) {
182 [[maybe_unused]] gid_t main_gid;
183 uid_t uid;
184
1/2
✓ Branch 1 taken 140 times.
✗ Branch 2 not taken.
140 ok = GetUidOf(qualifier, &uid, &main_gid);
185 140 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 140 times.
140 if (!ok) {
194 return EINVAL;
195 }
196 }
197 392 entry.e_id = htole32(number);
198 }
199
200 // parse perms
201 u_int16_t host_byteorder_perms;
202 int ret;
203 1736 ret = acl_parms_from_text(permissions, &host_byteorder_perms);
204
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1736 times.
1736 if (ret) {
205 return ret;
206 }
207 1736 entry.e_perm = htole16(host_byteorder_perms);
208
209 1736 return 0;
210 1764 }
211
212 588 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 588 bool types_met[ACL_OTHER + 1] = {
226 false,
227 };
228
229
2/2
✓ Branch 4 taken 1624 times.
✓ Branch 5 taken 560 times.
2184 for (auto entry_it = entries.begin(); entry_it != entries.end(); ++entry_it) {
230 1624 const acl_ea_entry &e = *entry_it;
231
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1624 times.
1624 assert(e.e_tag <= ACL_OTHER);
232 1624 bool &type_met = types_met[e.e_tag];
233
2/3
✓ Branch 0 taken 1288 times.
✓ Branch 1 taken 336 times.
✗ Branch 2 not taken.
1624 switch (e.e_tag) {
234 // at most one of these types
235 1288 case ACL_USER_OBJ:
236 case ACL_GROUP_OBJ:
237 case ACL_OTHER:
238 case ACL_MASK:
239
2/2
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 1260 times.
1288 if (type_met) {
240 28 return false;
241 } else {
242 1260 type_met = true;
243 }
244 1260 break;
245 336 case ACL_USER:
246 case ACL_GROUP:
247 336 type_met = true;
248 336 break;
249 default:
250 assert(false);
251 }
252 }
253
4/4
✓ Branch 0 taken 364 times.
✓ Branch 1 taken 196 times.
✓ Branch 2 taken 336 times.
✓ Branch 3 taken 28 times.
560 if (!(types_met[ACL_USER_OBJ] && types_met[ACL_GROUP_OBJ]
254
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 336 times.
336 && types_met[ACL_OTHER])) {
255 224 return false;
256 }
257
6/6
✓ Branch 0 taken 196 times.
✓ Branch 1 taken 140 times.
✓ Branch 2 taken 28 times.
✓ Branch 3 taken 168 times.
✓ Branch 4 taken 28 times.
✓ Branch 5 taken 140 times.
336 if ((types_met[ACL_USER] || types_met[ACL_GROUP]) && !types_met[ACL_MASK]) {
258 28 return false;
259 }
260 // TODO(autkin): ACL_USER, ACL_GROUP uniqueness checks. Not a pressing issue.
261 308 return true;
262 }
263
264 616 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 616 o_equiv_mode = true;
269
270 // get individual textual entries from one big text
271 616 vector<string> string_entries;
272
1/2
✓ Branch 1 taken 616 times.
✗ Branch 2 not taken.
616 ret = acl_from_text_to_string_entries(textual_acl, string_entries);
273
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 616 times.
616 if (ret) {
274 return ret;
275 }
276
277 // get individual entries in structural form
278 616 vector<acl_ea_entry> entries;
279 616 for (auto string_it = string_entries.begin();
280
2/2
✓ Branch 2 taken 1764 times.
✓ Branch 3 taken 588 times.
2352 string_it != string_entries.end();
281 1736 ++string_it) {
282 acl_ea_entry entry;
283
1/2
✓ Branch 2 taken 1764 times.
✗ Branch 3 not taken.
1764 ret = acl_entry_from_text(*string_it, entry);
284
2/2
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 1736 times.
1764 if (ret) {
285 28 return ret;
286 }
287
4/4
✓ Branch 0 taken 1624 times.
✓ Branch 1 taken 112 times.
✓ Branch 2 taken 280 times.
✓ Branch 3 taken 1344 times.
1736 if (entry.e_tag == ACL_GROUP || entry.e_tag == ACL_USER) {
288 392 o_equiv_mode = false;
289 }
290
1/2
✓ Branch 1 taken 1736 times.
✗ Branch 2 not taken.
1736 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 588 times.
✗ Branch 4 not taken.
588 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 280 times.
✓ Branch 2 taken 308 times.
588 if (!acl_valid_builtin(entries)) {
298 280 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 168 times.
✓ Branch 1 taken 140 times.
308 if (o_equiv_mode) {
303 168 o_binary_acl = NULL;
304 168 o_size = 0;
305 168 return 0;
306 }
307
308 // get one big buffer with all the entries in the "on-disk" xattr format
309 140 size_t const acl_entry_count = entries.size();
310 140 size_t const buf_size = sizeof(acl_ea_header)
311 140 + (acl_entry_count * sizeof(acl_ea_entry));
312 140 char *buf = static_cast<char *>(malloc(buf_size));
313
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 140 times.
140 if (!buf) {
314 return ENOMEM;
315 }
316 140 acl_ea_header *header = reinterpret_cast<acl_ea_header *>(buf);
317 140 header->a_version = htole32(ACL_EA_VERSION);
318 140 acl_ea_entry *ext_entry = reinterpret_cast<acl_ea_entry *>(header + 1);
319
2/2
✓ Branch 3 taken 812 times.
✓ Branch 4 taken 140 times.
952 for (auto entry_it = entries.begin(); entry_it != entries.end(); ++entry_it) {
320 812 *ext_entry = *entry_it;
321 812 ext_entry += 1;
322 }
323
324 140 o_binary_acl = buf;
325 140 o_size = buf_size;
326 140 return 0;
327 616 }
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