]>
git.ipfire.org Git - thirdparty/systemd.git/blob - src/core/dynamic-user.c
1 /* SPDX-License-Identifier: LGPL-2.1+ */
8 #include "dynamic-user.h"
11 #include "format-util.h"
14 #include "nscd-flush.h"
15 #include "parse-util.h"
16 #include "random-util.h"
17 #include "serialize.h"
18 #include "socket-util.h"
19 #include "stdio-util.h"
20 #include "string-util.h"
22 #include "user-util.h"
24 /* Takes a value generated randomly or by hashing and turns it into a UID in the right range */
25 #define UID_CLAMP_INTO_RANGE(rnd) (((uid_t) (rnd) % (DYNAMIC_UID_MAX - DYNAMIC_UID_MIN + 1)) + DYNAMIC_UID_MIN)
27 DEFINE_PRIVATE_TRIVIAL_REF_FUNC ( DynamicUser
, dynamic_user
);
29 static DynamicUser
* dynamic_user_free ( DynamicUser
* d
) {
34 ( void ) hashmap_remove ( d
-> manager
-> dynamic_users
, d
-> name
);
36 safe_close_pair ( d
-> storage_socket
);
40 static int dynamic_user_add ( Manager
* m
, const char * name
, int storage_socket
[ static 2 ], DynamicUser
** ret
) {
46 assert ( storage_socket
);
48 r
= hashmap_ensure_allocated (& m
-> dynamic_users
, & string_hash_ops
);
52 d
= malloc0 ( offsetof ( DynamicUser
, name
) + strlen ( name
) + 1 );
56 strcpy ( d
-> name
, name
);
58 d
-> storage_socket
[ 0 ] = storage_socket
[ 0 ];
59 d
-> storage_socket
[ 1 ] = storage_socket
[ 1 ];
61 r
= hashmap_put ( m
-> dynamic_users
, d
-> name
, d
);
75 static int dynamic_user_acquire ( Manager
* m
, const char * name
, DynamicUser
** ret
) {
76 _cleanup_close_pair_
int storage_socket
[ 2 ] = { - 1 , - 1 };
83 /* Return the DynamicUser structure for a specific user name. Note that this won't actually allocate a UID for
84 * it, but just prepare the data structure for it. The UID is allocated only on demand, when it's really
85 * needed, and in the child process we fork off, since allocation involves NSS checks which are not OK to do
86 * from PID 1. To allow the children and PID 1 share information about allocated UIDs we use an anonymous
87 * AF_UNIX/SOCK_DGRAM socket (called the "storage socket") that contains at most one datagram with the
88 * allocated UID number, plus an fd referencing the lock file for the UID
89 * (i.e. /run/systemd/dynamic-uid/$UID). Why involve the socket pair? So that PID 1 and all its children can
90 * share the same storage for the UID and lock fd, simply by inheriting the storage socket fds. The socket pair
91 * may exist in three different states:
93 * a) no datagram stored. This is the initial state. In this case the dynamic user was never realized.
95 * b) a datagram containing a UID stored, but no lock fd attached to it. In this case there was already a
96 * statically assigned UID by the same name, which we are reusing.
98 * c) a datagram containing a UID stored, and a lock fd is attached to it. In this case we allocated a dynamic
99 * UID and locked it in the file system, using the lock fd.
101 * As PID 1 and various children might access the socket pair simultaneously, and pop the datagram or push it
102 * back in any time, we also maintain a lock on the socket pair. Note one peculiarity regarding locking here:
103 * 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
104 * place as long as there's a reference to the fd open. The lock on the storage socket pair however is a POSIX
105 * file lock (i.e. a process-bound lock), as all users share the same fd of this (after all it is anonymous,
106 * nobody else could get any access to it except via our own fd) and we want to synchronize access between all
107 * processes that have access to it. */
109 d
= hashmap_get ( m
-> dynamic_users
, name
);
112 /* We already have a structure for the dynamic user, let's increase the ref count and reuse it */
119 if (! valid_user_group_name ( name
, VALID_USER_ALLOW_NUMERIC
))
122 if ( socketpair ( AF_UNIX
, SOCK_DGRAM
| SOCK_CLOEXEC
, 0 , storage_socket
) < 0 )
125 r
= dynamic_user_add ( m
, name
, storage_socket
, & d
);
129 storage_socket
[ 0 ] = storage_socket
[ 1 ] = - 1 ;
139 static int make_uid_symlinks ( uid_t uid
, const char * name
, bool b
) {
141 char path1
[ STRLEN ( "/run/systemd/dynamic-uid/direct:" ) + DECIMAL_STR_MAX ( uid_t
) + 1 ];
145 /* Add direct additional symlinks for direct lookups of dynamic UIDs and their names by userspace code. The
146 * only reason we have this is because dbus-daemon cannot use D-Bus for resolving users and groups (since it
147 * would be its own client then). We hence keep these world-readable symlinks in place, so that the
148 * unprivileged dbus user can read the mappings when it needs them via these symlinks instead of having to go
149 * via the bus. Ideally, we'd use the lock files we keep for this anyway, but we can't since we use BSD locks
150 * on them and as those may be taken by any user with read access we can't make them world-readable. */
152 xsprintf ( path1
, "/run/systemd/dynamic-uid/direct:" UID_FMT
, uid
);
153 if ( unlink ( path1
) < 0 && errno
!= ENOENT
)
156 if ( b
&& symlink ( name
, path1
) < 0 ) {
157 k
= log_warning_errno ( errno
, "Failed to symlink \" %s \" : %m" , path1
);
162 path2
= strjoina ( "/run/systemd/dynamic-uid/direct:" , name
);
163 if ( unlink ( path2
) < 0 && errno
!= ENOENT
) {
169 if ( b
&& symlink ( path1
+ STRLEN ( "/run/systemd/dynamic-uid/direct:" ), path2
) < 0 ) {
170 k
= log_warning_errno ( errno
, "Failed to symlink \" %s \" : %m" , path2
);
178 static int pick_uid ( char ** suggested_paths
, const char * name
, uid_t
* ret_uid
) {
180 /* Find a suitable free UID. We use the following strategy to find a suitable UID:
182 * 1. Initially, we try to read the UID of a number of specified paths. If any of these UIDs works, we use
183 * them. We use in order to increase the chance of UID reuse, if StateDirectory=, CacheDirectory= or
184 * LogsDirectory= are used, as reusing the UID these directories are owned by saves us from having to
185 * recursively chown() them to new users.
187 * 2. If that didn't yield a currently unused UID, we hash the user name, and try to use that. This should be
188 * pretty good, as the use ris by default derived from the unit name, and hence the same service and same
189 * user should usually get the same UID as long as our hashing doesn't clash.
191 * 3. Finally, if that didn't work, we randomly pick UIDs, until we find one that is empty.
193 * Since the dynamic UID space is relatively small we'll stop trying after 100 iterations, giving up. */
196 PHASE_SUGGESTED
, /* the first phase, reusing directory ownership UIDs */
197 PHASE_HASHED
, /* the second phase, deriving a UID from the username by hashing */
198 PHASE_RANDOM
, /* the last phase, randomly picking UIDs */
199 } phase
= PHASE_SUGGESTED
;
201 static const uint8_t hash_key
[] = {
202 0x37 , 0x53 , 0x7e , 0x31 , 0xcf , 0xce , 0x48 , 0xf5 ,
203 0x8a , 0xbb , 0x39 , 0x57 , 0x8d , 0xd9 , 0xec , 0x59
206 unsigned n_tries
= 100 , current_suggested
= 0 ;
209 ( void ) mkdir ( "/run/systemd/dynamic-uid" , 0755 );
212 char lock_path
[ STRLEN ( "/run/systemd/dynamic-uid/" ) + DECIMAL_STR_MAX ( uid_t
) + 1 ];
213 _cleanup_close_
int lock_fd
= - 1 ;
217 if (-- n_tries
<= 0 ) /* Give up retrying eventually */
222 case PHASE_SUGGESTED
: {
225 if (! suggested_paths
|| ! suggested_paths
[ current_suggested
]) {
226 /* We reached the end of the suggested paths list, let's try by hashing the name */
227 phase
= PHASE_HASHED
;
231 if ( stat ( suggested_paths
[ current_suggested
++], & st
) < 0 )
232 continue ; /* We can't read the UID of this path, but that doesn't matter, just try the next */
234 candidate
= st
. st_uid
;
239 /* A static user by this name does not exist yet. Let's find a free ID then, and use that. We
240 * start with a UID generated as hash from the user name. */
241 candidate
= UID_CLAMP_INTO_RANGE ( siphash24 ( name
, strlen ( name
), hash_key
));
243 /* If this one fails, we should proceed with random tries */
244 phase
= PHASE_RANDOM
;
249 /* Pick another random UID, and see if that works for us. */
250 random_bytes (& candidate
, sizeof ( candidate
));
251 candidate
= UID_CLAMP_INTO_RANGE ( candidate
);
255 assert_not_reached ( "unknown phase" );
258 /* Make sure whatever we picked here actually is in the right range */
259 if (! uid_is_dynamic ( candidate
))
262 xsprintf ( lock_path
, "/run/systemd/dynamic-uid/" UID_FMT
, candidate
);
267 lock_fd
= open ( lock_path
, O_CREAT
| O_RDWR
| O_NOFOLLOW
| O_CLOEXEC
| O_NOCTTY
, 0600 );
271 r
= flock ( lock_fd
, LOCK_EX
| LOCK_NB
); /* Try to get a BSD file lock on the UID lock file */
273 if ( IN_SET ( errno
, EBUSY
, EAGAIN
))
274 goto next
; /* already in use */
279 if ( fstat ( lock_fd
, & st
) < 0 )
284 /* Oh, bummer, we got the lock, but the file was unlinked between the time we opened it and
285 * got the lock. Close it, and try again. */
286 lock_fd
= safe_close ( lock_fd
);
289 /* Some superficial check whether this UID/GID might already be taken by some static user */
290 if ( getpwuid ( candidate
) ||
291 getgrgid (( gid_t
) candidate
) ||
292 search_ipc ( candidate
, ( gid_t
) candidate
) != 0 ) {
293 ( void ) unlink ( lock_path
);
297 /* Let's store the user name in the lock file, so that we can use it for looking up the username for a UID */
300 IOVEC_INIT_STRING ( name
),
301 IOVEC_INIT (( char [ 1 ]) { ' \n ' }, 1 ),
305 ( void ) unlink ( lock_path
);
309 ( void ) ftruncate ( lock_fd
, l
);
310 ( void ) make_uid_symlinks ( candidate
, name
, true ); /* also add direct lookup symlinks */
312 * ret_uid
= candidate
;
313 return TAKE_FD ( lock_fd
);
320 static int dynamic_user_pop ( DynamicUser
* d
, uid_t
* ret_uid
, int * ret_lock_fd
) {
321 uid_t uid
= UID_INVALID
;
322 struct iovec iov
= IOVEC_INIT (& uid
, sizeof ( uid
));
330 /* Read the UID and lock fd that is stored in the storage AF_UNIX socket. This should be called with the lock
331 * on the socket taken. */
333 k
= receive_one_fd_iov ( d
-> storage_socket
[ 0 ], & iov
, 1 , MSG_DONTWAIT
, & lock_fd
);
338 * ret_lock_fd
= lock_fd
;
343 static int dynamic_user_push ( DynamicUser
* d
, uid_t uid
, int lock_fd
) {
344 struct iovec iov
= IOVEC_INIT (& uid
, sizeof ( uid
));
348 /* Store the UID and lock_fd in the storage socket. This should be called with the socket pair lock taken. */
349 return send_one_fd_iov ( d
-> storage_socket
[ 1 ], lock_fd
, & iov
, 1 , MSG_DONTWAIT
);
352 static void unlink_uid_lock ( int lock_fd
, uid_t uid
, const char * name
) {
353 char lock_path
[ STRLEN ( "/run/systemd/dynamic-uid/" ) + DECIMAL_STR_MAX ( uid_t
) + 1 ];
358 xsprintf ( lock_path
, "/run/systemd/dynamic-uid/" UID_FMT
, uid
);
359 ( void ) unlink ( lock_path
);
361 ( void ) make_uid_symlinks ( uid
, name
, false ); /* remove direct lookup symlinks */
364 static int lockfp ( int fd
, int * fd_lock
) {
365 if ( lockf ( fd
, F_LOCK
, 0 ) < 0 )
371 static void unlockfp ( int * fd_lock
) {
374 lockf (* fd_lock
, F_ULOCK
, 0 );
378 static int dynamic_user_realize (
380 char ** suggested_dirs
,
381 uid_t
* ret_uid
, gid_t
* ret_gid
,
384 _cleanup_ ( unlockfp
) int storage_socket0_lock
= - 1 ;
385 _cleanup_close_
int uid_lock_fd
= - 1 ;
386 _cleanup_close_
int etc_passwd_lock_fd
= - 1 ;
387 uid_t num
= UID_INVALID
; /* a uid if is_user, and a gid otherwise */
388 gid_t gid
= GID_INVALID
; /* a gid if is_user, ignored otherwise */
389 bool flush_cache
= false ;
393 assert ( is_user
== !! ret_uid
);
396 /* Acquire a UID for the user name. This will allocate a UID for the user name if the user doesn't exist
397 * yet. If it already exists its existing UID/GID will be reused. */
399 r
= lockfp ( d
-> storage_socket
[ 0 ], & storage_socket0_lock
);
403 r
= dynamic_user_pop ( d
, & num
, & uid_lock_fd
);
411 /* OK, nothing stored yet, let's try to find something useful. While we are working on this release the
412 * lock however, so that nobody else blocks on our NSS lookups. */
413 unlockfp (& storage_socket0_lock
);
415 /* Let's see if a proper, static user or group by this name exists. Try to take the lock on
416 * /etc/passwd, if that fails with EROFS then /etc is read-only. In that case it's fine if we don't
417 * take the lock, given that users can't be added there anyway in this case. */
418 etc_passwd_lock_fd
= take_etc_passwd_lock ( NULL
);
419 if ( etc_passwd_lock_fd
< 0 && etc_passwd_lock_fd
!= - EROFS
)
420 return etc_passwd_lock_fd
;
422 /* First, let's parse this as numeric UID */
423 r
= parse_uid ( d
-> name
, & num
);
429 /* OK, this is not a numeric UID. Let's see if there's a user by this name */
430 p
= getpwnam ( d
-> name
);
435 /* if the user does not exist but the group with the same name exists, refuse operation */
436 g
= getgrnam ( d
-> name
);
441 /* Let's see if there's a group by this name */
442 g
= getgrnam ( d
-> name
);
444 num
= ( uid_t
) g
-> gr_gid
;
446 /* if the group does not exist but the user with the same name exists, refuse operation */
447 p
= getpwnam ( d
-> name
);
454 if ( num
== UID_INVALID
) {
455 /* No static UID assigned yet, excellent. Let's pick a new dynamic one, and lock it. */
457 uid_lock_fd
= pick_uid ( suggested_dirs
, d
-> name
, & num
);
462 /* So, we found a working UID/lock combination. Let's see if we actually still need it. */
463 r
= lockfp ( d
-> storage_socket
[ 0 ], & storage_socket0_lock
);
465 unlink_uid_lock ( uid_lock_fd
, num
, d
-> name
);
469 r
= dynamic_user_pop ( d
, & new_uid
, & new_uid_lock_fd
);
472 /* OK, something bad happened, let's get rid of the bits we acquired. */
473 unlink_uid_lock ( uid_lock_fd
, num
, d
-> name
);
477 /* Great! Nothing is stored here, still. Store our newly acquired data. */
480 /* Hmm, so as it appears there's now something stored in the storage socket. Throw away what we
481 * acquired, and use what's stored now. */
483 unlink_uid_lock ( uid_lock_fd
, num
, d
-> name
);
484 safe_close ( uid_lock_fd
);
487 uid_lock_fd
= new_uid_lock_fd
;
489 } else if ( is_user
&& ! uid_is_dynamic ( num
)) {
492 /* Statically allocated user may have different uid and gid. So, let's obtain the gid. */
496 return errno_or_else ( ESRCH
);
501 /* If the UID/GID was already allocated dynamically, push the data we popped out back in. If it was already
502 * allocated statically, push the UID back too, but do not push the lock fd in. If we allocated the UID
503 * dynamically right here, push that in along with the lock fd for it. */
504 r
= dynamic_user_push ( d
, num
, uid_lock_fd
);
509 /* If we allocated a new dynamic UID, refresh nscd, so that it forgets about potentially cached
510 * negative entries. But let's do so after we release the /etc/passwd lock, so that there's no
511 * potential for nscd wanting to lock that for completing the invalidation. */
512 etc_passwd_lock_fd
= safe_close ( etc_passwd_lock_fd
);
513 ( void ) nscd_flush_cache ( STRV_MAKE ( "passwd" , "group" ));
518 * ret_gid
= gid
!= GID_INVALID
? gid
: num
;
525 int dynamic_user_current ( DynamicUser
* d
, uid_t
* ret
) {
526 _cleanup_ ( unlockfp
) int storage_socket0_lock
= - 1 ;
527 _cleanup_close_
int lock_fd
= - 1 ;
533 /* Get the currently assigned UID for the user, if there's any. This simply pops the data from the storage socket, and pushes it back in right-away. */
535 r
= lockfp ( d
-> storage_socket
[ 0 ], & storage_socket0_lock
);
539 r
= dynamic_user_pop ( d
, & uid
, & lock_fd
);
543 r
= dynamic_user_push ( d
, uid
, lock_fd
);
553 static DynamicUser
* dynamic_user_unref ( DynamicUser
* d
) {
557 /* Note that this doesn't actually release any resources itself. If a dynamic user should be fully destroyed
558 * and its UID released, use dynamic_user_destroy() instead. NB: the dynamic user table may contain entries
559 * with no references, which is commonly the case right before a daemon reload. */
561 assert ( d
-> n_ref
> 0 );
567 static int dynamic_user_close ( DynamicUser
* d
) {
568 _cleanup_ ( unlockfp
) int storage_socket0_lock
= - 1 ;
569 _cleanup_close_
int lock_fd
= - 1 ;
573 /* Release the user ID, by releasing the lock on it, and emptying the storage socket. After this the user is
574 * unrealized again, much like it was after it the DynamicUser object was first allocated. */
576 r
= lockfp ( d
-> storage_socket
[ 0 ], & storage_socket0_lock
);
580 r
= dynamic_user_pop ( d
, & uid
, & lock_fd
);
582 /* User wasn't realized yet, nothing to do. */
587 /* This dynamic user was realized and dynamically allocated. In this case, let's remove the lock file. */
588 unlink_uid_lock ( lock_fd
, uid
, d
-> name
);
590 ( void ) nscd_flush_cache ( STRV_MAKE ( "passwd" , "group" ));
594 static DynamicUser
* dynamic_user_destroy ( DynamicUser
* d
) {
598 /* Drop a reference to a DynamicUser object, and destroy the user completely if this was the last
599 * reference. This is called whenever a service is shut down and wants its dynamic UID gone. Note that
600 * dynamic_user_unref() is what is called whenever a service is simply freed, for example during a reload
601 * cycle, where the dynamic users should not be destroyed, but our datastructures should. */
603 dynamic_user_unref ( d
);
608 ( void ) dynamic_user_close ( d
);
609 return dynamic_user_free ( d
);
612 int dynamic_user_serialize ( Manager
* m
, FILE * f
, FDSet
* fds
) {
619 /* Dump the dynamic user database into the manager serialization, to deal with daemon reloads. */
621 HASHMAP_FOREACH ( d
, m
-> dynamic_users
) {
624 copy0
= fdset_put_dup ( fds
, d
-> storage_socket
[ 0 ]);
626 return log_error_errno ( copy0
, "Failed to add dynamic user storage fd to serialization: %m" );
628 copy1
= fdset_put_dup ( fds
, d
-> storage_socket
[ 1 ]);
630 return log_error_errno ( copy1
, "Failed to add dynamic user storage fd to serialization: %m" );
632 ( void ) serialize_item_format ( f
, "dynamic-user" , "%s %i %i" , d
-> name
, copy0
, copy1
);
638 void dynamic_user_deserialize_one ( Manager
* m
, const char * value
, FDSet
* fds
) {
639 _cleanup_free_
char * name
= NULL
, * s0
= NULL
, * s1
= NULL
;
646 /* Parse the serialization again, after a daemon reload */
648 r
= extract_many_words (& value
, NULL
, 0 , & name
, & s0
, & s1
, NULL
);
649 if ( r
!= 3 || ! isempty ( value
)) {
650 log_debug ( "Unable to parse dynamic user line." );
654 if ( safe_atoi ( s0
, & fd0
) < 0 || ! fdset_contains ( fds
, fd0
)) {
655 log_debug ( "Unable to process dynamic user fd specification." );
659 if ( safe_atoi ( s1
, & fd1
) < 0 || ! fdset_contains ( fds
, fd1
)) {
660 log_debug ( "Unable to process dynamic user fd specification." );
664 r
= dynamic_user_add ( m
, name
, ( int []) { fd0
, fd1
}, NULL
);
666 log_debug_errno ( r
, "Failed to add dynamic user: %m" );
670 ( void ) fdset_remove ( fds
, fd0
);
671 ( void ) fdset_remove ( fds
, fd1
);
674 void dynamic_user_vacuum ( Manager
* m
, bool close_user
) {
679 /* Empty the dynamic user database, optionally cleaning up orphaned dynamic users, i.e. destroy and free users
680 * to which no reference exist. This is called after a daemon reload finished, in order to destroy users which
681 * might not be referenced anymore. */
683 HASHMAP_FOREACH ( d
, m
-> dynamic_users
) {
688 log_debug ( "Removing orphaned dynamic user %s" , d
-> name
);
689 ( void ) dynamic_user_close ( d
);
692 dynamic_user_free ( d
);
696 int dynamic_user_lookup_uid ( Manager
* m
, uid_t uid
, char ** ret
) {
697 char lock_path
[ STRLEN ( "/run/systemd/dynamic-uid/" ) + DECIMAL_STR_MAX ( uid_t
) + 1 ];
698 _cleanup_free_
char * user
= NULL
;
705 /* A friendly way to translate a dynamic user's UID into a name. */
706 if (! uid_is_dynamic ( uid
))
709 xsprintf ( lock_path
, "/run/systemd/dynamic-uid/" UID_FMT
, uid
);
710 r
= read_one_line_file ( lock_path
, & user
);
711 if ( IN_SET ( r
, - ENOENT
, 0 ))
716 /* The lock file might be stale, hence let's verify the data before we return it */
717 r
= dynamic_user_lookup_name ( m
, user
, & check_uid
);
720 if ( check_uid
!= uid
) /* lock file doesn't match our own idea */
723 * ret
= TAKE_PTR ( user
);
728 int dynamic_user_lookup_name ( Manager
* m
, const char * name
, uid_t
* ret
) {
735 /* A friendly call for translating a dynamic user's name into its UID */
737 d
= hashmap_get ( m
-> dynamic_users
, name
);
741 r
= dynamic_user_current ( d
, ret
);
742 if ( r
== - EAGAIN
) /* not realized yet? */
748 int dynamic_creds_acquire ( DynamicCreds
* creds
, Manager
* m
, const char * user
, const char * group
) {
749 bool acquired
= false ;
755 /* A DynamicUser object encapsulates an allocation of both a UID and a GID for a specific name. However, some
756 * services use different user and groups. For cases like that there's DynamicCreds containing a pair of user
757 * and group. This call allocates a pair. */
759 if (! creds
-> user
&& user
) {
760 r
= dynamic_user_acquire ( m
, user
, & creds
-> user
);
769 if ( creds
-> user
&& (! group
|| streq_ptr ( user
, group
)))
770 creds
-> group
= dynamic_user_ref ( creds
-> user
);
772 r
= dynamic_user_acquire ( m
, group
, & creds
-> group
);
775 creds
-> user
= dynamic_user_unref ( creds
-> user
);
784 int dynamic_creds_realize ( DynamicCreds
* creds
, char ** suggested_paths
, uid_t
* uid
, gid_t
* gid
) {
785 uid_t u
= UID_INVALID
;
786 gid_t g
= GID_INVALID
;
793 /* Realize both the referenced user and group */
796 r
= dynamic_user_realize ( creds
-> user
, suggested_paths
, & u
, & g
, true );
801 if ( creds
-> group
&& creds
-> group
!= creds
-> user
) {
802 r
= dynamic_user_realize ( creds
-> group
, suggested_paths
, NULL
, & g
, false );
812 void dynamic_creds_unref ( DynamicCreds
* creds
) {
815 creds
-> user
= dynamic_user_unref ( creds
-> user
);
816 creds
-> group
= dynamic_user_unref ( creds
-> group
);
819 void dynamic_creds_destroy ( DynamicCreds
* creds
) {
822 creds
-> user
= dynamic_user_destroy ( creds
-> user
);
823 creds
-> group
= dynamic_user_destroy ( creds
-> group
);