This is an automated email from the ASF dual-hosted git repository.

afs pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git


The following commit(s) were added to refs/heads/main by this push:
     new b7746f9bcd GH-4043: fix(dboe): don't poison shared buffer on block 
slice (#4051)
b7746f9bcd is described below

commit b7746f9bcd5dff37a40113672bbd284667fdf3aa
Author: Nigel Ang <[email protected]>
AuthorDate: Wed Jul 22 02:17:28 2026 +0800

    GH-4043: fix(dboe): don't poison shared buffer on block slice (#4051)
    
    * GH-4043: fix(dboe): don't poison shared buffer on block slice
    
    BlockAccessMapped.getByteBuffer sliced a block by shrinking the shared
    per-segment MappedByteBuffer's limit and restoring it afterwards, but
    the restore was not exception-safe. A throwable escaping between the
    shrink and the restore (e.g. OOME at slice allocation) left the segment
    buffer with a shrunken limit, so every later access to a higher block in
    that segment failed with "newPosition > limit" until JVM restart.
    
    The same fix is applied to the duplicated TDB1 BlockAccessMapped.
    
    Widen the diagnostic catch to RuntimeException since the absolute slice
    reports range violations as IndexOutOfBoundsException.
    
    Add TestBlockAccessMappedSegmentState
    
    * fix(dboe): slice block via single absolute slice
    
    The absolute slice(index, length) never mutates the shared segment
    buffer, so a single view object replaces the duplicate().clear() copy
    and the poisoned-limit state can no longer arise.
---
 .../jena/dboe/base/file/BlockAccessMapped.java     |  18 ++-
 .../org/apache/jena/dboe/base/file/TS_File.java    |   1 +
 .../file/TestBlockAccessMappedSegmentState.java    | 151 +++++++++++++++++++++
 .../jena/tdb1/base/file/BlockAccessMapped.java     |  18 ++-
 4 files changed, 168 insertions(+), 20 deletions(-)

diff --git 
a/jena-db/jena-dboe-base/src/main/java/org/apache/jena/dboe/base/file/BlockAccessMapped.java
 
b/jena-db/jena-dboe-base/src/main/java/org/apache/jena/dboe/base/file/BlockAccessMapped.java
index 269778a221..4a2bc9fecf 100644
--- 
a/jena-db/jena-dboe-base/src/main/java/org/apache/jena/dboe/base/file/BlockAccessMapped.java
+++ 
b/jena-db/jena-dboe-base/src/main/java/org/apache/jena/dboe/base/file/BlockAccessMapped.java
@@ -149,20 +149,18 @@ public class BlockAccessMapped extends BlockAccessBase
 
         synchronized (this) {
             try {
-                // Need to put the alloc AND the slice/reset inside a sync.
+                // Need to put the alloc AND the slice inside a sync.
                 ByteBuffer segBuffer = allocSegment(seg);
-                // Now slice the buffer to get the ByteBuffer to return
-                segBuffer.position(segOff);
-                segBuffer.limit(segOff+blockSize);
-                ByteBuffer dst = segBuffer.slice();
-
-                // And then reset limit to max for segment.
-                segBuffer.limit(segBuffer.capacity());
+                // Absolute slice: never mutates the shared segment buffer's
+                // position/limit. The old position/limit/slice/reset sequence
+                // shrank the shared buffer's limit; if anything threw before 
the
+                // reset, the limit stayed shrunk and every later access to a
+                // higher block in the segment failed ("newPosition > limit").
+                ByteBuffer dst = segBuffer.slice(segOff, blockSize);
                 // Extend block count when we allocate above end.
                 numFileBlocks = Math.max(numFileBlocks, id+1);
                 return dst;
-            } catch (IllegalArgumentException ex) {
-                // Shouldn't (ha!) happen because the second "limit" resets
+            } catch (RuntimeException ex) {
                 log.error("Id: "+id);
                 log.error("Seg="+seg);
                 log.error("Segoff="+segOff);
diff --git 
a/jena-db/jena-dboe-base/src/test/java/org/apache/jena/dboe/base/file/TS_File.java
 
b/jena-db/jena-dboe-base/src/test/java/org/apache/jena/dboe/base/file/TS_File.java
index f5f29b06d0..26cb25d47f 100644
--- 
a/jena-db/jena-dboe-base/src/test/java/org/apache/jena/dboe/base/file/TS_File.java
+++ 
b/jena-db/jena-dboe-base/src/test/java/org/apache/jena/dboe/base/file/TS_File.java
@@ -35,6 +35,7 @@ import org.junit.platform.suite.api.Suite;
     , TestBlockAccessByteArray.class
     , TestBlockAccessDirect.class
     , TestBlockAccessMapped.class
+    , TestBlockAccessMappedSegmentState.class
 
     , TestBinaryDataMem.class
     , TestBinaryDataFileWriteBufferedMem.class
diff --git 
a/jena-db/jena-dboe-base/src/test/java/org/apache/jena/dboe/base/file/TestBlockAccessMappedSegmentState.java
 
b/jena-db/jena-dboe-base/src/test/java/org/apache/jena/dboe/base/file/TestBlockAccessMappedSegmentState.java
new file mode 100644
index 0000000000..715e402618
--- /dev/null
+++ 
b/jena-db/jena-dboe-base/src/test/java/org/apache/jena/dboe/base/file/TestBlockAccessMappedSegmentState.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.apache.jena.dboe.base.file;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.lang.reflect.Field;
+import java.nio.MappedByteBuffer;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.jena.atlas.lib.FileOps;
+import org.apache.jena.dboe.ConfigTestDBOE;
+import org.apache.jena.dboe.base.block.Block;
+
+/**
+ * The shared per-segment {@link MappedByteBuffer} in {@link BlockAccessMapped}
+ * must never have its position/limit mutated by block access (GH-4043).
+ *
+ * <p>The previous implementation sliced blocks with a
+ * position/limit/slice/reset-limit sequence on the shared segment buffer. If
+ * anything threw between the limit-shrink and the reset, the segment buffer 
was
+ * left with a shrunken limit and every later access to a higher block in that
+ * segment failed with {@code IllegalArgumentException: newPosition &gt; 
limit}.
+ * The fix slices with the absolute {@code slice(index, length)}, which never
+ * touches the shared buffer's state. These tests lock in that invariant.</p>
+ */
+public class TestBlockAccessMappedSegmentState {
+
+    private static final int BlockSize = 64;
+    private static int counter = 0;
+
+    private String filename;
+    private BlockAccessMapped file;
+
+    @BeforeEach public void before() {
+        filename = ConfigTestDBOE.getTestingDir() + "/test-segment-state-" + 
(counter++);
+        FileOps.deleteSilent(filename);
+        file = new BlockAccessMapped(filename, BlockSize);
+    }
+
+    @AfterEach public void after() {
+        file.close();
+        FileOps.deleteSilent(filename);
+    }
+
+    private Block writePatternBlock(int marker) {
+        Block b = file.allocate(BlockSize);
+        for ( int i = 0; i < BlockSize; i++ )
+            b.getByteBuffer().put(i, (byte)((marker + i) & 0xFF));
+        file.write(b);
+        return b;
+    }
+
+    private MappedByteBuffer segmentBuffer(int seg) throws Exception {
+        Field f = BlockAccessMapped.class.getDeclaredField("segments");
+        f.setAccessible(true);
+        MappedByteBuffer[] segments = (MappedByteBuffer[])f.get(file);
+        return segments[seg];
+    }
+
+    @Test
+    public void sharedSegmentBufferStateUntouchedByAccess() throws Exception {
+        final int numBlocks = 20;
+        Block[] blocks = new Block[numBlocks];
+        for ( int i = 0; i < numBlocks; i++ )
+            blocks[i] = writePatternBlock(i * 7);
+
+        // Interleave reads in an order that, with the old position/limit
+        // slicing, walked the shared buffer's position and limit up and down.
+        // End on a high block: the old implementation left position at that
+        // block's segment offset.
+        for ( int i = 0; i < numBlocks; i++ )
+            file.read(blocks[i].getId().longValue());
+        file.read(blocks[0].getId().longValue());
+        file.read(blocks[numBlocks - 1].getId().longValue());
+
+        MappedByteBuffer seg0 = segmentBuffer(0);
+        assertNotNull(seg0, "segment 0 should be mapped after block access");
+
+        // A shrunken limit is the "newPosition > limit" poisoning vector.
+        assertEquals(seg0.capacity(), seg0.limit(),
+                     "shared segment buffer limit was shrunk by block access - 
"
+                     + "an exception mid-access would poison all higher blocks 
in the segment");
+        assertEquals(0, seg0.position(),
+                     "shared segment buffer position was mutated by block 
access");
+    }
+
+    @Test
+    public void blockRoundTripAfterMixedAccess() {
+        final int numBlocks = 8;
+        Block[] written = new Block[numBlocks];
+        for ( int i = 0; i < numBlocks; i++ )
+            written[i] = writePatternBlock(i * 31);
+
+        for ( int i = 0; i < numBlocks; i++ ) {
+            Block back = file.read(written[i].getId().longValue());
+            assertEquals(0, back.getByteBuffer().position(), "returned block 
slice should start at position 0");
+            assertEquals(BlockSize, back.getByteBuffer().capacity(), "returned 
block slice capacity");
+            for ( int j = 0; j < BlockSize; j++ ) {
+                byte expected = (byte)((i * 31 + j) & 0xFF);
+                byte actual = back.getByteBuffer().get(j);
+                assertEquals(expected, actual,
+                             String.format("content mismatch: block=%d byte=%d 
expected=%02x actual=%02x",
+                                           i, j, expected, actual));
+            }
+        }
+    }
+
+    @Test
+    public void returnedSlicesAreIndependent() {
+        Block b0 = writePatternBlock(1);
+        Block b1 = writePatternBlock(101);
+
+        Block r0 = file.read(b0.getId().longValue());
+        Block r1 = file.read(b1.getId().longValue());
+
+        // Moving one slice's position/limit must not disturb the other slice
+        // or the shared segment buffer behind them.
+        r0.getByteBuffer().position(BlockSize / 2);
+        r0.getByteBuffer().limit(BlockSize / 2);
+
+        assertEquals(0, r1.getByteBuffer().position(), "sibling slice position 
disturbed");
+        assertEquals(BlockSize, r1.getByteBuffer().limit(), "sibling slice 
limit disturbed");
+
+        Block again = file.read(b1.getId().longValue());
+        assertEquals((byte)((101) & 0xFF), again.getByteBuffer().get(0), 
"content after sibling slice mutation");
+    }
+}
diff --git 
a/jena-tdb1/src/main/java/org/apache/jena/tdb1/base/file/BlockAccessMapped.java 
b/jena-tdb1/src/main/java/org/apache/jena/tdb1/base/file/BlockAccessMapped.java
index 713d6c14a4..be88538c1e 100644
--- 
a/jena-tdb1/src/main/java/org/apache/jena/tdb1/base/file/BlockAccessMapped.java
+++ 
b/jena-tdb1/src/main/java/org/apache/jena/tdb1/base/file/BlockAccessMapped.java
@@ -156,20 +156,18 @@ public class BlockAccessMapped extends BlockAccessBase
 
         synchronized (this) {
             try {
-                // Need to put the alloc AND the slice/reset inside a sync.
+                // Need to put the alloc AND the slice inside a sync.
                 ByteBuffer segBuffer = allocSegment(seg) ;
-                // Now slice the buffer to get the ByteBuffer to return
-                segBuffer.position(segOff) ;
-                segBuffer.limit(segOff+blockSize) ;
-                ByteBuffer dst = segBuffer.slice() ;
-
-                // And then reset limit to max for segment.
-                segBuffer.limit(segBuffer.capacity()) ;
+                // Absolute slice: never mutates the shared segment buffer's
+                // position/limit. The old position/limit/slice/reset sequence
+                // shrank the shared buffer's limit; if anything threw before 
the
+                // reset, the limit stayed shrunk and every later access to a
+                // higher block in the segment failed ("newPosition > limit").
+                ByteBuffer dst = segBuffer.slice(segOff, blockSize) ;
                 // Extend block count when we allocate above end.
                 numFileBlocks = Math.max(numFileBlocks, id+1) ;
                 return dst ;
-            } catch (IllegalArgumentException ex) {
-                // Shouldn't (ha!) happen because the second "limit" resets
+            } catch (RuntimeException ex) {
                 log.error("Id: "+id) ;
                 log.error("Seg="+seg) ;
                 log.error("Segoff="+segOff) ;

Reply via email to