Vendor bitn v0.6.1 from the release artifact - #6
Conversation
Picks up the _compat.lua implementation-selection fix: the native-operator branch was chosen by testing whether `a & b` parses, which LuaJIT rolling releases from 2026 accept while returning signed 32-bit values from the bit library. That skipped the to_unsigned() normalisation those hosts need and made arshift return wrong values, not just differently-signed ones. Artifact taken from the v0.6.1 release, sha256 684ba2c5ab905f5aaceff9fdd4512eed5c327926ca13730dec3c34760cbd9dae. Full suite passes: 15/15 modules, including the 27 OpenSSL feature-gating tests.
There was a problem hiding this comment.
Verified the vendor bump end to end, including building a LuaJIT with the backported bit operators to reproduce the bug and confirm the fix. Approving.
Provenance: byte-exact
vendor/bitn.lua at 099b705 is byte-identical to the bitn.lua asset on the finitelabs/lua-bitn v0.6.1 release (asset 506709615, 108625 bytes), and its sha256 matches the one in the PR body exactly:
684ba2c5ab905f5aaceff9fdd4512eed5c327926ca13730dec3c34760cbd9dae
diff against the downloaded asset is empty, so nothing in the diff is hand-edited. The three non-version hunks (value probe, fresh bit_ok/bit_module locals, the local expected narrowing in selftest) all came from upstream.
The premise is real, and I can date it precisely
LuaJIT backported the v3.0 syntax extensions to v2.1 in a2ce8114 on 2026-07-20, including the bit operators ~ & | ~ << >> ~>>. Upstream doc/extensions.html pins the cutoff:
Bytecode that uses a bit operator can only be loaded by LuaJIT 2.1.1784535649 or higher.
The presence of a separate ~>> arithmetic-shift operator is itself the tell that these are 32-bit bit-library semantics rather than 5.3 integer semantics.
I built v2.1 HEAD (1edc3e5, LuaJIT 2.1.1785763465) and measured it:
a & b (0xFFFFFFFF, 0xFFFFFFFF) = -1 -- identical to bit.band
0x80000000 >> 1 = 1073741824
OLD probe (fn ~= nil) selects native: true <-- the bug
NEW probe (value check) : false <-- correct
Then ran both vendored revisions on that runtime:
| v0.6.0 (base) | v0.6.1 (head) | |
|---|---|---|
arshift(0x80000000, 1) |
0x40000000 |
0xC0000000 |
band(0xFFFFFFFF, 0xFFFFFFFF) |
-1 |
0xFFFFFFFF |
bnot(0) |
-1 |
0xFFFFFFFF |
bit32.selftest() / bit64.selftest() |
fails | passes |
So the 0x40000000 figure in the description reproduces exactly. The mechanism is worth spelling out: _compat.arshift masks with native_band(a, MASK32), which on this host yields -2147483648, so the a >= 0x80000000 sign test is false and the sign extension is skipped entirely.
Control4's 2.1.1700206165 is dated 2023-11-17, well below the 1784535649 cutoff, so the controller reading is right.
One correction: this is not merely forward-looking
The description says the controller build predating the syntax change is "the only reason we had no exposure yet." That understates it. CI on main at ce3e452 installed LuaJIT 2.1.1785763465 (job 93147638691), which is past the cutoff, and passed green while shipping the broken v0.6.0. The bug is live on a runtime the project already tests against today.
The reason nothing went red: src/crypto/ calls almost exclusively the raw_* entry points (raw_bxor, raw_band, raw_rshift, raw_add, raw_ror, raw_bor, raw_lshift, raw_bnot, raw_rol), and the signed 32-bit representation stays self-consistent through chained xor/and/or, with raw_rshift re-masking before shifting and the final byte extraction landing on correct values. The wrapped ops that are visibly wrong are the ones lua-crypto never calls. I confirmed this directly: the full suite is 15/15 on 2.1.1785763465 at both base and head.
That means "full test suite passes" is true but carries no signal here, in either direction. It is not evidence the bug was absent, and it will not be evidence if this regresses again.
Also, the impact is broader than arshift. On v0.6.0 the bit32 selftest fails on mask, band, bor and more, all returning sign-extended negatives. arshift is just the one that is wrong in a way a mask cannot launder.
Suggested follow-up (not blocking)
The CI matrix already floats luajit-2.1 to the latest rolling build, so it lands on affected runtimes automatically, but it cannot see this class of breakage. Gating bitn.bit32.selftest() and bitn.bit64.selftest() in the build would have caught it on main before this PR existed, and it is a couple of lines. Worth a ticket.
On the change itself
The value probe is the right shape. Testing fn(0xFFFFFFFF, 0xFFFFFFFF) == 0xFFFFFFFF compares a literal against a computed value in the same numeric domain, so it needs no runtime allowlist, and falling through to the bit library path is the conservative direction. Verified no regression on genuine 5.3+ semantics: Lua 5.5.0 answers 4294967295 and still selects the native branch.
The bit_ok/bit_module rename is a real hygiene fix rather than cosmetics, since the old code reassigned the same ok/result locals used by the native probe. I checked that nothing reads those locals after line 49, so there is no behaviour change, only the removal of a trap. The local expected hunk is a pure narrowing refactor with test unmutated in the loop.
What I did not verify
- No hardware controller run. The Control4 claim rests on the version number in the description plus the upstream cutoff, both of which check out, but I did not query a controller myself.
- I did not audit the rest of the 108KB vendored file beyond the diff, on the basis that it is byte-identical to a signed release artifact.
- CI facts above are cited by immutable job ID; I made no claim about current check state.
Vendors bitn v0.6.1, taken from the GitHub release asset rather than a local
make build.The bump carries a real correctness fix, not just a version string.
_compat.luaselected its native-operator implementation by testing whethera & bparses. LuaJIT rolling releases from 2026 accept that syntax but return a signed 32-bit result from thebitlibrary, so the branch was taken on a runtime whose values need theto_unsigned()normalisation that branch skips.arshiftgave plainly wrong answers on such a host, not merely sign-extended ones:arshift(0x80000000, 1)returned0x40000000instead of0xC0000000.v0.6.1 tests the value rather than the runtime, so it stays correct for whatever grows the syntax next.
Control4 controllers run LuaJIT (verified on a dev controller:
jitis present, 2.1.1700206165), so this is not hypothetical for us. That particular build predates the syntax change, which is the only reason we had no exposure yet.Full test suite passes against the bumped vendor.