Skip to content

Fix byte_update lowering for updates of symbolic size - #9380

Open
melver wants to merge 2 commits into
diffblue:developfrom
melver:fix-byte-update-symbolic-size
Open

melver wants to merge 2 commits into
diffblue:developfrom
melver:fix-byte-update-symbolic-size

Conversation

@melver

@melver melver commented Oct 30, 2026

Copy link
Copy Markdown

A memcpy with a symbolic size into an array of non-byte elements could leave
the destination unchanged:

uint32_t src[4], d[4];
size_t n; // symbolic, e.g. n == 4 || n == 8
memcpy(d, src, n);
assert(d[0] == src[0]); // fails

The memcpy model turns this into byte_update(d, 0, src_n), where src_n is
an unsigned char[n]. Because that size is not constant, byte-operator
lowering has to encode the update with a symbolic bound, and it got this wrong
in two independent ways. Every back-end uses this lowering, so the default SAT
back-end, SMT2 solvers (Z3, CVC5), and --arrays-uf-always were all affected.

  1. Updates spanning several elements
    (lower_byte_update_array_vector_unbounded): when the update ended exactly
    at an element boundary, the last updated element was treated as untouched.
    For n == 4, d did not change at all. When the update ended partway
    into a later element, that element's partial update ignored the update
    offset and used the wrong bytes. All elements after the first now use the
    same formula, which removes the last-element special case and is a net
    reduction in code.

  2. Updates starting partway into an element (bit-vector case of
    lower_byte_update): update bytes past the symbolic bound were replaced by
    the element's original byte i, when update byte i actually lands at
    byte offset + i. For memcpy((uint8_t *)d + 1, src, 1), bytes 2 and 3 of
    d[0] became copies of bytes 1 and 2. This also affected byte updates of
    struct members and unions.

Assisted-by: Antigravity


  • Each commit message has a non-empty body, explaining why the change was made.
  • Methods or procedures I have added are documented, following the guidelines provided in CODING_STANDARD.md.
  • n/a The feature or user visible behaviour I have added or modified has been documented in the User Guide in
    doc/cprover-manual/
    (bug fix only)
  • Regression or unit tests are included, or existing tests cover the modified code (in this case I have detailed
    which ones those are in the commit message).
  • n/a My commit message includes data points confirming performance improvements (if claimed). (no performance claims)
  • My PR is restricted to a single feature or bugfix.
  • White-space or formatting changes outside the feature-related changed lines are in commits of their own.

Consider a memcpy with a symbolic size into an array of non-byte
elements:

    uint32_t src[4], d[4];
    size_t n; // symbolic
    memcpy(d, src, n);

The memcpy model copies src into a temporary unsigned char[n] and
writes that into d:

    d = byte_update(d, 0, src_n); // src_n is unsigned char[n]

As the size of the update value is not constant, byte-operator lowering
encodes this update as an array comprehension over the elements of d
(lower_byte_update_array_vector_unbounded): the first element takes
the bytes of the update value that fall into it, the elements after it
take 4 bytes each, and the last element takes the remaining bytes.

The comprehension's upper bound, however, treated the last element as
not updated whenever the update ended at an element boundary. For
n == 4, the first and last element are both d[0], so d did not change at
all. Furthermore, the partial update of the last element ignored the
offset of the update, and only considered the first 4 bytes of src_n. As
all back-ends use this lowering, the default SAT back-end, SMT2 solvers,
and --arrays-uf-always were all affected.

Treat all elements after the first alike: each takes the 4 bytes of the
update value that map to it, of which only as many as remain in the
update value are applied (possibly none). This removes the need to
compute the last element and its partial update.
Consider a memcpy with a symbolic size to an offset that is not a
multiple of the element size:

    uint32_t src[4], d[4];
    size_t n; // symbolic
    memcpy((uint8_t *)d + 1, src, n);

For d[0], byte-operator lowering encodes this as a byte update of d[0]
at offset 1 with the bytes of src. Only the first n of these bytes may
be applied, so lowering keeps update byte i if i < n, and otherwise
replaces it by the original byte. Update byte i is placed at byte
1 + i of d[0], but the original byte used was byte i. For n == 1, for
example, bytes 2 and 3 of d[0] became copies of the original bytes 1
and 2. Byte updates of struct members and unions use the same lowering,
and were affected in the same way.

Use the original byte at the position the update byte is placed at.

This branch has not been deployed

No deployments
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.

1 participant