Skip to content

Commit 15e769b

Browse files
vladimirlagunovjbrbot
authored andcommitted
JBR-9779 io-over-nio: RandomAccessFile now can open Windows pipes
The WinAPI function `GetFileAttributesW` actually opens a file and immediately closes it. If the file is a named pipe, it triggers a connection to the listener of the pipe. The listener closes the connection also asynchronously. If the pipe is created with `nMaxInstances=1`, this code does not work: ```java var path = Path.of("""\\.\pipe\openssh-ssh-agent""") // `readAttributes` calls `GetFileAttributesW`, it opens the file and then closes it. if (Files.readAttributes(path, BasicFileAttributes.class).isRegularFile()) { // Very probable race condition: the listener of the pipe has already received // the request of opening the file triggered by `GetFileAttributesW` // but hasn't closed the connection on their side yet. Files.newByteChannel(path); // Fails with ERROR_PIPE_BUSY. } ``` This revision adds an explicit check if some file looks like a Windows pipe. In these cases `RandomAccessFile` and `FileInputStream` don't call `Files.readAttributes` for the file.
1 parent db83c46 commit 15e769b

File tree

2 files changed

+155
-0
lines changed

2 files changed

+155
-0
lines changed

src/java.base/share/classes/java/io/IoOverNioFileSystem.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import java.util.Arrays;
6060
import java.util.EnumSet;
6161
import java.util.List;
62+
import java.util.Locale;
6263
import java.util.Objects;
6364
import java.util.Set;
6465

@@ -142,6 +143,22 @@ static Path getNioPath(File file, boolean mustBeRegularFile) {
142143
return nioPath;
143144
}
144145

146+
if (isWindowsPipe(nioPath)) {
147+
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createnamedpipea see nMaxInstances:
148+
//
149+
// Getting file attributes in this case is dangerous.
150+
// GetFileAttributesW acquires a connection to the pipe internally,
151+
// occupying a place on the server side.
152+
// The server and the client are very likely two different processes, and it takes time to deliver
153+
// the connection closing message to the server.
154+
// If the caller invokes CreateFileW fast enough after GetFileAttributesW and nMaxInstances = 1,
155+
// CreateFileW is called before the server closes the previous connection created by GetFileAttributesW
156+
// and ERROR_PIPE_BUSY is returned.
157+
//
158+
// Anyway, `readAttributes(nioPath).isRegularFile()` returns true for pipes, so it's safe to return here.
159+
return nioPath;
160+
}
161+
145162
// Two significant differences between the legacy java.io and java.nio.files:
146163
// * java.nio.file allows to open directories as streams, java.io.FileInputStream doesn't.
147164
// * java.nio.file doesn't work well with pseudo devices, i.e., `seek()` fails, while java.io works well.
@@ -158,6 +175,19 @@ static Path getNioPath(File file, boolean mustBeRegularFile) {
158175
return null;
159176
}
160177

178+
/**
179+
* <a href="https://learn.microsoft.com/en-us/windows/win32/ipc/pipe-names">
180+
* The pipe path format: {@code ^\\(\w+|\.)\pipe\.*}
181+
* </a>
182+
*/
183+
private static boolean isWindowsPipe(Path path) {
184+
// A small JMH benchmark shows that this code takes less than a microsecond,
185+
// and the JIT compiler does its job very well here.
186+
return path.isAbsolute() &&
187+
path.getRoot().toString().startsWith("\\\\") &&
188+
path.getRoot().toString().toLowerCase(Locale.getDefault()).endsWith("\\pipe\\");
189+
}
190+
161191
private static boolean setPermission0(java.nio.file.FileSystem nioFs, File f, int access, boolean enable, boolean owneronly) {
162192
if (f.getPath().isEmpty()) {
163193
if (nioFs.getSeparator().equals("\\")) {

test/jdk/jb/java/io/IoOverNio/RandomAccessFileTest.java

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@
2424
/* @test
2525
* @summary java.io.RandomAccessFileTest uses java.nio.file inside.
2626
* @library testNio
27+
* @compile --enable-preview --source 27 RandomAccessFileTest.java
2728
* @run junit/othervm
2829
* -Djava.nio.file.spi.DefaultFileSystemProvider=testNio.ManglingFileSystemProvider
2930
* -Djbr.java.io.use.nio=true
3031
* --add-opens jdk.unsupported/com.sun.nio.file=ALL-UNNAMED
3132
* --add-opens java.base/java.io=ALL-UNNAMED
33+
* --enable-native-access=ALL-UNNAMED
34+
* --enable-preview
3235
* RandomAccessFileTest
3336
*/
3437

@@ -45,6 +48,9 @@
4548
import java.io.EOFException;
4649
import java.io.File;
4750
import java.io.RandomAccessFile;
51+
import java.lang.foreign.*;
52+
import java.lang.invoke.MethodHandle;
53+
import java.lang.invoke.VarHandle;
4854
import java.lang.reflect.Field;
4955
import java.nio.channels.FileChannel;
5056
import java.nio.channels.FileLock;
@@ -55,6 +61,7 @@
5561
import java.util.Collections;
5662
import java.util.EnumSet;
5763
import java.util.Objects;
64+
import java.util.concurrent.atomic.AtomicBoolean;
5865

5966
import static org.junit.Assert.assertEquals;
6067
import static org.junit.Assert.assertFalse;
@@ -293,4 +300,122 @@ public void testNoShareDelete() throws Exception {
293300
FileSystems.getDefault().provider().newFileChannel(file.toPath(), Collections.singleton(option)).close();
294301
}
295302
}
303+
304+
/**
305+
* JBR-9779
306+
*/
307+
@Test
308+
public void testWindowsPipe() throws Throwable {
309+
Assume.assumeTrue("Windows-only test", System.getProperty("os.name").toLowerCase().startsWith("win"));
310+
311+
// Creating a pipe.
312+
Linker linker = Linker.nativeLinker();
313+
SymbolLookup loader = SymbolLookup.libraryLookup("kernel32", Arena.global());
314+
315+
StructLayout captureLayout = Linker.Option.captureStateLayout();
316+
VarHandle GetLastError = captureLayout.varHandle(MemoryLayout.PathElement.groupElement("GetLastError"));
317+
318+
MethodHandle CreateNamedPipeW = linker.downcallHandle(
319+
loader.find("CreateNamedPipeW").get(),
320+
FunctionDescriptor.of(ValueLayout.JAVA_LONG,
321+
ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT,
322+
ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT,
323+
ValueLayout.ADDRESS),
324+
Linker.Option.captureCallState("GetLastError")
325+
);
326+
327+
MethodHandle ConnectNamedPipe = linker.downcallHandle(
328+
loader.find("ConnectNamedPipe").get(),
329+
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS),
330+
Linker.Option.captureCallState("GetLastError")
331+
);
332+
333+
MethodHandle DisconnectNamedPipe = linker.downcallHandle(
334+
loader.find("DisconnectNamedPipe").get(),
335+
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG)
336+
);
337+
338+
MethodHandle PeekNamedPipe = linker.downcallHandle(
339+
loader.find("PeekNamedPipe").get(),
340+
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG,
341+
ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.ADDRESS,
342+
ValueLayout.ADDRESS, ValueLayout.ADDRESS)
343+
);
344+
345+
MethodHandle CloseHandle = linker.downcallHandle(
346+
loader.find("CloseHandle").get(),
347+
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG)
348+
);
349+
350+
String pipeName = "\\\\.\\pipe\\jbr-test-pipe-" + System.nanoTime();
351+
Arena arena = Arena.ofAuto();
352+
char[] nameChars = (pipeName + "\0").toCharArray(); // `char` on Windows is UTF-16, as WinAPI expects.
353+
MemorySegment pipeWinPath = arena.allocateFrom(ValueLayout.JAVA_CHAR, nameChars);
354+
MemorySegment capturedState = arena.allocate(captureLayout);
355+
356+
final long INVALID_HANDLE_VALUE = -1L;
357+
358+
long hPipe = (long) CreateNamedPipeW.invokeExact(
359+
capturedState,
360+
pipeWinPath,
361+
0x00000003, // dwOpenMode = PIPE_ACCESS_DUPLEX
362+
0x00000000, // dwPipeMode = PIPE_TYPE_BYTE
363+
1, // nMaxInstances. Limit to 1 to force ERROR_PIPE_BUSY.
364+
1024, // nOutBufferSize
365+
1024, // nInBufferSize
366+
0, // nDefaultTimeOut
367+
MemorySegment.NULL // lpSecurityAttributes
368+
);
369+
370+
if (hPipe == INVALID_HANDLE_VALUE) {
371+
int errorCode = (int) GetLastError.get(capturedState);
372+
throw new Exception("CreateNamedPipeW failed: " + errorCode);
373+
}
374+
375+
AtomicBoolean keepRunning = new AtomicBoolean(true);
376+
Thread serverThread = new Thread(() -> {
377+
// This server accepts a connection and does nothing until the client disconnects explicitly.
378+
try {
379+
int i = 0;
380+
while (keepRunning.get()) {
381+
int connected = (int) ConnectNamedPipe.invokeExact(capturedState, hPipe, MemorySegment.NULL);
382+
if (connected == 0) {
383+
int errorCode = (int) GetLastError.get(capturedState);
384+
// https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
385+
if (errorCode == 6 && !keepRunning.get()) { // ERROR_INVALID_HANDLE
386+
break;
387+
}
388+
throw new Exception("ConnectNamedPipe failed: " + errorCode);
389+
}
390+
try {
391+
int peekResult;
392+
do {
393+
// Random timeout. The timeout must be big enough to reveal possible consequent
394+
// attempts to connect to the pipe.
395+
Thread.sleep(1000);
396+
397+
// Check if the pipe is still connected by peeking at it.
398+
peekResult = (int) PeekNamedPipe.invokeExact(hPipe, MemorySegment.NULL, 0,
399+
MemorySegment.NULL, MemorySegment.NULL, MemorySegment.NULL);
400+
}
401+
while (keepRunning.get() && peekResult != 0);
402+
} finally {
403+
int disconnected = (int) DisconnectNamedPipe.invokeExact(hPipe);
404+
}
405+
}
406+
} catch (Throwable e) {
407+
e.printStackTrace();
408+
}
409+
});
410+
411+
serverThread.setDaemon(true);
412+
serverThread.start();
413+
414+
try {
415+
new RandomAccessFile(pipeName, "rw").close();
416+
} finally {
417+
keepRunning.set(false);
418+
int closed = (int) CloseHandle.invokeExact(hPipe);
419+
}
420+
}
296421
}

0 commit comments

Comments
 (0)