Skip to content

Local array initializers can cause linear AST growth and compiler memory exhaustion #602

Description

@admkopec

Summary

CompCert expands the implicit tail of an automatic-storage array declaration initializer into one assignment AST node per omitted element. As a result, a constant-size initializer such as { 0 } can make compiler memory consumption proportional to the declared array bound and can exhaust memory on valid C programs.

For example:

int f(void)
{
  int aggregated[100000000] = { 0 };
  return aggregated[0];
}

The same issue occurs with a partially explicit initializer:

int f(void)
{
  int aggregated[100000000] = { 1, 2 };
  return aggregated[0] + aggregated[1];
}

This minimal reproducer can be verified in a container limited to 512 MiB, where compilation is killed while the front end processes the initializer. Reducing the bound to 100,000 and compiling with -dparse makes the expansion directly visible in the post-Unblock .parsed.c output.

During the Unblock front-end transformation, the local initializer is recursively expanded into assignments for every array position, including every implicit zero-initialized element. Therefore the intermediate C AST itself becomes linear in the number of omitted elements.

I initially reproduced the problem with CompCert 3.17 and reproduced it again with CompCert 3.18 at commit 66a9fd0. The issue also affects real programs such as certain post-quantum cryptographic algorithms, which require a workaround to compile on typical consumer-grade hardware.

Expected behavior

The size of the AST produced for an implicit tail of a local array declaration should be independent of the number of omitted elements, apart from the nesting depth and the structure needed to initialize one element.

Explicitly written initializer expressions must retain their current typing and lowering behavior, and each expression must still be evaluated exactly once. Implicit elements must receive the typed default initialization required by C, including for nested aggregates, pointers, floating-point objects, and volatile subobjects.

Proposed solution

For an automatic array declaration with bound N and K explicitly supplied elements:

  1. Keep the explicit prefix as the existing typed assignments.
  2. If K < N, emit one counted loop over the half-open range [K, N).
  3. Use a fresh function-local induction variable with the target's size_t
    integer kind.
  4. Recursively emit the existing typed default initialization for one element
    as the loop body.

Conceptually:

T a[N] = { e0, e1 };

would be lowered to the equivalent of:

a[0] = e0;
a[1] = e1;
for (size_t __init_index = 2; __init_index < N; ++__init_index) {
  /* existing typed default initialization of a[__init_index] */
}

This solution is implemented in #603.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions