Skip to content

ACD: 64-bit shift by 64 or more in local_extend_to - #541

Open
marcelwa wants to merge 1 commit into
berkeley-abc:masterfrom
marcelwa:acd-local-extend-shift-ub
Open

ACD: 64-bit shift by 64 or more in local_extend_to#541
marcelwa wants to merge 1 commit into
berkeley-abc:masterfrom
marcelwa:acd-local-extend-shift-ub

Conversation

@marcelwa

@marcelwa marcelwa commented Aug 8, 2026

Copy link
Copy Markdown

The defect

In src/map/if/acd/ac_decomposition.hpp, local_extend_to replicates a truth-table word:

auto mask = *tt.begin();

for ( auto i = real_num_vars; i < num_vars; ++i )
{
  mask |= ( mask << ( 1 << i ) );
}

mask is a 64-bit word, so for i >= 6 the shift amount 1 << i is at least 64. Shifting a 64-bit integer by 64 or more is undefined behaviour in C++, not a no-op — in practice x86 masks the count to 6 bits, so mask << 64 becomes mask << 0 and the value silently doubles instead of being left alone.

Variables 6 and above are already replicated by the std::fill that follows, so the loop only ever needs to cover the within-word variables.

Reach

This is hit on every if -K k -Z n invocation with k > 6, i.e. on every use of the delay-driven ACD path with cuts wider than a word. Found with UBSan.

Fix

Bound the loop at 6, with a comment recording why the tail is unnecessary rather than merely harmless.

Testing

Built clean. if -K 10 -Z 6 on cavlc is unchanged (nd = 121, lev = 4).

Related: #539 (uninitialised bestPerm read in the same file) and #540 (lutpack assertion), both found while measuring this decomposer across the EPFL suite.

ac_decomposition_impl::local_extend_to() replicates a truth table that really
depends on `real_num_vars` variables across the full `num_vars`-variable static
truth table.  For real_num_vars < 6 it does so by folding the first word:

    for ( auto i = real_num_vars; i < num_vars; ++i )
      mask |= ( mask << ( 1 << i ) );

Once i reaches 6 the shift distance is 1 << 6 == 64, which is at least the width
of the 64-bit operand, so the shift has undefined behaviour.  This is reached
whenever the cut being decomposed has more than six variables, i.e. in every
ordinary use of `if -K k -Z n` with k > 6; UBSan reports

  ac_decomposition.hpp: runtime error: shift exponent 64 is too large for
  64-bit type 'long unsigned int'

on, for example, `read adder.aig; strash; dch -f; if -K 11 -Z 6 -C 12`.

On x86 the shift is taken modulo 64 and the iteration happens to be a no-op, so
the observable behaviour today is correct, but that is not guaranteed by the
language and other targets shift in a saturating or unspecified way.

Variables 6 and above do not need the fold at all: the subsequent
std::fill() over the whole block array already replicates the word across every
block.  Clamp the loop to the variables that live inside one word.  No
behavioural change on x86.
@alanminko

Copy link
Copy Markdown
Contributor

Marcel, thank you for the fixes! I will review them and merge soon.

@marcelwa

marcelwa commented Aug 9, 2026

Copy link
Copy Markdown
Author

You're welcome. Let me know if you have any questions or need any further information I can provide 🙂

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