This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git
The following commit(s) were added to refs/heads/master by this push:
new 8dcf60003 BitField long-mask fields silently mis-answer int/short/byte
accessors: shifts masked mod 32, and sign extension makes above-bit-31 flags
read as set on any negative holder (f019).
8dcf60003 is described below
commit 8dcf60003f43df55003f901026ba15f6b6edea74
Author: Gary Gregory <[email protected]>
AuthorDate: Sat Sep 5 14:32:35 2026 -0400
BitField long-mask fields silently mis-answer int/short/byte accessors:
shifts masked mod 32, and sign extension makes above-bit-31 flags read
as set on any negative holder (f019).
---
src/changes/changes.xml | 1 +
.../java/org/apache/commons/lang3/BitField.java | 54 +++++++++++++++++++---
.../org/apache/commons/lang3/BitFieldLongTest.java | 32 +++++++------
.../org/apache/commons/lang3/BitFieldTest.java | 38 +++++++++++++++
4 files changed, 105 insertions(+), 20 deletions(-)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index f2bc1963f..82bc504eb 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -268,6 +268,7 @@ java.lang.NullPointerException: Cannot invoke
<action type="fix" dev="ggregory" due-to="Gary
Gregory">Match Javadoc to code in SystemProperties.getProperty() and
SystemUtils.getEnvironmentVariable() and update inline comments (f016).</action>
<action type="fix" dev="ggregory" due-to="Gary
Gregory">Fix LockingVisitors Javadoc to match code (f017).</action>
<action type="fix" dev="ggregory" due-to="Gary
Gregory">DoubleRange/Range accept NaN endpoints, silently producing a
half-unbounded range, contains()/fit() fail open for every value above the
minimum (f018).</action>
+ <action type="fix" dev="ggregory" due-to="Gary
Gregory">BitField long-mask fields silently mis-answer int/short/byte
accessors: shifts masked mod 32, and sign extension makes above-bit-31 flags
read as set on any negative holder (f019).</action>
<!-- ADD -->
<action type="add" dev="ggregory" due-to="Gary
Gregory">Add JavaVersion.JAVA_27.</action>
<action type="add" dev="ggregory" due-to="Gary
Gregory">Add SystemUtils.IS_JAVA_27.</action>
diff --git a/src/main/java/org/apache/commons/lang3/BitField.java
b/src/main/java/org/apache/commons/lang3/BitField.java
index 177a84d3f..28adb9ae9 100644
--- a/src/main/java/org/apache/commons/lang3/BitField.java
+++ b/src/main/java/org/apache/commons/lang3/BitField.java
@@ -88,6 +88,11 @@ public BitField(final int mask) {
/**
* Creates a BitField instance.
+ * <p>
+ * If any bit above bit 31 is set in the mask, the resulting field can
only be used with the {@code long} holder accessors; the {@code int}, {@code
short}
+ * and {@code byte} holder accessors throw {@link IllegalStateException}
for such a field, because those holder types cannot contain the masked bits and
+ * would otherwise silently answer wrongly (shift counts are truncated mod
32 and negative holders are sign-extended into bits 32-63).
+ * </p>
*
* @param mask The mask specifying which bits apply to this BitField. Bits
that are set in this mask are the bits that this BitField operates on.
* @since 3.21.0
@@ -102,9 +107,10 @@ public BitField(final long mask) {
*
* @param holder The int data containing the bits we're interested in.
* @return The value of holder with the specified bits cleared (set to
{@code 0}).
+ * @throws IllegalStateException if this field's mask has bits set above
bit 31 (only possible via {@link #BitField(long)}) and so cannot be represented
in this holder type.
*/
public int clear(final int holder) {
- return (int) (holder & ~mask);
+ return (int) (holder & ~intMask());
}
/**
@@ -123,6 +129,7 @@ public long clear(final long holder) {
*
* @param holder The byte data containing the bits we're interested in.
* @return The value of holder with the specified bits cleared (set to
{@code 0}).
+ * @throws IllegalStateException if this field's mask has bits set above
bit 31 (only possible via {@link #BitField(long)}) and so cannot be represented
in this holder type.
*/
public byte clearByte(final byte holder) {
return (byte) clear(holder);
@@ -133,6 +140,7 @@ public byte clearByte(final byte holder) {
*
* @param holder The short data containing the bits we're interested in.
* @return The value of holder with the specified bits cleared (set to
{@code 0}).
+ * @throws IllegalStateException if this field's mask has bits set above
bit 31 (only possible via {@link #BitField(long)}) and so cannot be represented
in this holder type.
*/
public short clearShort(final short holder) {
return (short) clear(holder);
@@ -143,9 +151,10 @@ public short clearShort(final short holder) {
*
* @param holder The int data containing the bits we're interested in.
* @return The selected bits.
+ * @throws IllegalStateException if this field's mask has bits set above
bit 31 (only possible via {@link #BitField(long)}) and so cannot be represented
in this holder type.
*/
public int getRawValue(final int holder) {
- return (int) (holder & mask);
+ return (int) (holder & intMask());
}
/**
@@ -164,6 +173,7 @@ public long getRawValue(final long holder) {
*
* @param holder The short data containing the bits we're interested in.
* @return The selected bits.
+ * @throws IllegalStateException if this field's mask has bits set above
bit 31 (only possible via {@link #BitField(long)}) and so cannot be represented
in this holder type.
*/
public short getShortRawValue(final short holder) {
return (short) getRawValue(holder);
@@ -179,6 +189,7 @@ public short getShortRawValue(final short holder) {
* @param holder The short data containing the bits we're interested in.
* @return The selected bits, shifted right appropriately.
* @see #setShortValue(short,short)
+ * @throws IllegalStateException if this field's mask has bits set above
bit 31 (only possible via {@link #BitField(long)}) and so cannot be represented
in this holder type.
*/
public short getShortValue(final short holder) {
return (short) getValue(holder);
@@ -194,6 +205,7 @@ public short getShortValue(final short holder) {
* @param holder The int data containing the bits we're interested in.
* @return The selected bits, shifted right appropriately.
* @see #setValue(int,int)
+ * @throws IllegalStateException if this field's mask has bits set above
bit 31 (only possible via {@link #BitField(long)}) and so cannot be represented
in this holder type.
*/
public int getValue(final int holder) {
return getRawValue(holder) >>> shiftCount;
@@ -215,6 +227,24 @@ public long getValue(final long holder) {
return getRawValue(holder) >>> shiftCount;
}
+ /**
+ * Verifies that this field's mask fits in an {@code int} holder before an
{@code int}, {@code short} or {@code byte} accessor uses it.
+ * <p>
+ * Without this check, a mask with bits above bit 31 makes the narrow
accessors silently wrong: the {@code int} shift count is truncated mod 32, and a
+ * negative narrow holder is sign-extended to 64 bits before the {@code
long} mask is applied, reporting above-bit-31 flags as set even though the
holder
+ * type cannot contain them.
+ * </p>
+ *
+ * @return the mask, guaranteed to fit in 32 bits.
+ * @throws IllegalStateException if the mask has bits set above bit 31.
+ */
+ private long intMask() {
+ if (mask >>> Integer.SIZE != 0) {
+ throw new IllegalStateException("BitField mask 0x" +
Long.toHexString(mask) + " exceeds 32 bits; use the long accessors for this
field.");
+ }
+ return mask;
+ }
+
/**
* Tests whether all of the bits are set or not.
* <p>
@@ -223,9 +253,11 @@ public long getValue(final long holder) {
*
* @param holder The int data containing the bits we're interested in.
* @return {@code true} if all of the bits are set, else {@code false}.
+ * @throws IllegalStateException if this field's mask has bits set above
bit 31 (only possible via {@link #BitField(long)}) and so cannot be represented
in this holder type.
*/
public boolean isAllSet(final int holder) {
- return (holder & mask) == mask;
+ final long intMask = intMask();
+ return (holder & intMask) == intMask;
}
/**
@@ -251,9 +283,10 @@ public boolean isAllSet(final long holder) {
*
* @param holder The int data containing the bits we're interested in
* @return {@code true} if any of the bits are set, else {@code false}
+ * @throws IllegalStateException if this field's mask has bits set above
bit 31 (only possible via {@link #BitField(long)}) and so cannot be represented
in this holder type.
*/
public boolean isSet(final int holder) {
- return (holder & mask) != 0;
+ return (holder & intMask()) != 0;
}
/**
@@ -276,9 +309,10 @@ public boolean isSet(final long holder) {
*
* @param holder The int data containing the bits we're interested in.
* @return The value of holder with the specified bits set to {@code 1}.
+ * @throws IllegalStateException if this field's mask has bits set above
bit 31 (only possible via {@link #BitField(long)}) and so cannot be represented
in this holder type.
*/
public int set(final int holder) {
- return (int) (holder | mask);
+ return (int) (holder | intMask());
}
/**
@@ -298,6 +332,7 @@ public long set(final long holder) {
* @param holder The int data containing the bits we're interested in.
* @param flag indicating whether to set or clear the bits.
* @return The value of holder with the specified bits set or cleared.
+ * @throws IllegalStateException if this field's mask has bits set above
bit 31 (only possible via {@link #BitField(long)}) and so cannot be represented
in this holder type.
*/
public int setBoolean(final int holder, final boolean flag) {
return flag ? set(holder) : clear(holder);
@@ -320,6 +355,7 @@ public long setBoolean(final long holder, final boolean
flag) {
*
* @param holder The byte data containing the bits we're interested in
* @return The value of holder with the specified bits set to {@code 1}
+ * @throws IllegalStateException if this field's mask has bits set above
bit 31 (only possible via {@link #BitField(long)}) and so cannot be represented
in this holder type.
*/
public byte setByte(final byte holder) {
return (byte) set(holder);
@@ -331,6 +367,7 @@ public byte setByte(final byte holder) {
* @param holder The byte data containing the bits we're interested in.
* @param flag indicating whether to set or clear the bits.
* @return The value of holder with the specified bits set or cleared.
+ * @throws IllegalStateException if this field's mask has bits set above
bit 31 (only possible via {@link #BitField(long)}) and so cannot be represented
in this holder type.
*/
public byte setByteBoolean(final byte holder, final boolean flag) {
return flag ? setByte(holder) : clearByte(holder);
@@ -341,6 +378,7 @@ public byte setByteBoolean(final byte holder, final boolean
flag) {
*
* @param holder The short data containing the bits we're interested in.
* @return The value of holder with the specified bits set to {@code 1}.
+ * @throws IllegalStateException if this field's mask has bits set above
bit 31 (only possible via {@link #BitField(long)}) and so cannot be represented
in this holder type.
*/
public short setShort(final short holder) {
return (short) set(holder);
@@ -352,6 +390,7 @@ public short setShort(final short holder) {
* @param holder The short data containing the bits we're interested in.
* @param flag indicating whether to set or clear the bits.
* @return The value of holder with the specified bits set or cleared.
+ * @throws IllegalStateException if this field's mask has bits set above
bit 31 (only possible via {@link #BitField(long)}) and so cannot be represented
in this holder type.
*/
public short setShortBoolean(final short holder, final boolean flag) {
return flag ? setShort(holder) : clearShort(holder);
@@ -364,6 +403,7 @@ public short setShortBoolean(final short holder, final
boolean flag) {
* @param value The new value for the specified bits
* @return The value of holder with the bits from the value parameter
replacing the old bits
* @see #getShortValue(short)
+ * @throws IllegalStateException if this field's mask has bits set above
bit 31 (only possible via {@link #BitField(long)}) and so cannot be represented
in this holder type.
*/
public short setShortValue(final short holder, final short value) {
return (short) setValue(holder, value);
@@ -376,9 +416,11 @@ public short setShortValue(final short holder, final short
value) {
* @param value The new value for the specified bits.
* @return The value of holder with the bits from the value parameter
replacing the old bits.
* @see #getValue(int)
+ * @throws IllegalStateException if this field's mask has bits set above
bit 31 (only possible via {@link #BitField(long)}) and so cannot be represented
in this holder type.
*/
public int setValue(final int holder, final int value) {
- return (int) (holder & ~mask | value << shiftCount & mask);
+ final long intMask = intMask();
+ return (int) (holder & ~intMask | value << shiftCount & intMask);
}
/**
diff --git a/src/test/java/org/apache/commons/lang3/BitFieldLongTest.java
b/src/test/java/org/apache/commons/lang3/BitFieldLongTest.java
index ad2c19a24..442c7dffe 100644
--- a/src/test/java/org/apache/commons/lang3/BitFieldLongTest.java
+++ b/src/test/java/org/apache/commons/lang3/BitFieldLongTest.java
@@ -19,6 +19,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
@@ -68,8 +69,9 @@ void testClearInt() {
assertEquals(BF_MULTI.clear(-1), 0xFFFF_C07F);
assertEquals(BF_SINGLE.clear(-1), 0xFFFF_BFFF);
assertEquals(BF_ZERO.clear(-1), 0xFFFF_FFFF);
- assertEquals(BF_MULTI_L.clear(-1), 0xFFFF_FFFF);
- assertEquals(BF_SINGLE_L.clear(-1), 0xFFFF_FFFF);
+ // Above-bit-31 masks fail loudly on int holders instead of silently
no-op'ing (see BitField#intMask()).
+ assertThrows(IllegalStateException.class, () -> BF_MULTI_L.clear(-1));
+ assertThrows(IllegalStateException.class, () -> BF_SINGLE_L.clear(-1));
assertEquals(BF_ZERO_L.clear(-1), 0xFFFF_FFFF);
}
@@ -94,8 +96,9 @@ void testClearShort() {
assertEquals(BF_MULTI.clearShort((short) - 1), (short) 0xC07F);
assertEquals(BF_SINGLE.clearShort((short) - 1), (short) 0xBFFF);
assertEquals(BF_ZERO.clearShort((short) -1), (short) 0xFFFF);
- assertEquals(BF_MULTI_L.clearShort((short) - 1), (short) 0xFFFF);
- assertEquals(BF_SINGLE_L.clearShort((short) - 1), (short) 0xFFFF);
+ // Above-bit-31 masks fail loudly on short holders instead of silently
no-op'ing (see BitField#intMask()).
+ assertThrows(IllegalStateException.class, () ->
BF_MULTI_L.clearShort((short) -1));
+ assertThrows(IllegalStateException.class, () ->
BF_SINGLE_L.clearShort((short) -1));
assertEquals(BF_ZERO_L.clearShort((short) -1), (short) 0xFFFF);
}
@@ -121,11 +124,12 @@ void testGetRawValue() {
assertEquals(BF_SINGLE.getRawValue(0), 0);
assertEquals(BF_ZERO.getRawValue(-1), 0);
assertEquals(BF_ZERO.getRawValue(0), 0);
- // mask > max int and int input
- assertEquals(BF_MULTI_L.getRawValue(-1), 0);
- assertEquals(BF_MULTI_L.getRawValue(0), 0);
- assertEquals(BF_SINGLE_L.getRawValue(-1), 0);
- assertEquals(BF_SINGLE_L.getRawValue(0), 0);
+ // mask > max int and int input: fails loudly (the old silent 0 was
incoherent with the
+ // sign-extended answers the same mixture produced in isSet/isAllSet).
+ assertThrows(IllegalStateException.class, () ->
BF_MULTI_L.getRawValue(-1));
+ assertThrows(IllegalStateException.class, () ->
BF_MULTI_L.getRawValue(0));
+ assertThrows(IllegalStateException.class, () ->
BF_SINGLE_L.getRawValue(-1));
+ assertThrows(IllegalStateException.class, () ->
BF_SINGLE_L.getRawValue(0));
assertEquals(BF_ZERO_L.getRawValue(-1), 0);
assertEquals(BF_ZERO_L.getRawValue(0), 0);
// mask > max int and long input
@@ -149,11 +153,11 @@ void testGetShortRawValue() {
assertEquals(BF_SINGLE.getShortRawValue((short) 0), (short) 0);
assertEquals(BF_ZERO.getShortRawValue((short) -1), (short) 0);
assertEquals(BF_ZERO.getShortRawValue((short) 0), (short) 0);
- // mask > max int and short input
- assertEquals(BF_MULTI_L.getShortRawValue((short) - 1), (short) 0);
- assertEquals(BF_MULTI_L.getShortRawValue((short) 0), (short) 0);
- assertEquals(BF_SINGLE_L.getShortRawValue((short) - 1), (short) 0);
- assertEquals(BF_SINGLE_L.getShortRawValue((short) 0), (short) 0);
+ // mask > max int and short input: fails loudly (see testGetRawValue).
+ assertThrows(IllegalStateException.class, () ->
BF_MULTI_L.getShortRawValue((short) -1));
+ assertThrows(IllegalStateException.class, () ->
BF_MULTI_L.getShortRawValue((short) 0));
+ assertThrows(IllegalStateException.class, () ->
BF_SINGLE_L.getShortRawValue((short) -1));
+ assertThrows(IllegalStateException.class, () ->
BF_SINGLE_L.getShortRawValue((short) 0));
assertEquals(BF_ZERO_L.getShortRawValue((short) -1), (short) 0);
assertEquals(BF_ZERO_L.getShortRawValue((short) 0), (short) 0);
}
diff --git a/src/test/java/org/apache/commons/lang3/BitFieldTest.java
b/src/test/java/org/apache/commons/lang3/BitFieldTest.java
index 784acb8f5..4624d7436 100644
--- a/src/test/java/org/apache/commons/lang3/BitFieldTest.java
+++ b/src/test/java/org/apache/commons/lang3/BitFieldTest.java
@@ -19,6 +19,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
@@ -305,4 +306,41 @@ void testSetValue() {
// verify that excess bits are stripped off
assertEquals(BF_SINGLE.setValue(0x4000, 2), 0);
}
+
+ /**
+ * Tests that a mask wider than 32 bits (only constructible via {@link
BitField#BitField(long)}) makes every int/short/byte holder accessor fail loudly
+ * instead of silently answering wrongly, and that a long-constructed mask
that fits in 32 bits keeps working with the narrow accessors.
+ */
+ @Test
+ void testWideMaskRejectsNarrowHolderAccessors() {
+ final BitField wide = new BitField(0x100000000L);
+ // Before the guard, sign extension made every above-bit-31 flag read
as set on any negative int holder.
+ assertThrows(IllegalStateException.class, () -> wide.isSet(-1));
+ assertThrows(IllegalStateException.class, () -> wide.isAllSet(-1));
+ assertThrows(IllegalStateException.class, () -> wide.getValue(-1));
+ assertThrows(IllegalStateException.class, () -> wide.getRawValue(-1));
+ assertThrows(IllegalStateException.class, () ->
wide.getShortValue((short) -1));
+ assertThrows(IllegalStateException.class, () ->
wide.getShortRawValue((short) -1));
+ assertThrows(IllegalStateException.class, () -> wide.clear(-1));
+ assertThrows(IllegalStateException.class, () -> wide.clearByte((byte)
-1));
+ assertThrows(IllegalStateException.class, () ->
wide.clearShort((short) -1));
+ assertThrows(IllegalStateException.class, () -> wide.set(0));
+ assertThrows(IllegalStateException.class, () -> wide.setBoolean(0,
true));
+ assertThrows(IllegalStateException.class, () -> wide.setByte((byte)
0));
+ assertThrows(IllegalStateException.class, () ->
wide.setByteBoolean((byte) 0, true));
+ assertThrows(IllegalStateException.class, () -> wide.setShort((short)
0));
+ assertThrows(IllegalStateException.class, () ->
wide.setShortBoolean((short) 0, true));
+ assertThrows(IllegalStateException.class, () ->
wide.setShortValue((short) 0, (short) 1));
+ assertThrows(IllegalStateException.class, () -> wide.setValue(0, 1));
+ // The long accessors remain fully functional for the same field.
+ assertFalse(wide.isSet(0xFFFFFFFFL));
+ assertTrue(wide.isSet(0x100000000L));
+ assertEquals(1L, wide.getValue(0x100000000L));
+ assertEquals(0x100000000L, wide.set(0L));
+ // A long-constructed mask that fits in 32 bits keeps working with the
narrow accessors.
+ final BitField narrow = new BitField(0x80000000L);
+ assertTrue(narrow.isSet(-1));
+ assertEquals(1, narrow.getValue(-1));
+ assertEquals(0x80000000, narrow.set(0));
+ }
}