Skip to content

# cc_list_reduce() passes NULL as the second callback argument for single-element lists (undocumented, crashes e2-dereferencing callbacks) #182

Description

@trustqq

Environment

  • Collections-C, commit dbedaf9599716c9aa16c6e955fdc3ef76d5c5cca (master, checked 2026-08-26)
  • src/cc_list.c, src/include/cc_list.h, test/unit/list_test.c
  • Found via static analysis; confirmed against this commit

Summary

cc_list_reduce() special-cases list_size == 1 and invokes the callback with a literal NULL as its second argument:

/* src/cc_list.c:1240-1249 */
size_t list_size = cc_list_size(list);

if (list_size == 0)
    return CC_ERR_OUT_OF_RANGE;

if (list_size == 1)
{
    fn(list->head->data, NULL, result);   /* e2 == NULL */
    return CC_OK;
}

fn(list->head->data, list->head->next->data, result);

Neither the prototype (src/include/cc_list.h:147) nor the function's doc comment (src/cc_list.c:1228-1236) documents any convention that e2 may be NULL. Any callback written under the natural assumption that e2 is an element pointer — including the library's own test callbacks — deterministically segfaults on a single-element list:

/* test/unit/list_test.c:42-48 */
void sum_reduce(void* e1, void* e2, void* res)
{
    int i = *(int*)e1;
    int j = *(int*)e2;      /* SIGSEGV when e2 == NULL */

    *(int*)res = i + j;
}

/* test/unit/list_test.c:50-56 — bool_and() has the same unconditional *(bool*)e2 */

The existing reduce tests never cover this branch: test_reduce1 (list_test.c:1166) uses the pre_filled_lists fixture (4 elements: 1, 2, 3, 4), and test_reduce2 (list_test.c:1179) builds its own 4-element list — so the defect is latent in CI.

Steps to reproduce

CC_List *list = cc_list_new();
int one = 1;
int res = 0;
cc_list_add(list, &one);
cc_list_reduce(list, sum_reduce, &res);   /* SIGSEGV, e2 == NULL */

Expected: cc_list_reduce() works on a single-element list (either e2 holds the single element, or a documented NULL contract that callbacks are told to check).

Actual: deterministic NULL pointer dereference (CWE-476) inside the callback.

Suggested fix (library side, preferred)

if (list_size == 1)
{
    fn(list->head->data, list->head->data, result);   /* e2 = the only element */
    return CC_OK;
}

and document the argument semantics in the doc comment. If passing e2 = e1 is not desired, then at minimum the NULL-e2 contract must be documented and the project's own test callbacks made NULL-safe.

Impact: public API (cc_list_reduce), deterministic non-OOM crash of the calling program; silent because CI never exercises the single-element branch.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions