fix: memory leak of ngrp in updates_groups() - #1638
Conversation
savoptik
commented
Jun 8, 2026
13e9f5d to
26d9972
Compare
26d9972 to
15cc201
Compare
bf13d38 to
c156b77
Compare
update_groups() duplicates each shadow group with __sgr_dup() to edit
its member list, but never releases the copy. The 'nsgrp' struct sgrp,
along with its deep-copied name, passwd and member/admin arrays, leaks
once per iteration.
__sgr_dup() is not malloc()-like, so free() would not release the inner
allocations; use the matching sgr_free().
valgrind --leak-check=full --show-leak-kinds=all src/userdel -P <prefix> bob
(bob is a member of 10 groups), before this commit:
551 (320 direct, 231 indirect) bytes in 10 blocks are definitely lost
at 0x4848EB8: calloc
by __sgr_dup (sgroupio.c:37)
by update_groups (userdel.c:255)
...
definitely lost: 1,120 bytes in 50 blocks
after this commit:
definitely lost: 800 bytes in 40 blocks
Signed-off-by: Artem Semenov <savoptik@altlinux.org>
Reviewed-by: Alejandro Colomar <alx@kernel.org>
Likewise, update_groups() duplicates each regular group with __gr_dup()
and never frees the 'ngrp' copy. Release it with gr_free(); as with
__sgr_dup(), the duplicate owns deep-copied fields, so free() alone would
leak them.
valgrind --leak-check=full --show-leak-kinds=all src/userdel -P <prefix> bob,
before this commit:
471 (320 direct, 151 indirect) bytes in 10 blocks are definitely lost
at 0x4848EB8: calloc
by __gr_dup (groupmem.c:28)
by update_groups (userdel.c:197)
definitely lost: 800 bytes in 40 blocks
after this commit:
definitely lost: 480 bytes in 30 blocks
Signed-off-by: Artem Semenov <savoptik@altlinux.org>
Reviewed-by: Alejandro Colomar <alx@kernel.org>
grp_update() duplicates each supplementary group with __gr_dup() to add
the new user, but never frees the 'ngrp' copy. Release it with
gr_free().
valgrind --leak-check=full --show-leak-kinds=all \
src/useradd -M -N -G grp1,...,grp10 alice
(the 10 groups already exist), before this commit:
611 (320 direct, 291 indirect) bytes in 10 blocks are definitely lost
at 0x4848EB8: calloc
by __gr_dup (groupmem.c:28)
by grp_update (useradd.c:1042)
definitely lost: 800 bytes in 40 blocks
after this commit:
definitely lost: 480 bytes in 30 blocks
Signed-off-by: Artem Semenov <savoptik@altlinux.org>
Reviewed-by: Alejandro Colomar <alx@kernel.org>
grp_update() also duplicates each shadow group with __sgr_dup() and
never frees the 'nsgrp' copy. Release it with sgr_free().
valgrind --leak-check=full --show-leak-kinds=all \
src/useradd -M -N -G grp1,...,grp10 alice, before this commit:
691 (320 direct, 371 indirect) bytes in 10 blocks are definitely lost
at 0x4848EB8: calloc
by __sgr_dup (sgroupio.c:37)
by grp_update (useradd.c:1104)
definitely lost: 480 bytes in 30 blocks
after this commit:
definitely lost: 160 bytes in 20 blocks
Signed-off-by: Artem Semenov <savoptik@altlinux.org>
Reviewed-by: Alejandro Colomar <alx@kernel.org>
c156b77 to
f71fdac
Compare
| } | ||
| } |
There was a problem hiding this comment.
From the commit message:
, producing a
use-after-free and an invalid free in the subsequent gr_update() /
sgr_update() call.
Does gr_update() free the input? If so, I'd appreciate some more detailed analysis, as this code is difficult to follow.
There was a problem hiding this comment.
Both of these calls lead to lib/commonio.c int commonio_update (struct commonio_db *db, const void *eptr).
In different execution paths, the cleanup function ```db->ops->cio_free()```` is called.
There was a problem hiding this comment.
Both of these calls lead to
lib/commonio.cint commonio_update (struct commonio_db *db, const void *eptr).
Yup.
alx@devuan:~/src/shadow/shadow/master$ grepc -h -tfd gr_update .
int gr_update (const struct group *gr)
{
return commonio_update (&group_db, gr);
}
alx@devuan:~/src/shadow/shadow/master$ grepc -h -tfd sgr_update .
int sgr_update (const struct sgrp *sg)
{
return commonio_update (&gshadow_db, sg);
}In different execution paths, the cleanup function ```db->ops->cio_free()```` is called.
Yup, ...->cio_free() ends up being called, but it seems to be called on memory allocated within the function itself, AFAICS.
alx@devuan:~/src/shadow/shadow/master$ grepc -h -tfd commonio_update . | grep -e '^\w' -e cio_free
int commonio_update (struct commonio_db *db, const void *eptr)
db->ops->cio_free(nentry);
db->ops->cio_free(p->eptr);
db->ops->cio_free(nentry);If that memory points to memory owned by the caller, at least it's not obvious to me (but this function is hard to read, so it could be).
f71fdac to
c4aba30
Compare
| } else { | ||
| /* The removed entry is no longer referenced by the | ||
| * new list, so free its string here. */ | ||
| free (list[i]); |
There was a problem hiding this comment.
Please separate the last commit message in two. Also, please move the comments into the commit message and remove them from the source code.
There was a problem hiding this comment.
Could you please clarify what you mean by “splitting the commit message”?
Also, I’ve removed the comments you pointed out — do you mean I should remove all comments from the code, or just the ones we discussed?
There was a problem hiding this comment.
Could you please clarify what you mean by “splitting the commit message”?
Oops, I meant the commit, not commit message. :)
Also, I’ve removed the comments you pointed out — do you mean I should remove all comments from the code, or just the ones we discussed?
The existing comments don't need to be removed. I remove them when modifying the related code enough, and otherwise leave them.
But I'm quite averse to adding new source-code comments.
There was a problem hiding this comment.
I removed all comments from the source code in all my commits.
Are you sure the last (8th) commit needs to be split?
There was a problem hiding this comment.
Yes, please; those are two different leaks. Thanks!
c4aba30 to
3454923
Compare
In the -a (append) path, grp.gr_mem and sgrp.sg_mem were duplicated
only when the existing member list was non-empty:
if (NULL != grp.gr_mem[0])
grp.gr_mem = dup_list(grp.gr_mem);
When the list was empty, the pointers still referred to the array
inside the gr_locate() / sgr_locate() database entry. add_list()
is declared /*@only@*/ on its first parameter and will free that
array once it starts releasing the old container, producing a
use-after-free and an invalid free in the subsequent gr_update() /
sgr_update() call.
Remove the NULL-check guards so dup_list() always runs, giving
add_list() an owned copy it may safely free.
Signed-off-by: Artem Semenov <savoptik@altlinux.org>
The pflg branch that synthesizes a fresh gshadow entry eagerly
duplicated the member list:
sgrp.sg_mem = dup_list(grp.gr_mem);
That owned copy is only ever released in the user_list != NULL path
(where sgrp.sg_mem is unconditionally overwritten by dup_list()/
xmalloc_T() before it is freed). When groupmod is invoked with -p and
no member list (user_list == NULL), sgrp.sg_mem keeps the eager
duplicate, which nothing frees -> a leak on every such call.
Borrow grp.gr_mem instead. sgr_update() deep-copies the entry, so the
borrowed array is only read. The borrow stays live exclusively while
user_list == NULL (never freed); as soon as a member list is given,
sgrp.sg_mem is reassigned to an owned array before the free at the end
of grp_update(). No use-after-free, no double free.
Signed-off-by: Artem Semenov <savoptik@altlinux.org>
add_list() allocates a new array, copies the existing pointers into it and appends the new member, but never frees the old array it was handed. The parameter is annotated /*@only@*/, so add_list() owns it; free the old container before returning the new one. This leaked the previous member array on every addition, e.g. in useradd's grp_update(): valgrind --leak-check=full --show-leak-kinds=all \ src/useradd -M -N -G grp1,...,grp10 alice before this commit: 80 bytes in 10 blocks are definitely lost at 0x4849388: reallocarray by __gr_dup (groupmem.c:52) by grp_update (useradd.c:1042) 80 bytes in 10 blocks are definitely lost at 0x4849388: reallocarray by __sgr_dup (sgroupio.c:86) by grp_update (useradd.c:1104) definitely lost: 160 bytes in 20 blocks after this commit: definitely lost: 0 bytes in 0 blocks Two callers relied on add_list() not freeing its argument and must be given an owned allocation now: - groupadd: new_grent()/new_sgent() pointed the member lists at a shared static sentinel (&empty_list), which free() must never be applied to. Give groupadd owned heap lists (comma_to_list("")) and release them once the entry has been stored (gr_update()/sgr_update() keep a copy). This also drops the member arrays groupadd leaked: valgrind ... src/groupadd -U alice,bob newgrp before this commit: definitely lost: 80 bytes in 4 blocks after this commit: definitely lost: 0 bytes in 0 blocks - groupmod: release owned member lists built in the append path once gr_update()/sgr_update() have stored their copy; this plugs the pre-existing leak of the duplicated array. valgrind ... src/groupmod -a -U alice emptygrp before this commit: definitely lost: 32 bytes in 2 blocks after this commit: definitely lost: 0 bytes in 0 blocks Signed-off-by: Artem Semenov <savoptik@altlinux.org> Reviewed-by: Alejandro Colomar <alx@kernel.org>
3454923 to
e9e14ac
Compare
When del_list() removes a member it allocates a new array and copies the surviving pointers into it, but it never freed the removed member's string. Once the new array is returned that string becomes unreachable. Free the removed entry's string as it is skipped in the copy loop. The freeing happens after the new array has been allocated, and the freed string is never compared against 'member' again, so the loop stays safe. Signed-off-by: Artem Semenov <savoptik@altlinux.org>
del_list() allocates a new array for the surviving pointers but never freed the old container array, which becomes unreachable once the new array is returned. Free the old container after the survivors have been transferred to the new array. The parameter is /*@only@*/, so del_list() owns it and is responsible for releasing it. valgrind --leak-check=full --show-leak-kinds=all src/userdel -P <prefix> bob, before this commit: 200 (160 direct, 40 indirect) bytes in 10 blocks are definitely lost at 0x4849388: reallocarray by __gr_dup (groupmem.c:52) by update_groups (userdel.c:197) 200 (160 direct, 40 indirect) bytes in 10 blocks are definitely lost at 0x4849388: reallocarray by __sgr_dup (sgroupio.c:61) by update_groups (userdel.c:256) 200 (160 direct, 40 indirect) bytes in 10 blocks are definitely lost at 0x4849388: reallocarray by __sgr_dup (sgroupio.c:86) by update_groups (userdel.c:256) definitely lost: 480 bytes in 30 blocks after this commit: definitely lost: 0 bytes in 0 blocks Signed-off-by: Artem Semenov <savoptik@altlinux.org>
e9e14ac to
be94855
Compare