Skip to content

Commit f76ee12

Browse files
ppkarwaszCopilot
andauthored
Fix concurrency issue in IOUtils.skip (#801)
* Fix concurrency issue in `IOUtils.skip` This patch addresses a concurrency problem in `IOUtils.skip`, as reported in [COMPRESS-666](https://issues.apache.org/jira/browse/COMPRESS-666) and [COMPRESS-697](https://issues.apache.org/jira/browse/COMPRESS-697). Previously, `IOUtils.skip` relied on `InputStream#read` to skip bytes, using a buffer shared across **all** threads. Although `IOUtils.skip` itself does not consume the data read, certain `InputStream` implementations (e.g. `ChecksumInputStream`) may process that data internally. In concurrent scenarios, this shared buffer could be overwritten by another thread between the `read` and the subsequent internal processing (such as checksum calculation), leading to incorrect behavior. This change reverts commit c12eaff and restores the use of a **per-thread buffer** in `IOUtils.skip`, ensuring thread safety and correct behavior in concurrent environments. * Adds a reentrancy guard to the thread-local pool * Apply suggestion from @Copilot (1) Co-authored-by: Copilot <[email protected]> * Apply suggestions from @Copilot (2) Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 3c5166e commit f76ee12

File tree

5 files changed

+419
-150
lines changed

5 files changed

+419
-150
lines changed

src/changes/changes.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ The <action> type attribute can be add,update,fix,remove.
5656
<action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz">IOUtils.toByteArray(InputStream) now throws IOException on byte array overflow.</action>
5757
<action type="fix" dev="ggregory" due-to="Gary Gregory, Piotr P. Karwasz">Javadoc general improvements.</action>
5858
<action type="fix" dev="ggregory" due-to="Piotr P. Karwasz">IOUtils.toByteArray() now throws EOFException when not enough data is available #796.</action>
59+
<action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz">Fix IOUtils.skip() usage in concurrent scenarios.</action>
5960
<!-- ADD -->
6061
<action dev="ggregory" type="add" due-to="strangelookingnerd, Gary Gregory">FileUtils#byteCountToDisplaySize() supports Zettabyte, Yottabyte, Ronnabyte and Quettabyte #763.</action>
6162
<action dev="ggregory" type="add" due-to="strangelookingnerd, Gary Gregory">Add org.apache.commons.io.FileUtils.ONE_RB #763.</action>

src/main/java/org/apache/commons/io/CopyUtils.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -278,14 +278,18 @@ public static int copy(
278278
final Reader input,
279279
final Writer output)
280280
throws IOException {
281-
final char[] buffer = IOUtils.getScratchCharArray();
282-
int count = 0;
283-
int n;
284-
while (EOF != (n = input.read(buffer))) {
285-
output.write(buffer, 0, n);
286-
count += n;
281+
final char[] buffer = IOUtils.ScratchBufferHolder.getScratchCharArray();
282+
try {
283+
int count = 0;
284+
int n;
285+
while (EOF != (n = input.read(buffer))) {
286+
output.write(buffer, 0, n);
287+
count += n;
288+
}
289+
return count;
290+
} finally {
291+
IOUtils.ScratchBufferHolder.releaseScratchCharArray(buffer);
287292
}
288-
return count;
289293
}
290294

291295
/**

src/main/java/org/apache/commons/io/IOUtils.java

Lines changed: 154 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,94 @@ public class IOUtils {
132132
// Writer. Each method should take at least one of these as a parameter,
133133
// or return one of them.
134134

135+
/**
136+
* Holder for per-thread internal scratch buffers.
137+
*
138+
* <p>Buffers are created lazily and reused within the same thread to reduce allocation overhead. In the rare case of reentrant access, a temporary buffer
139+
* is allocated to avoid data corruption.</p>
140+
*
141+
* <p>Typical usage:</p>
142+
*
143+
* <pre>{@code
144+
* final byte[] buffer = ScratchBufferHolder.getScratchByteArray();
145+
* try {
146+
* // use the buffer
147+
* } finally {
148+
* ScratchBufferHolder.releaseScratchByteArray(buffer);
149+
* }
150+
* }</pre>
151+
*/
152+
static final class ScratchBufferHolder {
153+
154+
/**
155+
* Holder for internal byte array buffer.
156+
*/
157+
private static final ThreadLocal<Object[]> SCRATCH_BYTE_BUFFER_HOLDER = ThreadLocal.withInitial(() -> new Object[] { false, byteArray() });
158+
159+
/**
160+
* Holder for internal char array buffer.
161+
*/
162+
private static final ThreadLocal<Object[]> SCRATCH_CHAR_BUFFER_HOLDER = ThreadLocal.withInitial(() -> new Object[] { false, charArray() });
163+
164+
165+
/**
166+
* Gets the internal byte array buffer.
167+
*
168+
* @return the internal byte array buffer.
169+
*/
170+
static byte[] getScratchByteArray() {
171+
final Object[] holder = SCRATCH_BYTE_BUFFER_HOLDER.get();
172+
// If already in use, return a new array
173+
if ((boolean) holder[0]) {
174+
return byteArray();
175+
}
176+
holder[0] = true;
177+
return (byte[]) holder[1];
178+
}
179+
180+
/**
181+
* Gets the char array buffer.
182+
*
183+
* @return the char array buffer.
184+
*/
185+
static char[] getScratchCharArray() {
186+
final Object[] holder = SCRATCH_CHAR_BUFFER_HOLDER.get();
187+
// If already in use, return a new array
188+
if ((boolean) holder[0]) {
189+
return charArray();
190+
}
191+
holder[0] = true;
192+
return (char[]) holder[1];
193+
}
194+
195+
196+
/**
197+
* If the argument is the internal byte array, release it for reuse.
198+
*
199+
* @param array the byte array to release.
200+
*/
201+
static void releaseScratchByteArray(byte[] array) {
202+
final Object[] holder = SCRATCH_BYTE_BUFFER_HOLDER.get();
203+
if (array == holder[1]) {
204+
Arrays.fill(array, (byte) 0);
205+
holder[0] = false;
206+
}
207+
}
208+
209+
/**
210+
* If the argument is the internal char array, release it for reuse.
211+
*
212+
* @param array the char array to release.
213+
*/
214+
static void releaseScratchCharArray(char[] array) {
215+
final Object[] holder = SCRATCH_CHAR_BUFFER_HOLDER.get();
216+
if (array == holder[1]) {
217+
Arrays.fill(array, (char) 0);
218+
holder[0] = false;
219+
}
220+
}
221+
}
222+
135223
/**
136224
* CR char '{@value}'.
137225
*
@@ -201,26 +289,6 @@ public class IOUtils {
201289
*/
202290
public static final String LINE_SEPARATOR_WINDOWS = StandardLineSeparator.CRLF.getString();
203291

204-
/**
205-
* Internal byte array buffer, intended for both reading and writing.
206-
*/
207-
private static final ThreadLocal<byte[]> SCRATCH_BYTE_BUFFER_RW = ThreadLocal.withInitial(IOUtils::byteArray);
208-
209-
/**
210-
* Internal byte array buffer, intended for write only operations.
211-
*/
212-
private static final byte[] SCRATCH_BYTE_BUFFER_WO = byteArray();
213-
214-
/**
215-
* Internal char array buffer, intended for both reading and writing.
216-
*/
217-
private static final ThreadLocal<char[]> SCRATCH_CHAR_BUFFER_RW = ThreadLocal.withInitial(IOUtils::charArray);
218-
219-
/**
220-
* Internal char array buffer, intended for write only operations.
221-
*/
222-
private static final char[] SCRATCH_CHAR_BUFFER_WO = charArray();
223-
224292
/**
225293
* The maximum size of an array in many Java VMs.
226294
* <p>
@@ -592,10 +660,8 @@ static void checkFromToIndex(final int fromIndex, final int toIndex, final int l
592660
* @see IO#clear()
593661
*/
594662
static void clear() {
595-
SCRATCH_BYTE_BUFFER_RW.remove();
596-
SCRATCH_CHAR_BUFFER_RW.remove();
597-
Arrays.fill(SCRATCH_BYTE_BUFFER_WO, (byte) 0);
598-
Arrays.fill(SCRATCH_CHAR_BUFFER_WO, (char) 0);
663+
ScratchBufferHolder.SCRATCH_BYTE_BUFFER_HOLDER.remove();
664+
ScratchBufferHolder.SCRATCH_CHAR_BUFFER_HOLDER.remove();
599665
}
600666

601667
/**
@@ -1139,39 +1205,43 @@ public static boolean contentEquals(final Reader input1, final Reader input2) th
11391205
}
11401206

11411207
// reuse one
1142-
final char[] array1 = getScratchCharArray();
1208+
final char[] array1 = ScratchBufferHolder.getScratchCharArray();
11431209
// but allocate another
11441210
final char[] array2 = charArray();
11451211
int pos1;
11461212
int pos2;
11471213
int count1;
11481214
int count2;
1149-
while (true) {
1150-
pos1 = 0;
1151-
pos2 = 0;
1152-
for (int index = 0; index < DEFAULT_BUFFER_SIZE; index++) {
1153-
if (pos1 == index) {
1154-
do {
1155-
count1 = input1.read(array1, pos1, DEFAULT_BUFFER_SIZE - pos1);
1156-
} while (count1 == 0);
1157-
if (count1 == EOF) {
1158-
return pos2 == index && input2.read() == EOF;
1215+
try {
1216+
while (true) {
1217+
pos1 = 0;
1218+
pos2 = 0;
1219+
for (int index = 0; index < DEFAULT_BUFFER_SIZE; index++) {
1220+
if (pos1 == index) {
1221+
do {
1222+
count1 = input1.read(array1, pos1, DEFAULT_BUFFER_SIZE - pos1);
1223+
} while (count1 == 0);
1224+
if (count1 == EOF) {
1225+
return pos2 == index && input2.read() == EOF;
1226+
}
1227+
pos1 += count1;
11591228
}
1160-
pos1 += count1;
1161-
}
1162-
if (pos2 == index) {
1163-
do {
1164-
count2 = input2.read(array2, pos2, DEFAULT_BUFFER_SIZE - pos2);
1165-
} while (count2 == 0);
1166-
if (count2 == EOF) {
1167-
return pos1 == index && input1.read() == EOF;
1229+
if (pos2 == index) {
1230+
do {
1231+
count2 = input2.read(array2, pos2, DEFAULT_BUFFER_SIZE - pos2);
1232+
} while (count2 == 0);
1233+
if (count2 == EOF) {
1234+
return pos1 == index && input1.read() == EOF;
1235+
}
1236+
pos2 += count2;
1237+
}
1238+
if (array1[index] != array2[index]) {
1239+
return false;
11681240
}
1169-
pos2 += count2;
1170-
}
1171-
if (array1[index] != array2[index]) {
1172-
return false;
11731241
}
11741242
}
1243+
} finally {
1244+
ScratchBufferHolder.releaseScratchCharArray(array1);
11751245
}
11761246
}
11771247

@@ -1651,9 +1721,13 @@ public static long copyLarge(final InputStream inputStream, final OutputStream o
16511721
* @throws IOException if an I/O error occurs.
16521722
* @since 2.2
16531723
*/
1654-
public static long copyLarge(final InputStream input, final OutputStream output, final long inputOffset,
1655-
final long length) throws IOException {
1656-
return copyLarge(input, output, inputOffset, length, getScratchByteArray());
1724+
public static long copyLarge(final InputStream input, final OutputStream output, final long inputOffset, final long length) throws IOException {
1725+
final byte[] buffer = ScratchBufferHolder.getScratchByteArray();
1726+
try {
1727+
return copyLarge(input, output, inputOffset, length, buffer);
1728+
} finally {
1729+
ScratchBufferHolder.releaseScratchByteArray(buffer);
1730+
}
16571731
}
16581732

16591733
/**
@@ -1723,7 +1797,12 @@ public static long copyLarge(final InputStream input, final OutputStream output,
17231797
* @since 1.3
17241798
*/
17251799
public static long copyLarge(final Reader reader, final Writer writer) throws IOException {
1726-
return copyLarge(reader, writer, getScratchCharArray());
1800+
final char[] buffer = ScratchBufferHolder.getScratchCharArray();
1801+
try {
1802+
return copyLarge(reader, writer, buffer);
1803+
} finally {
1804+
ScratchBufferHolder.releaseScratchCharArray(buffer);
1805+
}
17271806
}
17281807

17291808
/**
@@ -1770,7 +1849,12 @@ public static long copyLarge(final Reader reader, final Writer writer, final cha
17701849
* @since 2.2
17711850
*/
17721851
public static long copyLarge(final Reader reader, final Writer writer, final long inputOffset, final long length) throws IOException {
1773-
return copyLarge(reader, writer, inputOffset, length, getScratchCharArray());
1852+
final char[] buffer = ScratchBufferHolder.getScratchCharArray();
1853+
try {
1854+
return copyLarge(reader, writer, inputOffset, length, buffer);
1855+
} finally {
1856+
ScratchBufferHolder.releaseScratchCharArray(buffer);
1857+
}
17741858
}
17751859

17761860
/**
@@ -1837,64 +1921,6 @@ static UnsynchronizedByteArrayOutputStream copyToOutputStream(
18371921
}
18381922
}
18391923

1840-
/**
1841-
* Fills the given array with 0s.
1842-
*
1843-
* @param arr The non-null array to fill.
1844-
* @return The given array.
1845-
*/
1846-
private static byte[] fill0(final byte[] arr) {
1847-
Arrays.fill(arr, (byte) 0);
1848-
return arr;
1849-
}
1850-
1851-
/**
1852-
* Fills the given array with 0s.
1853-
*
1854-
* @param arr The non-null array to fill.
1855-
* @return The given array.
1856-
*/
1857-
private static char[] fill0(final char[] arr) {
1858-
Arrays.fill(arr, (char) 0);
1859-
return arr;
1860-
}
1861-
1862-
/**
1863-
* Gets the internal byte array buffer, intended for both reading and writing.
1864-
*
1865-
* @return the internal byte array buffer, intended for both reading and writing.
1866-
*/
1867-
static byte[] getScratchByteArray() {
1868-
return fill0(SCRATCH_BYTE_BUFFER_RW.get());
1869-
}
1870-
1871-
/**
1872-
* Gets the internal byte array intended for write only operations.
1873-
*
1874-
* @return the internal byte array intended for write only operations.
1875-
*/
1876-
static byte[] getScratchByteArrayWriteOnly() {
1877-
return fill0(SCRATCH_BYTE_BUFFER_WO);
1878-
}
1879-
1880-
/**
1881-
* Gets the char byte array buffer, intended for both reading and writing.
1882-
*
1883-
* @return the char byte array buffer, intended for both reading and writing.
1884-
*/
1885-
static char[] getScratchCharArray() {
1886-
return fill0(SCRATCH_CHAR_BUFFER_RW.get());
1887-
}
1888-
1889-
/**
1890-
* Gets the internal char array intended for write only operations.
1891-
*
1892-
* @return the internal char array intended for write only operations.
1893-
*/
1894-
static char[] getScratchCharArrayWriteOnly() {
1895-
return fill0(SCRATCH_CHAR_BUFFER_WO);
1896-
}
1897-
18981924
/**
18991925
* Returns the length of the given array in a null-safe manner.
19001926
*
@@ -2527,7 +2553,12 @@ public static URL resourceToURL(final String name, final ClassLoader classLoader
25272553
* @since 2.0
25282554
*/
25292555
public static long skip(final InputStream input, final long skip) throws IOException {
2530-
return skip(input, skip, IOUtils::getScratchByteArrayWriteOnly);
2556+
final byte[] buffer = ScratchBufferHolder.getScratchByteArray();
2557+
try {
2558+
return skip(input, skip, () -> buffer);
2559+
} finally {
2560+
ScratchBufferHolder.releaseScratchByteArray(buffer);
2561+
}
25312562
}
25322563

25332564
/**
@@ -2634,14 +2665,18 @@ public static long skip(final Reader reader, final long toSkip) throws IOExcepti
26342665
throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip);
26352666
}
26362667
long remain = toSkip;
2637-
while (remain > 0) {
2638-
// See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip()
2639-
final char[] charArray = getScratchCharArrayWriteOnly();
2640-
final long n = reader.read(charArray, 0, (int) Math.min(remain, charArray.length));
2641-
if (n < 0) { // EOF
2642-
break;
2668+
final char[] charArray = ScratchBufferHolder.getScratchCharArray();
2669+
try {
2670+
while (remain > 0) {
2671+
// See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip()
2672+
final long n = reader.read(charArray, 0, (int) Math.min(remain, charArray.length));
2673+
if (n < 0) { // EOF
2674+
break;
2675+
}
2676+
remain -= n;
26432677
}
2644-
remain -= n;
2678+
} finally {
2679+
ScratchBufferHolder.releaseScratchCharArray(charArray);
26452680
}
26462681
return toSkip - remain;
26472682
}
@@ -2667,7 +2702,7 @@ public static long skip(final Reader reader, final long toSkip) throws IOExcepti
26672702
* @since 2.0
26682703
*/
26692704
public static void skipFully(final InputStream input, final long toSkip) throws IOException {
2670-
final long skipped = skip(input, toSkip, IOUtils::getScratchByteArrayWriteOnly);
2705+
final long skipped = skip(input, toSkip);
26712706
if (skipped != toSkip) {
26722707
throw new EOFException("Bytes to skip: " + toSkip + " actual: " + skipped);
26732708
}

0 commit comments

Comments
 (0)