1 | /* Copyright (C) 1998-2021 Free Software Foundation, Inc. |
2 | This file is part of the GNU C Library. |
3 | Contributed by Thorsten Kukuk <kukuk@suse.de>, 1998. |
4 | |
5 | The GNU C Library is free software; you can redistribute it and/or |
6 | modify it under the terms of the GNU Lesser General Public |
7 | License as published by the Free Software Foundation; either |
8 | version 2.1 of the License, or (at your option) any later version. |
9 | |
10 | The GNU C Library is distributed in the hope that it will be useful, |
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
13 | Lesser General Public License for more details. |
14 | |
15 | You should have received a copy of the GNU Lesser General Public |
16 | License along with the GNU C Library; if not, see |
17 | <https://www.gnu.org/licenses/>. */ |
18 | |
19 | #include <ctype.h> |
20 | #include <errno.h> |
21 | #include <fcntl.h> |
22 | #include <grp.h> |
23 | #include <nss.h> |
24 | #include <stdio_ext.h> |
25 | #include <string.h> |
26 | #include <unistd.h> |
27 | #include <sys/param.h> |
28 | #include <nsswitch.h> |
29 | #include <libc-lock.h> |
30 | #include <kernel-features.h> |
31 | #include <scratch_buffer.h> |
32 | #include <nss_files.h> |
33 | |
34 | NSS_DECLARE_MODULE_FUNCTIONS (compat) |
35 | |
36 | static nss_action_list ni; |
37 | static enum nss_status (*initgroups_dyn_impl) (const char *, gid_t, |
38 | long int *, long int *, |
39 | gid_t **, long int, int *); |
40 | static enum nss_status (*getgrnam_r_impl) (const char *name, |
41 | struct group * grp, char *buffer, |
42 | size_t buflen, int *errnop); |
43 | static enum nss_status (*getgrgid_r_impl) (gid_t gid, struct group * grp, |
44 | char *buffer, size_t buflen, |
45 | int *errnop); |
46 | static enum nss_status (*setgrent_impl) (int stayopen); |
47 | static enum nss_status (*getgrent_r_impl) (struct group * grp, char *buffer, |
48 | size_t buflen, int *errnop); |
49 | static enum nss_status (*endgrent_impl) (void); |
50 | |
51 | /* Protect global state against multiple changers. */ |
52 | __libc_lock_define_initialized (static, lock) |
53 | |
54 | |
55 | /* Get the declaration of the parser function. */ |
56 | #define ENTNAME grent |
57 | #define STRUCTURE group |
58 | #define EXTERN_PARSER |
59 | #include <nss/nss_files/files-parse.c> |
60 | |
61 | /* Structure for remembering -group members ... */ |
62 | #define BLACKLIST_INITIAL_SIZE 512 |
63 | #define BLACKLIST_INCREMENT 256 |
64 | struct blacklist_t |
65 | { |
66 | char *data; |
67 | int current; |
68 | int size; |
69 | }; |
70 | |
71 | struct ent_t |
72 | { |
73 | bool files; |
74 | bool need_endgrent; |
75 | bool skip_initgroups_dyn; |
76 | FILE *stream; |
77 | struct blacklist_t blacklist; |
78 | }; |
79 | typedef struct ent_t ent_t; |
80 | |
81 | /* Prototypes for local functions. */ |
82 | static void blacklist_store_name (const char *, ent_t *); |
83 | static bool in_blacklist (const char *, int, ent_t *); |
84 | |
85 | /* Initialize the NSS interface/functions. The calling function must |
86 | hold the lock. */ |
87 | static void |
88 | init_nss_interface (void) |
89 | { |
90 | __libc_lock_lock (lock); |
91 | |
92 | /* Retest. */ |
93 | if (ni == NULL |
94 | && __nss_database_lookup2 ("group_compat" , NULL, "nis" , &ni) >= 0) |
95 | { |
96 | initgroups_dyn_impl = __nss_lookup_function (ni, "initgroups_dyn" ); |
97 | getgrnam_r_impl = __nss_lookup_function (ni, "getgrnam_r" ); |
98 | getgrgid_r_impl = __nss_lookup_function (ni, "getgrgid_r" ); |
99 | setgrent_impl = __nss_lookup_function (ni, "setgrent" ); |
100 | getgrent_r_impl = __nss_lookup_function (ni, "getgrent_r" ); |
101 | endgrent_impl = __nss_lookup_function (ni, "endgrent" ); |
102 | } |
103 | |
104 | __libc_lock_unlock (lock); |
105 | } |
106 | |
107 | static enum nss_status |
108 | internal_setgrent (ent_t *ent) |
109 | { |
110 | enum nss_status status = NSS_STATUS_SUCCESS; |
111 | |
112 | ent->files = true; |
113 | |
114 | if (ni == NULL) |
115 | init_nss_interface (); |
116 | |
117 | if (ent->blacklist.data != NULL) |
118 | { |
119 | ent->blacklist.current = 1; |
120 | ent->blacklist.data[0] = '|'; |
121 | ent->blacklist.data[1] = '\0'; |
122 | } |
123 | else |
124 | ent->blacklist.current = 0; |
125 | |
126 | ent->stream = __nss_files_fopen ("/etc/group" ); |
127 | |
128 | if (ent->stream == NULL) |
129 | status = errno == EAGAIN ? NSS_STATUS_TRYAGAIN : NSS_STATUS_UNAVAIL; |
130 | |
131 | return status; |
132 | } |
133 | |
134 | |
135 | static enum nss_status __attribute_warn_unused_result__ |
136 | internal_endgrent (ent_t *ent) |
137 | { |
138 | if (ent->stream != NULL) |
139 | { |
140 | fclose (ent->stream); |
141 | ent->stream = NULL; |
142 | } |
143 | |
144 | if (ent->blacklist.data != NULL) |
145 | { |
146 | ent->blacklist.current = 1; |
147 | ent->blacklist.data[0] = '|'; |
148 | ent->blacklist.data[1] = '\0'; |
149 | } |
150 | else |
151 | ent->blacklist.current = 0; |
152 | |
153 | if (ent->need_endgrent && endgrent_impl != NULL) |
154 | endgrent_impl (); |
155 | |
156 | return NSS_STATUS_SUCCESS; |
157 | } |
158 | |
159 | /* Like internal_endgrent, but preserve errno in all cases. */ |
160 | static void |
161 | internal_endgrent_noerror (ent_t *ent) |
162 | { |
163 | int saved_errno = errno; |
164 | enum nss_status unused __attribute__ ((unused)) = internal_endgrent (ent); |
165 | __set_errno (saved_errno); |
166 | } |
167 | |
168 | /* Add new group record. */ |
169 | static void |
170 | add_group (long int *start, long int *size, gid_t **groupsp, long int limit, |
171 | gid_t gid) |
172 | { |
173 | gid_t *groups = *groupsp; |
174 | |
175 | /* Matches user. Insert this group. */ |
176 | if (__glibc_unlikely (*start == *size)) |
177 | { |
178 | /* Need a bigger buffer. */ |
179 | gid_t *newgroups; |
180 | long int newsize; |
181 | |
182 | if (limit > 0 && *size == limit) |
183 | /* We reached the maximum. */ |
184 | return; |
185 | |
186 | if (limit <= 0) |
187 | newsize = 2 * *size; |
188 | else |
189 | newsize = MIN (limit, 2 * *size); |
190 | |
191 | newgroups = realloc (groups, newsize * sizeof (*groups)); |
192 | if (newgroups == NULL) |
193 | return; |
194 | *groupsp = groups = newgroups; |
195 | *size = newsize; |
196 | } |
197 | |
198 | groups[*start] = gid; |
199 | *start += 1; |
200 | } |
201 | |
202 | /* This function checks, if the user is a member of this group and if |
203 | yes, add the group id to the list. Return nonzero is we couldn't |
204 | handle the group because the user is not in the member list. */ |
205 | static int |
206 | check_and_add_group (const char *user, gid_t group, long int *start, |
207 | long int *size, gid_t **groupsp, long int limit, |
208 | struct group *grp) |
209 | { |
210 | char **member; |
211 | |
212 | /* Don't add main group to list of groups. */ |
213 | if (grp->gr_gid == group) |
214 | return 0; |
215 | |
216 | for (member = grp->gr_mem; *member != NULL; ++member) |
217 | if (strcmp (*member, user) == 0) |
218 | { |
219 | add_group (start, size, groupsp, limit, grp->gr_gid); |
220 | return 0; |
221 | } |
222 | |
223 | return 1; |
224 | } |
225 | |
226 | /* Get the next group from NSS (+ entry). If the NSS module supports |
227 | initgroups_dyn, get all entries at once. */ |
228 | static enum nss_status |
229 | getgrent_next_nss (ent_t *ent, char *buffer, size_t buflen, const char *user, |
230 | gid_t group, long int *start, long int *size, |
231 | gid_t **groupsp, long int limit, int *errnop) |
232 | { |
233 | enum nss_status status; |
234 | struct group grpbuf; |
235 | |
236 | /* Try nss_initgroups_dyn if supported. We also need getgrgid_r. |
237 | If this function is not supported, step through the whole group |
238 | database with getgrent_r. */ |
239 | if (! ent->skip_initgroups_dyn) |
240 | { |
241 | long int mystart = 0; |
242 | long int mysize = limit <= 0 ? *size : limit; |
243 | gid_t *mygroups = malloc (mysize * sizeof (gid_t)); |
244 | |
245 | if (mygroups == NULL) |
246 | return NSS_STATUS_TRYAGAIN; |
247 | |
248 | /* For every gid in the list we get from the NSS module, |
249 | get the whole group entry. We need to do this, since we |
250 | need the group name to check if it is in the blacklist. |
251 | In worst case, this is as twice as slow as stepping with |
252 | getgrent_r through the whole group database. But for large |
253 | group databases this is faster, since the user can only be |
254 | in a limited number of groups. */ |
255 | if (initgroups_dyn_impl (user, group, &mystart, &mysize, &mygroups, |
256 | limit, errnop) == NSS_STATUS_SUCCESS) |
257 | { |
258 | status = NSS_STATUS_NOTFOUND; |
259 | |
260 | /* If there is no blacklist we can trust the underlying |
261 | initgroups implementation. */ |
262 | if (ent->blacklist.current <= 1) |
263 | for (int i = 0; i < mystart; i++) |
264 | add_group (start, size, groupsp, limit, mygroups[i]); |
265 | else |
266 | { |
267 | /* A temporary buffer. We use the normal buffer, until we find |
268 | an entry, for which this buffer is to small. In this case, we |
269 | overwrite the pointer with one to a bigger buffer. */ |
270 | char *tmpbuf = buffer; |
271 | size_t tmplen = buflen; |
272 | |
273 | for (int i = 0; i < mystart; i++) |
274 | { |
275 | while ((status = getgrgid_r_impl (mygroups[i], &grpbuf, |
276 | tmpbuf, tmplen, errnop)) |
277 | == NSS_STATUS_TRYAGAIN |
278 | && *errnop == ERANGE) |
279 | { |
280 | /* Check for overflow. */ |
281 | if (__glibc_unlikely (tmplen * 2 < tmplen)) |
282 | { |
283 | __set_errno (ENOMEM); |
284 | status = NSS_STATUS_TRYAGAIN; |
285 | goto done; |
286 | } |
287 | /* Increase the size. Make sure that we retry |
288 | with a reasonable size. */ |
289 | tmplen *= 2; |
290 | if (tmplen < 1024) |
291 | tmplen = 1024; |
292 | if (tmpbuf != buffer) |
293 | free (tmpbuf); |
294 | tmpbuf = malloc (tmplen); |
295 | if (__glibc_unlikely (tmpbuf == NULL)) |
296 | { |
297 | status = NSS_STATUS_TRYAGAIN; |
298 | goto done; |
299 | } |
300 | } |
301 | |
302 | if (__builtin_expect (status != NSS_STATUS_NOTFOUND, 1)) |
303 | { |
304 | if (__builtin_expect (status != NSS_STATUS_SUCCESS, 0)) |
305 | goto done; |
306 | |
307 | if (!in_blacklist (grpbuf.gr_name, |
308 | strlen (grpbuf.gr_name), ent) |
309 | && check_and_add_group (user, group, start, size, |
310 | groupsp, limit, &grpbuf)) |
311 | { |
312 | if (setgrent_impl != NULL) |
313 | { |
314 | setgrent_impl (1); |
315 | ent->need_endgrent = true; |
316 | } |
317 | ent->skip_initgroups_dyn = true; |
318 | |
319 | goto iter; |
320 | } |
321 | } |
322 | } |
323 | |
324 | status = NSS_STATUS_NOTFOUND; |
325 | |
326 | done: |
327 | if (tmpbuf != buffer) |
328 | free (tmpbuf); |
329 | } |
330 | |
331 | free (mygroups); |
332 | |
333 | return status; |
334 | } |
335 | |
336 | free (mygroups); |
337 | } |
338 | |
339 | /* If we come here, the NSS module does not support initgroups_dyn |
340 | or we were confronted with a split group. In these cases we have |
341 | to step through the whole list ourself. */ |
342 | iter: |
343 | do |
344 | { |
345 | if ((status = getgrent_r_impl (&grpbuf, buffer, buflen, errnop)) |
346 | != NSS_STATUS_SUCCESS) |
347 | break; |
348 | } |
349 | while (in_blacklist (grpbuf.gr_name, strlen (grpbuf.gr_name), ent)); |
350 | |
351 | if (status == NSS_STATUS_SUCCESS) |
352 | check_and_add_group (user, group, start, size, groupsp, limit, &grpbuf); |
353 | |
354 | return status; |
355 | } |
356 | |
357 | static enum nss_status |
358 | internal_getgrent_r (ent_t *ent, char *buffer, size_t buflen, const char *user, |
359 | gid_t group, long int *start, long int *size, |
360 | gid_t **groupsp, long int limit, int *errnop) |
361 | { |
362 | struct parser_data *data = (void *) buffer; |
363 | struct group grpbuf; |
364 | |
365 | if (!ent->files) |
366 | return getgrent_next_nss (ent, buffer, buflen, user, group, |
367 | start, size, groupsp, limit, errnop); |
368 | |
369 | while (1) |
370 | { |
371 | fpos_t pos; |
372 | int parse_res = 0; |
373 | char *p; |
374 | |
375 | do |
376 | { |
377 | /* We need at least 3 characters for one line. */ |
378 | if (__glibc_unlikely (buflen < 3)) |
379 | { |
380 | erange: |
381 | *errnop = ERANGE; |
382 | return NSS_STATUS_TRYAGAIN; |
383 | } |
384 | |
385 | fgetpos (ent->stream, &pos); |
386 | buffer[buflen - 1] = '\xff'; |
387 | p = fgets_unlocked (buffer, buflen, ent->stream); |
388 | if (p == NULL && feof_unlocked (ent->stream)) |
389 | return NSS_STATUS_NOTFOUND; |
390 | |
391 | if (p == NULL || __builtin_expect (buffer[buflen - 1] != '\xff', 0)) |
392 | { |
393 | erange_reset: |
394 | fsetpos (ent->stream, &pos); |
395 | goto erange; |
396 | } |
397 | |
398 | /* Terminate the line for any case. */ |
399 | buffer[buflen - 1] = '\0'; |
400 | |
401 | /* Skip leading blanks. */ |
402 | while (isspace (*p)) |
403 | ++p; |
404 | } |
405 | while (*p == '\0' || *p == '#' /* Ignore empty and comment lines. */ |
406 | /* Parse the line. If it is invalid, loop to |
407 | get the next line of the file to parse. */ |
408 | || !(parse_res = _nss_files_parse_grent (p, &grpbuf, data, buflen, |
409 | errnop))); |
410 | |
411 | if (__glibc_unlikely (parse_res == -1)) |
412 | /* The parser ran out of space. */ |
413 | goto erange_reset; |
414 | |
415 | if (grpbuf.gr_name[0] != '+' && grpbuf.gr_name[0] != '-') |
416 | /* This is a real entry. */ |
417 | break; |
418 | |
419 | /* -group */ |
420 | if (grpbuf.gr_name[0] == '-' && grpbuf.gr_name[1] != '\0' |
421 | && grpbuf.gr_name[1] != '@') |
422 | { |
423 | blacklist_store_name (&grpbuf.gr_name[1], ent); |
424 | continue; |
425 | } |
426 | |
427 | /* +group */ |
428 | if (grpbuf.gr_name[0] == '+' && grpbuf.gr_name[1] != '\0' |
429 | && grpbuf.gr_name[1] != '@') |
430 | { |
431 | if (in_blacklist (&grpbuf.gr_name[1], |
432 | strlen (&grpbuf.gr_name[1]), ent)) |
433 | continue; |
434 | /* Store the group in the blacklist for the "+" at the end of |
435 | /etc/group */ |
436 | blacklist_store_name (&grpbuf.gr_name[1], ent); |
437 | if (getgrnam_r_impl == NULL) |
438 | return NSS_STATUS_UNAVAIL; |
439 | else if (getgrnam_r_impl (&grpbuf.gr_name[1], &grpbuf, buffer, |
440 | buflen, errnop) != NSS_STATUS_SUCCESS) |
441 | continue; |
442 | |
443 | check_and_add_group (user, group, start, size, groupsp, |
444 | limit, &grpbuf); |
445 | |
446 | return NSS_STATUS_SUCCESS; |
447 | } |
448 | |
449 | /* +:... */ |
450 | if (grpbuf.gr_name[0] == '+' && grpbuf.gr_name[1] == '\0') |
451 | { |
452 | /* If the selected module does not support getgrent_r or |
453 | initgroups_dyn, abort. We cannot find the needed group |
454 | entries. */ |
455 | if (initgroups_dyn_impl == NULL || getgrgid_r_impl == NULL) |
456 | { |
457 | if (setgrent_impl != NULL) |
458 | { |
459 | setgrent_impl (1); |
460 | ent->need_endgrent = true; |
461 | } |
462 | ent->skip_initgroups_dyn = true; |
463 | |
464 | if (getgrent_r_impl == NULL) |
465 | return NSS_STATUS_UNAVAIL; |
466 | } |
467 | |
468 | ent->files = false; |
469 | |
470 | return getgrent_next_nss (ent, buffer, buflen, user, group, |
471 | start, size, groupsp, limit, errnop); |
472 | } |
473 | } |
474 | |
475 | check_and_add_group (user, group, start, size, groupsp, limit, &grpbuf); |
476 | |
477 | return NSS_STATUS_SUCCESS; |
478 | } |
479 | |
480 | |
481 | enum nss_status |
482 | _nss_compat_initgroups_dyn (const char *user, gid_t group, long int *start, |
483 | long int *size, gid_t **groupsp, long int limit, |
484 | int *errnop) |
485 | { |
486 | enum nss_status status; |
487 | ent_t intern = { true, false, false, NULL, {NULL, 0, 0} }; |
488 | |
489 | status = internal_setgrent (&intern); |
490 | if (status != NSS_STATUS_SUCCESS) |
491 | return status; |
492 | |
493 | struct scratch_buffer tmpbuf; |
494 | scratch_buffer_init (&tmpbuf); |
495 | |
496 | do |
497 | { |
498 | while ((status = internal_getgrent_r (&intern, tmpbuf.data, tmpbuf.length, |
499 | user, group, start, size, |
500 | groupsp, limit, errnop)) |
501 | == NSS_STATUS_TRYAGAIN && *errnop == ERANGE) |
502 | if (!scratch_buffer_grow (&tmpbuf)) |
503 | goto done; |
504 | } |
505 | while (status == NSS_STATUS_SUCCESS); |
506 | |
507 | status = NSS_STATUS_SUCCESS; |
508 | |
509 | done: |
510 | scratch_buffer_free (&tmpbuf); |
511 | |
512 | internal_endgrent_noerror (&intern); |
513 | |
514 | return status; |
515 | } |
516 | |
517 | |
518 | /* Support routines for remembering -@netgroup and -user entries. |
519 | The names are stored in a single string with `|' as separator. */ |
520 | static void |
521 | blacklist_store_name (const char *name, ent_t *ent) |
522 | { |
523 | int namelen = strlen (name); |
524 | char *tmp; |
525 | |
526 | /* First call, setup cache. */ |
527 | if (ent->blacklist.size == 0) |
528 | { |
529 | ent->blacklist.size = MAX (BLACKLIST_INITIAL_SIZE, 2 * namelen); |
530 | ent->blacklist.data = malloc (ent->blacklist.size); |
531 | if (ent->blacklist.data == NULL) |
532 | return; |
533 | ent->blacklist.data[0] = '|'; |
534 | ent->blacklist.data[1] = '\0'; |
535 | ent->blacklist.current = 1; |
536 | } |
537 | else |
538 | { |
539 | if (in_blacklist (name, namelen, ent)) |
540 | return; /* no duplicates */ |
541 | |
542 | if (ent->blacklist.current + namelen + 1 >= ent->blacklist.size) |
543 | { |
544 | ent->blacklist.size += MAX (BLACKLIST_INCREMENT, 2 * namelen); |
545 | tmp = realloc (ent->blacklist.data, ent->blacklist.size); |
546 | if (tmp == NULL) |
547 | { |
548 | free (ent->blacklist.data); |
549 | ent->blacklist.size = 0; |
550 | return; |
551 | } |
552 | ent->blacklist.data = tmp; |
553 | } |
554 | } |
555 | |
556 | tmp = stpcpy (ent->blacklist.data + ent->blacklist.current, name); |
557 | *tmp++ = '|'; |
558 | *tmp = '\0'; |
559 | ent->blacklist.current += namelen + 1; |
560 | |
561 | return; |
562 | } |
563 | |
564 | /* Return whether ent->blacklist contains name. */ |
565 | static bool |
566 | in_blacklist (const char *name, int namelen, ent_t *ent) |
567 | { |
568 | char buf[namelen + 3]; |
569 | char *cp; |
570 | |
571 | if (ent->blacklist.data == NULL) |
572 | return false; |
573 | |
574 | buf[0] = '|'; |
575 | cp = stpcpy (&buf[1], name); |
576 | *cp++ = '|'; |
577 | *cp = '\0'; |
578 | return strstr (ent->blacklist.data, buf) != NULL; |
579 | } |
580 | |