Repository navigation
Conversation
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.
melver
requested review from
kroening,
peterschrammel and
tautschnig
as code owners
October 30, 2026 10:30
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A
memcpywith a symbolic size into an array of non-byte elements could leavethe destination unchanged:
The
memcpymodel turns this intobyte_update(d, 0, src_n), wheresrc_nisan
unsigned char[n]. Because that size is not constant, byte-operatorlowering 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-alwayswere all affected.Updates spanning several elements
(
lower_byte_update_array_vector_unbounded): when the update ended exactlyat an element boundary, the last updated element was treated as untouched.
For
n == 4,ddid not change at all. When the update ended partwayinto 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.
Updates starting partway into an element (bit-vector case of
lower_byte_update): update bytes past the symbolic bound were replaced bythe element's original byte
i, when update byteiactually lands atbyte
offset + i. Formemcpy((uint8_t *)d + 1, src, 1), bytes 2 and 3 ofd[0]became copies of bytes 1 and 2. This also affected byte updates ofstruct members and unions.
Assisted-by: Antigravity
The feature or user visible behaviour I have added or modified has been documented in the User Guide in(bug fix only)doc/cprover-manual/
which ones those are in the commit message).
My commit message includes data points confirming performance improvements (if claimed).(no performance claims)