From c31169b7592622b3b8aad76c180bf259b3631002 Mon Sep 17 00:00:00 2001 From: Pix3lPirat3 Date: Sun, 20 Sep 2026 17:36:50 -0700 Subject: [PATCH 1/2] Write 32-bit bitflags unsigned so bit 31 does not crash Building a bitflags value with |= is signed in JS, so a flag on bit 31 makes the accumulator negative and the underlying u32 writer throws "value out of range". Coerce the value to unsigned (>>> 0) before writing on both the interpreted and compiled paths. Fixes the update_abilities AbilitySet write crash (bedrock-protocol#781). Adds a bit-31 round-trip test on both paths. --- src/datatypes/compiler-utils.js | 2 ++ src/datatypes/utils.js | 2 ++ test/misc.js | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/src/datatypes/compiler-utils.js b/src/datatypes/compiler-utils.js index d60eda5..2022571 100644 --- a/src/datatypes/compiler-utils.js +++ b/src/datatypes/compiler-utils.js @@ -154,6 +154,7 @@ let val = value._value ${big ? '|| 0n' : ''} for (const key in flags) { if (value[key]) val |= flags[key] } +${big ? '' : 'val = val >>> 0 // keep 32-bit values unsigned: |= is signed in JS, so bit 31 makes val negative and the writer rejects it'} return (ctx.${type})(val, buffer, offset) `.trim()) }], @@ -208,6 +209,7 @@ let val = value._value ${big ? '|| 0n' : ''} for (const key in flags) { if (value[key]) val |= flags[key] } +${big ? '' : 'val = val >>> 0 // keep 32-bit values unsigned (see above)'} return (ctx.${type})(val) `.trim()) }], diff --git a/src/datatypes/utils.js b/src/datatypes/utils.js index 2f1722b..84e0822 100644 --- a/src/datatypes/utils.js +++ b/src/datatypes/utils.js @@ -257,6 +257,8 @@ function writeBitflags (value, buffer, offset, { type, flags, shift, big }, root for (const key in f) { if (value[key]) val |= f[key] } + // keep 32-bit values unsigned: |= is signed in JS, so bit 31 makes val negative and the writer rejects it + if (!big) val = val >>> 0 return this.write(val, buffer, offset, type, rootNode) } diff --git a/test/misc.js b/test/misc.js index fc726e9..f190e85 100644 --- a/test/misc.js +++ b/test/misc.js @@ -26,6 +26,29 @@ describe('mapper', () => { } }) +describe('bitflags', () => { + // A 32-bit bitflags whose top flag is bit 31. `|=` is signed in JS, so building the value makes it negative; + // the writer must treat it as unsigned or writeUInt32LE rejects it (regression for a bit-31 write crash). + const flags = Array.from({ length: 32 }, (_, i) => (i === 31 ? 'topbit' : 'f' + i)) + const type = ['bitflags', { type: 'lu32', flags }] + const proto = new ProtoDef() + proto.addType('flags32', type) + const compiler = new ProtoDefCompiler() + compiler.addTypesToCompile({ flags32: type }) + const compiled = compiler.compileProtoDefSync() + + for (const [label, p] of [['interpreted', proto], ['compiled', compiled]]) { + it(`round-trips a value with bit 31 set (${label})`, () => { + const buf = p.createPacketBuffer('flags32', { topbit: true, f0: true }) + assert.deepStrictEqual(buf, Buffer.from([0x01, 0x00, 0x00, 0x80])) + const back = p.parsePacketBuffer('flags32', buf).data + assert.strictEqual(back.topbit, true) + assert.strictEqual(back.f0, true) + assert.strictEqual(back.f1, false) + }) + } +}) + describe('FullPacketParser', () => { const packet = ['container', [{ name: 'a', type: 'i32' }]] const proto = new ProtoDef() From d2a8b55ec7308fa0974897c6c1446a6d56dbbffe Mon Sep 17 00:00:00 2001 From: Pix3lPirat3 Date: Tue, 22 Sep 2026 22:55:10 -0700 Subject: [PATCH 2/2] Preserve signed underlying types in bitflags unsigned write --- src/datatypes/compiler-utils.js | 4 ++-- src/datatypes/utils.js | 6 ++++-- test/misc.js | 22 ++++++++++++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/datatypes/compiler-utils.js b/src/datatypes/compiler-utils.js index 2022571..6625a51 100644 --- a/src/datatypes/compiler-utils.js +++ b/src/datatypes/compiler-utils.js @@ -154,7 +154,7 @@ let val = value._value ${big ? '|| 0n' : ''} for (const key in flags) { if (value[key]) val |= flags[key] } -${big ? '' : 'val = val >>> 0 // keep 32-bit values unsigned: |= is signed in JS, so bit 31 makes val negative and the writer rejects it'} +${(!big && /^l?u/.test(type)) ? 'val = val >>> 0 // unsigned underlying type: keep bit 31 from making val negative and rejected' : ''} return (ctx.${type})(val, buffer, offset) `.trim()) }], @@ -209,7 +209,7 @@ let val = value._value ${big ? '|| 0n' : ''} for (const key in flags) { if (value[key]) val |= flags[key] } -${big ? '' : 'val = val >>> 0 // keep 32-bit values unsigned (see above)'} +${(!big && /^l?u/.test(type)) ? 'val = val >>> 0 // unsigned underlying type (see above)' : ''} return (ctx.${type})(val) `.trim()) }], diff --git a/src/datatypes/utils.js b/src/datatypes/utils.js index 84e0822..40adce8 100644 --- a/src/datatypes/utils.js +++ b/src/datatypes/utils.js @@ -257,8 +257,10 @@ function writeBitflags (value, buffer, offset, { type, flags, shift, big }, root for (const key in f) { if (value[key]) val |= f[key] } - // keep 32-bit values unsigned: |= is signed in JS, so bit 31 makes val negative and the writer rejects it - if (!big) val = val >>> 0 + // Coerce to unsigned only for unsigned underlying types: |= is signed in JS, so bit 31 makes val negative and the + // unsigned writer rejects it. Signed types (i8/i16/i32/li32) keep the |= result, which is already the correct value; + // forcing them unsigned would push a valid negative out of the signed writer's range. + if (!big && /^l?u/.test(type)) val = val >>> 0 return this.write(val, buffer, offset, type, rootNode) } diff --git a/test/misc.js b/test/misc.js index f190e85..6877ba4 100644 --- a/test/misc.js +++ b/test/misc.js @@ -49,6 +49,28 @@ describe('bitflags', () => { } }) +describe('bitflags with a signed underlying type', () => { + // Signed underlying type with bit 31 set: reading 0xffffffff as i32 yields -1. The unsigned coercion must NOT apply + // here, or writing the decoded value pushes -1 to 4294967295 and the signed writer rejects it (a regression the + // unsigned bit-31 fix introduced). The |= result is already the correct signed value. + const type = ['bitflags', { type: 'i32', flags: { top: 31 }, shift: true }] + const proto = new ProtoDef() + proto.addType('sflags', type) + const compiler = new ProtoDefCompiler() + compiler.addTypesToCompile({ sflags: type }) + const compiled = compiler.compileProtoDefSync() + + for (const [label, p] of [['interpreted', proto], ['compiled', compiled]]) { + it(`round-trips a signed value with bit 31 set (${label})`, () => { + const buf = Buffer.from([0xff, 0xff, 0xff, 0xff]) // i32 -1, top bit set + const obj = p.parsePacketBuffer('sflags', buf).data + assert.strictEqual(obj.top, true) + const back = p.createPacketBuffer('sflags', obj) // must not throw and must reproduce the original bytes + assert.deepStrictEqual(back, buf) + }) + } +}) + describe('FullPacketParser', () => { const packet = ['container', [{ name: 'a', type: 'i32' }]] const proto = new ProtoDef()