Unit
LibCast.asAddressesArray(uint256[] memory) — src/LibCast.sol:20-24.
Intent oracle
The LibCast NatSpec, which is explicit that validity is not checked:
The cast does NOT (can't) check that the input is a valid output ... It is the
calling context that MUST ensure the validity of the data, the cast will merely
retype the data in place, generally without additional checks.
and the README:
a cast moves between Solidity types without changing the binary data.
So the absence of masking is intended. This issue is not a claim that the cast
should mask. It is about a consequence of that intended behaviour that is
nowhere written down, and about who is in a position to notice it.
The property in question
Solidity's memory convention is that a value narrower than 256 bits is held
zero-extended in memory. asAddressesArray retypes a uint256[] to address[]
without masking, so for any input word >= 2**160 the resulting address[]
contains memory words whose upper 96 bits are set. That array is well-typed to
the compiler but violates the representation invariant the compiler's own
convention establishes.
The question for triage is whether the resulting hazard is adequately covered by
"the calling context MUST ensure the validity of the data", given who the callers
are in this org.
Verified repro
test/Adversarial.t.sol (run against a4692fddbb95a0018cec42969ef6c7fa1b0cf86c,
not proposed for merge):
uint256[] memory us = new uint256[](1);
us[0] = type(uint256).max;
address[] memory a = LibCast.asAddressesArray(us);
Measured observables, all at solc 0.8.25 with the optimizer on:
| Consumer |
Word observed |
raw mload of the element |
0xffff...ffff (all 256 bits set) |
a[0] loaded into a stack slot |
0xffff...ffff (not cleaned) |
a[0] == address(type(uint160).max) |
true (cleaned) |
abi.encode(a) |
cleaned to 160 bits |
abi.encodePacked(a) |
cleaned to 160 bits |
keccak256(abi.encodePacked(a[0])) |
equal to the clean equivalent |
assignment into a storage address[] |
cleaned to 160 bits |
use as a mapping(address => ...) key |
cleaned; reads back under the clean address |
normal address[] element write |
cleaned to 160 bits |
external call taking address[] calldata |
cleaned; no revert at the ABI boundary |
mstore(ptr, a[0]) inside assembly |
0xffff...ffff (not cleaned) |
Neutral triage framing
Why this may be entirely intended. Every high-level Solidity consumer tested
cleans the value. There is no silent state corruption, no wrong storage write, no
ABI-boundary revert, and no mismatch between == and keccak256. The NatSpec
already assigns validity to the caller. On this evidence the library is behaving
exactly as documented and a reasonable reviewer may close this as by design.
Why it may still be worth acting on. The two rows that do not clean are
both assembly reads, and assembly is the normal consumer idiom across the rain
libraries. A downstream library that takes an address[] memory and reads
elements with mload — entirely reasonable, since it is entitled to assume
Solidity's memory convention holds — will observe a 256-bit word where it expects
an address, and will do so silently. The NatSpec's warning is generic ("ensure
the validity of the data") and does not name this specific hazard or say which
consumers are affected.
Cheap options, if any action is wanted. (a) Document the hazard on
asAddressesArray specifically, naming assembly consumers as the ones that see
the dirty word. (b) Additionally offer a checked or masking variant alongside the
unchecked cast, leaving the current function untouched. (c) Close as by design.
This is filed as a hazard-surface and documentation finding, not as a
correctness bug: no case was found in which the library produces a wrong result
or corrupts state.
Notes
Found during an adversarial mutation test run over the whole repo. The related
coverage gap — the test suite could not distinguish the documented no-masking
behaviour from its opposite, because the round trip tests compared the input
buffer against itself — is fixed separately in #18.
Unit
LibCast.asAddressesArray(uint256[] memory)—src/LibCast.sol:20-24.Intent oracle
The
LibCastNatSpec, which is explicit that validity is not checked:and the README:
So the absence of masking is intended. This issue is not a claim that the cast
should mask. It is about a consequence of that intended behaviour that is
nowhere written down, and about who is in a position to notice it.
The property in question
Solidity's memory convention is that a value narrower than 256 bits is held
zero-extended in memory.
asAddressesArrayretypes auint256[]toaddress[]without masking, so for any input word >= 2**160 the resulting
address[]contains memory words whose upper 96 bits are set. That array is well-typed to
the compiler but violates the representation invariant the compiler's own
convention establishes.
The question for triage is whether the resulting hazard is adequately covered by
"the calling context MUST ensure the validity of the data", given who the callers
are in this org.
Verified repro
test/Adversarial.t.sol(run againsta4692fddbb95a0018cec42969ef6c7fa1b0cf86c,not proposed for merge):
Measured observables, all at solc 0.8.25 with the optimizer on:
mloadof the element0xffff...ffff(all 256 bits set)a[0]loaded into a stack slot0xffff...ffff(not cleaned)a[0] == address(type(uint160).max)true(cleaned)abi.encode(a)abi.encodePacked(a)keccak256(abi.encodePacked(a[0]))address[]mapping(address => ...)keyaddress[]element writeaddress[] calldatamstore(ptr, a[0])inside assembly0xffff...ffff(not cleaned)Neutral triage framing
Why this may be entirely intended. Every high-level Solidity consumer tested
cleans the value. There is no silent state corruption, no wrong storage write, no
ABI-boundary revert, and no mismatch between
==andkeccak256. The NatSpecalready assigns validity to the caller. On this evidence the library is behaving
exactly as documented and a reasonable reviewer may close this as by design.
Why it may still be worth acting on. The two rows that do not clean are
both assembly reads, and assembly is the normal consumer idiom across the rain
libraries. A downstream library that takes an
address[] memoryand readselements with
mload— entirely reasonable, since it is entitled to assumeSolidity's memory convention holds — will observe a 256-bit word where it expects
an address, and will do so silently. The NatSpec's warning is generic ("ensure
the validity of the data") and does not name this specific hazard or say which
consumers are affected.
Cheap options, if any action is wanted. (a) Document the hazard on
asAddressesArrayspecifically, naming assembly consumers as the ones that seethe dirty word. (b) Additionally offer a checked or masking variant alongside the
unchecked cast, leaving the current function untouched. (c) Close as by design.
This is filed as a hazard-surface and documentation finding, not as a
correctness bug: no case was found in which the library produces a wrong result
or corrupts state.
Notes
Found during an adversarial mutation test run over the whole repo. The related
coverage gap — the test suite could not distinguish the documented no-masking
behaviour from its opposite, because the round trip tests compared the input
buffer against itself — is fixed separately in #18.