Skip to content

fix: memory leak of ngrp in updates_groups() - #1638

Open
savoptik wants to merge 9 commits into
shadow-maint:masterfrom
savoptik:savoptik/mlik-userdel
Open

fix: memory leak of ngrp in updates_groups()#1638
savoptik wants to merge 9 commits into
shadow-maint:masterfrom
savoptik:savoptik/mlik-userdel

Conversation

@savoptik

@savoptik savoptik commented Jun 8, 2026

Copy link
Copy Markdown
Contributor
In src/userdel.c:update_groups(), the 'ngrp' pointer was not freed
after use, leading to a memory leak. Added free(ngrp) call.

In src/userdel.c:update_groups(), the 'nsgrp' pointer was not freed
between loop iterations, leading to cumulative memory leaks. Added
free(nsgrp) call before proceeding to next iteration.

Comment thread src/userdel.c Outdated
Comment thread src/userdel.c
@savoptik
savoptik force-pushed the savoptik/mlik-userdel branch from 26d9972 to 15cc201 Compare July 15, 2026 09:37
Comment thread lib/list.c
@savoptik
savoptik force-pushed the savoptik/mlik-userdel branch 3 times, most recently from bf13d38 to c156b77 Compare July 24, 2026 12:49
Comment thread src/userdel.c
Comment thread src/useradd.c
Comment thread src/useradd.c
Comment thread src/userdel.c
Comment thread lib/list.c
savoptik added 4 commits July 29, 2026 13:57
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>
@savoptik
savoptik force-pushed the savoptik/mlik-userdel branch from c156b77 to f71fdac Compare July 29, 2026 11:03
Comment thread src/groupmod.c
Comment thread src/groupmod.c
Comment on lines 277 to 278
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@alejandro-colomar alejandro-colomar Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of these calls lead to lib/commonio.c int 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).

@savoptik
savoptik force-pushed the savoptik/mlik-userdel branch from f71fdac to c4aba30 Compare July 29, 2026 12:46
Comment thread src/groupmod.c Outdated
Comment thread src/groupmod.c
Comment thread src/groupadd.c
Comment thread lib/list.c
Comment on lines +124 to +127
} else {
/* The removed entry is no longer referenced by the
* new list, so free its string here. */
free (list[i]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please separate the last commit message in two. Also, please move the comments into the commit message and remove them from the source code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed all comments from the source code in all my commits.

Are you sure the last (8th) commit needs to be split?

@alejandro-colomar alejandro-colomar Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please; those are two different leaks. Thanks!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@savoptik
savoptik force-pushed the savoptik/mlik-userdel branch from c4aba30 to 3454923 Compare July 30, 2026 09:26
savoptik added 3 commits July 30, 2026 15:30
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>
@savoptik
savoptik force-pushed the savoptik/mlik-userdel branch from 3454923 to e9e14ac Compare July 30, 2026 12:42
savoptik added 2 commits July 30, 2026 17:01
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>
@savoptik
savoptik force-pushed the savoptik/mlik-userdel branch from e9e14ac to be94855 Compare July 30, 2026 14:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants