]>
git.ipfire.org Git - thirdparty/systemd.git/blob - src/core/dynamic-user.c
1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
8 #include "dynamic-user.h"
11 #include "format-util.h"
13 #include "iovec-util.h"
14 #include "lock-util.h"
15 #include "nscd-flush.h"
16 #include "parse-util.h"
17 #include "random-util.h"
18 #include "serialize.h"
19 #include "socket-util.h"
20 #include "stdio-util.h"
21 #include "string-util.h"
23 #include "uid-classification.h"
24 #include "user-util.h"
26 /* Takes a value generated randomly or by hashing and turns it into a UID in the right range */
27 #define UID_CLAMP_INTO_RANGE(rnd) (((uid_t) (rnd) % (DYNAMIC_UID_MAX - DYNAMIC_UID_MIN + 1)) + DYNAMIC_UID_MIN)
29 DEFINE_TRIVIAL_REF_FUNC(DynamicUser
, dynamic_user
);
31 DynamicUser
* dynamic_user_free(DynamicUser
*d
) {
36 (void) hashmap_remove(d
->manager
->dynamic_users
, d
->name
);
38 safe_close_pair(d
->storage_socket
);
42 static int dynamic_user_add(Manager
*m
, const char *name
, int storage_socket
[static 2], DynamicUser
**ret
) {
48 assert(storage_socket
);
50 if (m
) { /* Might be called in sd-executor with no manager object */
51 r
= hashmap_ensure_allocated(&m
->dynamic_users
, &string_hash_ops
);
56 d
= malloc0(offsetof(DynamicUser
, name
) + strlen(name
) + 1);
60 strcpy(d
->name
, name
);
62 d
->storage_socket
[0] = storage_socket
[0];
63 d
->storage_socket
[1] = storage_socket
[1];
65 if (m
) { /* Might be called in sd-executor with no manager object */
66 r
= hashmap_put(m
->dynamic_users
, d
->name
, d
);
81 static int dynamic_user_acquire(Manager
*m
, const char *name
, DynamicUser
** ret
) {
82 _cleanup_close_pair_
int storage_socket
[2] = EBADF_PAIR
;
89 /* Return the DynamicUser structure for a specific user name. Note that this won't actually allocate a UID for
90 * it, but just prepare the data structure for it. The UID is allocated only on demand, when it's really
91 * needed, and in the child process we fork off, since allocation involves NSS checks which are not OK to do
92 * from PID 1. To allow the children and PID 1 share information about allocated UIDs we use an anonymous
93 * AF_UNIX/SOCK_DGRAM socket (called the "storage socket") that contains at most one datagram with the
94 * allocated UID number, plus an fd referencing the lock file for the UID
95 * (i.e. /run/systemd/dynamic-uid/$UID). Why involve the socket pair? So that PID 1 and all its children can
96 * share the same storage for the UID and lock fd, simply by inheriting the storage socket fds. The socket pair
97 * may exist in three different states:
99 * a) no datagram stored. This is the initial state. In this case the dynamic user was never realized.
101 * b) a datagram containing a UID stored, but no lock fd attached to it. In this case there was already a
102 * statically assigned UID by the same name, which we are reusing.
104 * c) a datagram containing a UID stored, and a lock fd is attached to it. In this case we allocated a dynamic
105 * UID and locked it in the file system, using the lock fd.
107 * As PID 1 and various children might access the socket pair simultaneously, and pop the datagram or push it
108 * back in any time, we also maintain a lock on the socket pair. Note one peculiarity regarding locking here:
109 * the UID lock on disk is protected via a BSD file lock (i.e. an fd-bound lock), so that the lock is kept in
110 * place as long as there's a reference to the fd open. The lock on the storage socket pair however is a POSIX
111 * file lock (i.e. a process-bound lock), as all users share the same fd of this (after all it is anonymous,
112 * nobody else could get any access to it except via our own fd) and we want to synchronize access between all
113 * processes that have access to it. */
115 d
= hashmap_get(m
->dynamic_users
, name
);
118 /* We already have a structure for the dynamic user, let's increase the ref count and reuse it */
125 if (!valid_user_group_name(name
, VALID_USER_ALLOW_NUMERIC
))
128 if (socketpair(AF_UNIX
, SOCK_DGRAM
|SOCK_CLOEXEC
, 0, storage_socket
) < 0)
131 r
= dynamic_user_add(m
, name
, storage_socket
, &d
);
135 storage_socket
[0] = storage_socket
[1] = -EBADF
;
145 static int make_uid_symlinks(uid_t uid
, const char *name
, bool b
) {
147 char path1
[STRLEN("/run/systemd/dynamic-uid/direct:") + DECIMAL_STR_MAX(uid_t
) + 1];
151 /* Add direct additional symlinks for direct lookups of dynamic UIDs and their names by userspace code. The
152 * only reason we have this is because dbus-daemon cannot use D-Bus for resolving users and groups (since it
153 * would be its own client then). We hence keep these world-readable symlinks in place, so that the
154 * unprivileged dbus user can read the mappings when it needs them via these symlinks instead of having to go
155 * via the bus. Ideally, we'd use the lock files we keep for this anyway, but we can't since we use BSD locks
156 * on them and as those may be taken by any user with read access we can't make them world-readable. */
158 xsprintf(path1
, "/run/systemd/dynamic-uid/direct:" UID_FMT
, uid
);
159 if (unlink(path1
) < 0 && errno
!= ENOENT
)
162 if (b
&& symlink(name
, path1
) < 0) {
163 k
= log_warning_errno(errno
, "Failed to symlink \"%s\": %m", path1
);
168 path2
= strjoina("/run/systemd/dynamic-uid/direct:", name
);
169 if (unlink(path2
) < 0 && errno
!= ENOENT
) {
175 if (b
&& symlink(path1
+ STRLEN("/run/systemd/dynamic-uid/direct:"), path2
) < 0) {
176 k
= log_warning_errno(errno
, "Failed to symlink \"%s\": %m", path2
);
184 static int pick_uid(char **suggested_paths
, const char *name
, uid_t
*ret_uid
) {
186 /* Find a suitable free UID. We use the following strategy to find a suitable UID:
188 * 1. Initially, we try to read the UID of a number of specified paths. If any of these UIDs works, we use
189 * them. We use in order to increase the chance of UID reuse, if StateDirectory=, CacheDirectory= or
190 * LogsDirectory= are used, as reusing the UID these directories are owned by saves us from having to
191 * recursively chown() them to new users.
193 * 2. If that didn't yield a currently unused UID, we hash the user name, and try to use that. This should be
194 * pretty good, as the use ris by default derived from the unit name, and hence the same service and same
195 * user should usually get the same UID as long as our hashing doesn't clash.
197 * 3. Finally, if that didn't work, we randomly pick UIDs, until we find one that is empty.
199 * Since the dynamic UID space is relatively small we'll stop trying after 100 iterations, giving up. */
202 PHASE_SUGGESTED
, /* the first phase, reusing directory ownership UIDs */
203 PHASE_HASHED
, /* the second phase, deriving a UID from the username by hashing */
204 PHASE_RANDOM
, /* the last phase, randomly picking UIDs */
205 } phase
= PHASE_SUGGESTED
;
207 static const uint8_t hash_key
[] = {
208 0x37, 0x53, 0x7e, 0x31, 0xcf, 0xce, 0x48, 0xf5,
209 0x8a, 0xbb, 0x39, 0x57, 0x8d, 0xd9, 0xec, 0x59
212 unsigned n_tries
= 100, current_suggested
= 0;
215 (void) mkdir("/run/systemd/dynamic-uid", 0755);
218 char lock_path
[STRLEN("/run/systemd/dynamic-uid/") + DECIMAL_STR_MAX(uid_t
) + 1];
219 _cleanup_close_
int lock_fd
= -EBADF
;
223 if (--n_tries
<= 0) /* Give up retrying eventually */
228 case PHASE_SUGGESTED
: {
231 if (!suggested_paths
|| !suggested_paths
[current_suggested
]) {
232 /* We reached the end of the suggested paths list, let's try by hashing the name */
233 phase
= PHASE_HASHED
;
237 if (stat(suggested_paths
[current_suggested
++], &st
) < 0)
238 continue; /* We can't read the UID of this path, but that doesn't matter, just try the next */
240 candidate
= st
.st_uid
;
245 /* A static user by this name does not exist yet. Let's find a free ID then, and use that. We
246 * start with a UID generated as hash from the user name. */
247 candidate
= UID_CLAMP_INTO_RANGE(siphash24(name
, strlen(name
), hash_key
));
249 /* If this one fails, we should proceed with random tries */
250 phase
= PHASE_RANDOM
;
255 /* Pick another random UID, and see if that works for us. */
256 random_bytes(&candidate
, sizeof(candidate
));
257 candidate
= UID_CLAMP_INTO_RANGE(candidate
);
261 assert_not_reached();
264 /* Make sure whatever we picked here actually is in the right range */
265 if (!uid_is_dynamic(candidate
))
268 xsprintf(lock_path
, "/run/systemd/dynamic-uid/" UID_FMT
, candidate
);
273 lock_fd
= open(lock_path
, O_CREAT
|O_RDWR
|O_NOFOLLOW
|O_CLOEXEC
|O_NOCTTY
, 0600);
277 r
= flock(lock_fd
, LOCK_EX
|LOCK_NB
); /* Try to get a BSD file lock on the UID lock file */
279 if (IN_SET(errno
, EBUSY
, EAGAIN
))
280 goto next
; /* already in use */
285 if (fstat(lock_fd
, &st
) < 0)
290 /* Oh, bummer, we got the lock, but the file was unlinked between the time we opened it and
291 * got the lock. Close it, and try again. */
292 lock_fd
= safe_close(lock_fd
);
295 /* Some superficial check whether this UID/GID might already be taken by some static user */
296 if (getpwuid_malloc(candidate
, /* ret= */ NULL
) >= 0 ||
297 getgrgid_malloc((gid_t
) candidate
, /* ret= */ NULL
) >= 0 ||
298 search_ipc(candidate
, (gid_t
) candidate
) != 0) {
299 (void) unlink(lock_path
);
303 /* Let's store the user name in the lock file, so that we can use it for looking up the username for a UID */
306 IOVEC_MAKE_STRING(name
),
307 IOVEC_MAKE((char[1]) { '\n' }, 1),
311 (void) unlink(lock_path
);
315 (void) ftruncate(lock_fd
, l
);
316 (void) make_uid_symlinks(candidate
, name
, true); /* also add direct lookup symlinks */
318 *ret_uid
= candidate
;
319 return TAKE_FD(lock_fd
);
326 static int dynamic_user_pop(DynamicUser
*d
, uid_t
*ret_uid
, int *ret_lock_fd
) {
327 uid_t uid
= UID_INVALID
;
328 struct iovec iov
= IOVEC_MAKE(&uid
, sizeof(uid
));
336 /* Read the UID and lock fd that is stored in the storage AF_UNIX socket. This should be called with
337 * the lock on the socket taken. */
339 k
= receive_one_fd_iov(d
->storage_socket
[0], &iov
, 1, MSG_DONTWAIT
, &lock_fd
);
344 *ret_lock_fd
= lock_fd
;
349 static int dynamic_user_push(DynamicUser
*d
, uid_t uid
, int lock_fd
) {
350 struct iovec iov
= IOVEC_MAKE(&uid
, sizeof(uid
));
354 /* Store the UID and lock_fd in the storage socket. This should be called with the socket pair lock taken. */
355 return send_one_fd_iov(d
->storage_socket
[1], lock_fd
, &iov
, 1, MSG_DONTWAIT
);
358 static void unlink_uid_lock(int lock_fd
, uid_t uid
, const char *name
) {
359 char lock_path
[STRLEN("/run/systemd/dynamic-uid/") + DECIMAL_STR_MAX(uid_t
) + 1];
364 xsprintf(lock_path
, "/run/systemd/dynamic-uid/" UID_FMT
, uid
);
365 (void) unlink(lock_path
);
367 (void) make_uid_symlinks(uid
, name
, false); /* remove direct lookup symlinks */
370 static int dynamic_user_realize(
372 char **suggested_dirs
,
373 uid_t
*ret_uid
, gid_t
*ret_gid
,
376 _cleanup_close_
int uid_lock_fd
= -EBADF
;
377 _cleanup_close_
int etc_passwd_lock_fd
= -EBADF
;
378 uid_t num
= UID_INVALID
; /* a uid if is_user, and a gid otherwise */
379 gid_t gid
= GID_INVALID
; /* a gid if is_user, ignored otherwise */
380 bool flush_cache
= false;
384 assert(is_user
== !!ret_uid
);
387 /* Acquire a UID for the user name. This will allocate a UID for the user name if the user doesn't exist
388 * yet. If it already exists its existing UID/GID will be reused. */
390 r
= posix_lock(d
->storage_socket
[0], LOCK_EX
);
394 CLEANUP_POSIX_UNLOCK(d
->storage_socket
[0]);
396 r
= dynamic_user_pop(d
, &num
, &uid_lock_fd
);
404 /* OK, nothing stored yet, let's try to find something useful. While we are working on this release the
405 * lock however, so that nobody else blocks on our NSS lookups. */
406 r
= posix_lock(d
->storage_socket
[0], LOCK_UN
);
410 /* Let's see if a proper, static user or group by this name exists. Try to take the lock on
411 * /etc/passwd, if that fails with EROFS then /etc is read-only. In that case it's fine if we don't
412 * take the lock, given that users can't be added there anyway in this case. */
413 etc_passwd_lock_fd
= take_etc_passwd_lock(NULL
);
414 if (etc_passwd_lock_fd
< 0 && etc_passwd_lock_fd
!= -EROFS
)
415 return etc_passwd_lock_fd
;
417 /* First, let's parse this as numeric UID */
418 r
= parse_uid(d
->name
, &num
);
420 _cleanup_free_
struct passwd
*p
= NULL
;
421 _cleanup_free_
struct group
*g
= NULL
;
424 /* OK, this is not a numeric UID. Let's see if there's a user by this name */
425 if (getpwnam_malloc(d
->name
, &p
) >= 0) {
429 /* if the user does not exist but the group with the same name exists, refuse operation */
430 if (getgrnam_malloc(d
->name
, /* ret= */ NULL
) >= 0)
434 /* Let's see if there's a group by this name */
435 if (getgrnam_malloc(d
->name
, &g
) >= 0)
436 num
= (uid_t
) g
->gr_gid
;
438 /* if the group does not exist but the user with the same name exists, refuse operation */
439 if (getpwnam_malloc(d
->name
, /* ret= */ NULL
) >= 0)
445 if (num
== UID_INVALID
) {
446 /* No static UID assigned yet, excellent. Let's pick a new dynamic one, and lock it. */
448 uid_lock_fd
= pick_uid(suggested_dirs
, d
->name
, &num
);
453 /* So, we found a working UID/lock combination. Let's see if we actually still need it. */
454 r
= posix_lock(d
->storage_socket
[0], LOCK_EX
);
456 unlink_uid_lock(uid_lock_fd
, num
, d
->name
);
460 r
= dynamic_user_pop(d
, &new_uid
, &new_uid_lock_fd
);
463 /* OK, something bad happened, let's get rid of the bits we acquired. */
464 unlink_uid_lock(uid_lock_fd
, num
, d
->name
);
468 /* Great! Nothing is stored here, still. Store our newly acquired data. */
471 /* Hmm, so as it appears there's now something stored in the storage socket. Throw away what we
472 * acquired, and use what's stored now. */
474 unlink_uid_lock(uid_lock_fd
, num
, d
->name
);
475 safe_close(uid_lock_fd
);
478 uid_lock_fd
= new_uid_lock_fd
;
480 } else if (is_user
&& !uid_is_dynamic(num
)) {
481 _cleanup_free_
struct passwd
*p
= NULL
;
483 /* Statically allocated user may have different uid and gid. So, let's obtain the gid. */
484 r
= getpwuid_malloc(num
, &p
);
491 /* If the UID/GID was already allocated dynamically, push the data we popped out back in. If it was already
492 * allocated statically, push the UID back too, but do not push the lock fd in. If we allocated the UID
493 * dynamically right here, push that in along with the lock fd for it. */
494 r
= dynamic_user_push(d
, num
, uid_lock_fd
);
499 /* If we allocated a new dynamic UID, refresh nscd, so that it forgets about potentially cached
500 * negative entries. But let's do so after we release the /etc/passwd lock, so that there's no
501 * potential for nscd wanting to lock that for completing the invalidation. */
502 etc_passwd_lock_fd
= safe_close(etc_passwd_lock_fd
);
503 (void) nscd_flush_cache(STRV_MAKE("passwd", "group"));
508 *ret_gid
= gid
!= GID_INVALID
? gid
: num
;
515 int dynamic_user_current(DynamicUser
*d
, uid_t
*ret
) {
516 _cleanup_close_
int lock_fd
= -EBADF
;
522 /* Get the currently assigned UID for the user, if there's any. This simply pops the data from the
523 * storage socket, and pushes it back in right-away. */
525 r
= posix_lock(d
->storage_socket
[0], LOCK_EX
);
529 CLEANUP_POSIX_UNLOCK(d
->storage_socket
[0]);
531 r
= dynamic_user_pop(d
, &uid
, &lock_fd
);
535 r
= dynamic_user_push(d
, uid
, lock_fd
);
545 static DynamicUser
* dynamic_user_unref(DynamicUser
*d
) {
549 /* Note that this doesn't actually release any resources itself. If a dynamic user should be fully
550 * destroyed and its UID released, use dynamic_user_destroy() instead. NB: the dynamic user table may
551 * contain entries with no references, which is commonly the case right before a daemon reload. */
553 assert(d
->n_ref
> 0);
559 static int dynamic_user_close(DynamicUser
*d
) {
560 _cleanup_close_
int lock_fd
= -EBADF
;
564 /* Release the user ID, by releasing the lock on it, and emptying the storage socket. After this the
565 * user is unrealized again, much like it was after it the DynamicUser object was first allocated. */
567 r
= posix_lock(d
->storage_socket
[0], LOCK_EX
);
571 CLEANUP_POSIX_UNLOCK(d
->storage_socket
[0]);
573 r
= dynamic_user_pop(d
, &uid
, &lock_fd
);
575 /* User wasn't realized yet, nothing to do. */
580 /* This dynamic user was realized and dynamically allocated. In this case, let's remove the lock file. */
581 unlink_uid_lock(lock_fd
, uid
, d
->name
);
583 (void) nscd_flush_cache(STRV_MAKE("passwd", "group"));
587 static DynamicUser
* dynamic_user_destroy(DynamicUser
*d
) {
591 /* Drop a reference to a DynamicUser object, and destroy the user completely if this was the last
592 * reference. This is called whenever a service is shut down and wants its dynamic UID gone. Note that
593 * dynamic_user_unref() is what is called whenever a service is simply freed, for example during a reload
594 * cycle, where the dynamic users should not be destroyed, but our datastructures should. */
596 dynamic_user_unref(d
);
601 (void) dynamic_user_close(d
);
602 return dynamic_user_free(d
);
605 int dynamic_user_serialize_one(DynamicUser
*d
, const char *key
, FILE *f
, FDSet
*fds
) {
615 if (d
->storage_socket
[0] < 0 || d
->storage_socket
[1] < 0)
618 copy0
= fdset_put_dup(fds
, d
->storage_socket
[0]);
620 return log_error_errno(copy0
, "Failed to add dynamic user storage fd to serialization: %m");
622 copy1
= fdset_put_dup(fds
, d
->storage_socket
[1]);
624 return log_error_errno(copy1
, "Failed to add dynamic user storage fd to serialization: %m");
626 (void) serialize_item_format(f
, key
, "%s %i %i", d
->name
, copy0
, copy1
);
631 int dynamic_user_serialize(Manager
*m
, FILE *f
, FDSet
*fds
) {
636 /* Dump the dynamic user database into the manager serialization, to deal with daemon reloads. */
638 HASHMAP_FOREACH(d
, m
->dynamic_users
)
639 (void) dynamic_user_serialize_one(d
, "dynamic-user", f
, fds
);
644 void dynamic_user_deserialize_one(Manager
*m
, const char *value
, FDSet
*fds
, DynamicUser
**ret
) {
645 _cleanup_free_
char *name
= NULL
, *s0
= NULL
, *s1
= NULL
;
646 _cleanup_close_
int fd0
= -EBADF
, fd1
= -EBADF
;
652 /* Parse the serialization again, after a daemon reload */
654 r
= extract_many_words(&value
, NULL
, 0, &name
, &s0
, &s1
, NULL
);
655 if (r
!= 3 || !isempty(value
)) {
656 log_debug("Unable to parse dynamic user line.");
660 fd0
= deserialize_fd(fds
, s0
);
664 fd1
= deserialize_fd(fds
, s1
);
668 r
= dynamic_user_add(m
, name
, (int[]) { fd0
, fd1
}, ret
);
670 log_debug_errno(r
, "Failed to add dynamic user: %m");
677 if (ret
) /* If the caller uses it directly, increment the refcount */
681 void dynamic_user_vacuum(Manager
*m
, bool close_user
) {
686 /* Empty the dynamic user database, optionally cleaning up orphaned dynamic users, i.e. destroy and free users
687 * to which no reference exist. This is called after a daemon reload finished, in order to destroy users which
688 * might not be referenced anymore. */
690 HASHMAP_FOREACH(d
, m
->dynamic_users
) {
695 log_debug("Removing orphaned dynamic user %s", d
->name
);
696 (void) dynamic_user_close(d
);
699 dynamic_user_free(d
);
703 int dynamic_user_lookup_uid(Manager
*m
, uid_t uid
, char **ret
) {
704 char lock_path
[STRLEN("/run/systemd/dynamic-uid/") + DECIMAL_STR_MAX(uid_t
) + 1];
705 _cleanup_free_
char *user
= NULL
;
712 /* A friendly way to translate a dynamic user's UID into a name. */
713 if (!uid_is_dynamic(uid
))
716 xsprintf(lock_path
, "/run/systemd/dynamic-uid/" UID_FMT
, uid
);
717 r
= read_one_line_file(lock_path
, &user
);
718 if (IN_SET(r
, -ENOENT
, 0))
723 /* The lock file might be stale, hence let's verify the data before we return it */
724 r
= dynamic_user_lookup_name(m
, user
, &check_uid
);
727 if (check_uid
!= uid
) /* lock file doesn't match our own idea */
730 *ret
= TAKE_PTR(user
);
735 int dynamic_user_lookup_name(Manager
*m
, const char *name
, uid_t
*ret
) {
742 /* A friendly call for translating a dynamic user's name into its UID */
744 d
= hashmap_get(m
->dynamic_users
, name
);
748 r
= dynamic_user_current(d
, ret
);
749 if (r
== -EAGAIN
) /* not realized yet? */
755 int dynamic_creds_make(Manager
*m
, const char *user
, const char *group
, DynamicCreds
**ret
) {
756 _cleanup_(dynamic_creds_unrefp
) DynamicCreds
*creds
= NULL
;
757 bool acquired
= false;
763 if (!user
&& !group
) {
768 creds
= new0(DynamicCreds
, 1);
772 /* A DynamicUser object encapsulates an allocation of both a UID and a GID for a specific name. However, some
773 * services use different user and groups. For cases like that there's DynamicCreds containing a pair of user
774 * and group. This call allocates a pair. */
777 r
= dynamic_user_acquire(m
, user
, &creds
->user
);
784 if (creds
->user
&& (!group
|| streq_ptr(user
, group
)))
785 creds
->group
= dynamic_user_ref(creds
->user
);
787 r
= dynamic_user_acquire(m
, group
, &creds
->group
);
790 creds
->user
= dynamic_user_unref(creds
->user
);
795 *ret
= TAKE_PTR(creds
);
800 int dynamic_creds_realize(DynamicCreds
*creds
, char **suggested_paths
, uid_t
*uid
, gid_t
*gid
) {
801 uid_t u
= UID_INVALID
;
802 gid_t g
= GID_INVALID
;
809 /* Realize both the referenced user and group */
812 r
= dynamic_user_realize(creds
->user
, suggested_paths
, &u
, &g
, true);
817 if (creds
->group
&& creds
->group
!= creds
->user
) {
818 r
= dynamic_user_realize(creds
->group
, suggested_paths
, NULL
, &g
, false);
828 DynamicCreds
* dynamic_creds_unref(DynamicCreds
*creds
) {
832 creds
->user
= dynamic_user_unref(creds
->user
);
833 creds
->group
= dynamic_user_unref(creds
->group
);
838 DynamicCreds
* dynamic_creds_destroy(DynamicCreds
*creds
) {
842 creds
->user
= dynamic_user_destroy(creds
->user
);
843 creds
->group
= dynamic_user_destroy(creds
->group
);
848 void dynamic_creds_done(DynamicCreds
*creds
) {
852 if (creds
->group
!= creds
->user
)
853 dynamic_user_free(creds
->group
);
854 creds
->group
= creds
->user
= dynamic_user_free(creds
->user
);
857 void dynamic_creds_close(DynamicCreds
*creds
) {
862 safe_close_pair(creds
->user
->storage_socket
);
864 if (creds
->group
&& creds
->group
!= creds
->user
)
865 safe_close_pair(creds
->group
->storage_socket
);