Reapply "JBR-9531 Prevent unexpected recursive usage of java.io over nio wrappers"

This reverts commit 77da73f22f.
This commit is contained in:
Vitaly Provodin
2025-12-03 08:15:31 +04:00
parent 56f339b7ef
commit 3a7a01cdc3
9 changed files with 244 additions and 115 deletions

View File

@@ -159,5 +159,84 @@ public class IoOverNio {
* <p>
* The problem was found with the test {@code jtreg:test/jdk/java/io/FileDescriptor/Sharing.java}.
*/
public static final ThreadLocal<Closeable> PARENT_FOR_FILE_CHANNEL_IMPL = new ThreadLocal<>();
public static class ParentForFileChannelImplHolder {
private static final ThreadLocal<Closeable> holder = new ThreadLocal<>();
private ParentForFileChannelImplHolder() {}
public static Closeable get() {
return holder.get();
}
public static void set(Closeable parent) {
RecursionGuard.ensureActive();
holder.set(parent);
}
public static void remove() {
RecursionGuard.ensureActive();
holder.remove();
}
}
/**
* <p>With java.io over java.nio backend, it's possible that some code invokes file system operations while
* already executing a similar operation. An example is when a classloader uses {@link FileSystems#getDefault()}
* during class loading. Such cases break usage of {@link ParentForFileChannelImplHolder}.</p>
*
* <p>This class is to be used around places that can hypothetically access {@link ParentForFileChannelImplHolder}
* recursively.</p>
*/
public static class RecursionGuard implements Closeable {
private static final ThreadLocal<RecursionGuard> HEAD = new ThreadLocal<>();
private final RecursionGuard parent;
private final Object label;
private final ThreadLocalCloseable additionalClosable;
/**
* @param label A unique object for a specific method. The object is used for reference equality.
* A static string or a reference to a class is a good candidate.
*/
public static RecursionGuard create(Object label) {
ThreadLocalCloseable additionalClosable = null;
for (var guard = HEAD.get(); guard != null; guard = guard.parent) {
if (guard.label == label) {
additionalClosable = disableInThisThread();
break;
}
}
var result = new RecursionGuard(HEAD.get(), label, additionalClosable);
HEAD.set(result);
return result;
}
private RecursionGuard(RecursionGuard parent, Object label, ThreadLocalCloseable additionalClosable) {
this.parent = parent;
this.label = label;
this.additionalClosable = additionalClosable;
}
public static void ensureActive() {
if (HEAD.get() == null) {
throw new Error("RecursionGuard is not installed");
}
}
@Override
public void close() {
HEAD.set(parent);
if (additionalClosable != null) {
additionalClosable.close();
}
}
}
/**
* Intended only for suppressing warnings about unused variables.
*/
@SuppressWarnings("unused")
public static void blackhole(Object any) {
// Nothing here.
}
}

View File

@@ -175,37 +175,40 @@ public class FileInputStream extends InputStream
}
path = file.getPath();
java.nio.file.FileSystem nioFs = IoOverNioFileSystem.acquireNioFs(path);
Path nioPath = null;
if (nioFs != null && path != null) {
try {
nioPath = nioFs.getPath(path);
isRegularFile = Files.isRegularFile(nioPath);
} catch (InvalidPathException|SecurityException ignored) {
// Nothing.
try (var guard = IoOverNio.RecursionGuard.create(FileInputStream.class)) {
IoOverNio.blackhole(guard);
java.nio.file.FileSystem nioFs = IoOverNioFileSystem.acquireNioFs(path);
Path nioPath = null;
if (nioFs != null && path != null) {
try {
nioPath = nioFs.getPath(path);
isRegularFile = Files.isRegularFile(nioPath);
} catch (InvalidPathException|SecurityException ignored) {
// Nothing.
}
}
}
// Two significant differences between the legacy java.io and java.nio.files:
// * java.nio.file allows to open directories as streams, java.io.FileInputStream doesn't.
// * java.nio.file doesn't work well with pseudo devices, i.e., `seek()` fails, while java.io works well.
useNio = nioPath != null && isRegularFile == Boolean.TRUE;
// Two significant differences between the legacy java.io and java.nio.files:
// * java.nio.file allows to open directories as streams, java.io.FileInputStream doesn't.
// * java.nio.file doesn't work well with pseudo devices, i.e., `seek()` fails, while java.io works well.
useNio = nioPath != null && isRegularFile == Boolean.TRUE;
if (useNio) {
var bundle = IoOverNioFileSystem.initializeStreamUsingNio(
this, nioFs, file, nioPath, Set.of(StandardOpenOption.READ), channelCleanable);
channel = bundle.channel();
fd = bundle.fd();
externalChannelHolder = bundle.externalChannelHolder();
} else {
fd = new FileDescriptor();
fd.attach(this);
open(path);
FileCleanable.register(fd); // open set the fd, register the cleanup
externalChannelHolder = null;
}
if (DEBUG.writeTraces()) {
System.err.printf("Created a FileInputStream for %s%n", file);
if (useNio) {
var bundle = IoOverNioFileSystem.initializeStreamUsingNio(
this, nioFs, file, nioPath, Set.of(StandardOpenOption.READ), channelCleanable);
channel = bundle.channel();
fd = bundle.fd();
externalChannelHolder = bundle.externalChannelHolder();
} else {
fd = new FileDescriptor();
fd.attach(this);
open(path);
FileCleanable.register(fd); // open set the fd, register the cleanup
externalChannelHolder = null;
}
if (DEBUG.writeTraces()) {
System.err.printf("Created a FileInputStream for %s%n", file);
}
}
}

View File

@@ -247,45 +247,49 @@ public class FileOutputStream extends OutputStream
}
this.path = name;
java.nio.file.FileSystem nioFs = IoOverNioFileSystem.acquireNioFs(path);
useNio = path != null && nioFs != null;
if (useNio) {
Path nioPath = nioFs.getPath(path);
try (var guard = IoOverNio.RecursionGuard.create(FileOutputStream.class)) {
IoOverNio.blackhole(guard);
java.nio.file.FileSystem nioFs = IoOverNioFileSystem.acquireNioFs(path);
useNio = path != null && nioFs != null;
if (useNio) {
Path nioPath = nioFs.getPath(path);
// java.io backend doesn't open DOS hidden files for writing, but java.nio.file opens.
// This code mimics the old behavior.
if (nioFs.getSeparator().equals("\\")) {
DosFileAttributes attrs = null;
try {
var view = Files.getFileAttributeView(nioPath, DosFileAttributeView.class);
if (view != null) {
attrs = view.readAttributes();
// java.io backend doesn't open DOS hidden files for writing, but java.nio.file opens.
// This code mimics the old behavior.
if (nioFs.getSeparator().equals("\\")) {
DosFileAttributes attrs = null;
try {
var view = Files.getFileAttributeView(nioPath, DosFileAttributeView.class);
if (view != null) {
attrs = view.readAttributes();
}
} catch (IOException | UnsupportedOperationException | SecurityException ignored) {
// Windows paths without DOS attributes? Not a problem in this case.
}
if (attrs != null && (attrs.isHidden() || attrs.isDirectory())) {
throw new FileNotFoundException(file.getPath() + " (Access is denied)");
}
} catch (IOException | UnsupportedOperationException | SecurityException ignored) {
// Windows paths without DOS attributes? Not a problem in this case.
}
if (attrs != null && (attrs.isHidden() || attrs.isDirectory())) {
throw new FileNotFoundException(file.getPath() + " (Access is denied)");
}
}
Set<OpenOption> options = append
? Set.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND)
: Set.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
var bundle = IoOverNioFileSystem.initializeStreamUsingNio(
this, nioFs, file, nioPath, options, channelCleanable);
channel = bundle.channel();
fd = bundle.fd();
externalChannelHolder = bundle.externalChannelHolder();
} else {
this.fd = new FileDescriptor();
fd.attach(this);
open(name, append);
FileCleanable.register(fd); // open sets the fd, register the cleanup
externalChannelHolder = null;
}
if (DEBUG.writeTraces()) {
System.err.printf("Created a FileOutputStream for %s%n", file);
Set<OpenOption> options = append
? Set.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND)
: Set.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
var bundle = IoOverNioFileSystem.initializeStreamUsingNio(
this, nioFs, file, nioPath, options, channelCleanable);
channel = bundle.channel();
fd = bundle.fd();
externalChannelHolder = bundle.externalChannelHolder();
} else {
this.fd = new FileDescriptor();
fd.attach(this);
open(name, append);
FileCleanable.register(fd); // open sets the fd, register the cleanup
externalChannelHolder = null;
}
if (DEBUG.writeTraces()) {
System.err.printf("Created a FileOutputStream for %s%n", file);
}
}
}

View File

@@ -275,10 +275,10 @@ class IoOverNioFileSystem extends FileSystem {
try {
// This tricky thread local variable allows specifying an argument for sun.nio.ch.FileChannelImpl.<init>
// which is not present in the NIO public API and which is not easy to specify another way.
IoOverNio.PARENT_FOR_FILE_CHANNEL_IMPL.set(owner);
IoOverNio.ParentForFileChannelImplHolder.set(owner);
return initializeStreamsUsingNio0(owner, nioFs, file, nioPath, optionsForChannel, channelCleanable);
} finally {
IoOverNio.PARENT_FOR_FILE_CHANNEL_IMPL.remove();
IoOverNio.ParentForFileChannelImplHolder.remove();
}
}
@@ -869,7 +869,8 @@ class IoOverNioFileSystem extends FileSystem {
@Override
public boolean delete(File f) {
try {
try (var guard = IoOverNio.RecursionGuard.create("IoOverNioFileSystem.delete")) {
IoOverNio.blackhole(guard);
boolean result = delete0(f, true);
if (DEBUG.writeTraces()) {
System.err.printf("IoOverNioFileSystem.delete(%s) = %b%n", f, result);

View File

@@ -302,47 +302,51 @@ public class RandomAccessFile implements DataOutput, DataInput, Closeable {
throw new FileNotFoundException("Invalid file path");
}
path = name;
FileSystem nioFs = IoOverNioFileSystem.acquireNioFs(path);
Path nioPath = null;
if (nioFs != null) {
try {
nioPath = nioFs.getPath(path);
} catch (InvalidPathException ignored) {
// Nothing.
try (var guard = IoOverNio.RecursionGuard.create(RandomAccessFile.class)) {
IoOverNio.blackhole(guard);
FileSystem nioFs = IoOverNioFileSystem.acquireNioFs(path);
Path nioPath = null;
if (nioFs != null) {
try {
nioPath = nioFs.getPath(path);
} catch (InvalidPathException ignored) {
// Nothing.
}
}
}
// Two significant differences between the legacy java.io and java.nio.files:
// * java.nio.file allows to open directories as streams, java.io.FileInputStream doesn't.
// * java.nio.file doesn't work well with pseudo devices, i.e., `seek()` fails, while java.io works well.
boolean isRegularFile;
try {
isRegularFile = nioPath != null &&
Files.readAttributes(nioPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS).isRegularFile();
}
catch (NoSuchFileException ignored) {
isRegularFile = true;
}
catch (IOException ignored) {
isRegularFile = false;
}
// Two significant differences between the legacy java.io and java.nio.files:
// * java.nio.file allows to open directories as streams, java.io.FileInputStream doesn't.
// * java.nio.file doesn't work well with pseudo devices, i.e., `seek()` fails, while java.io works well.
boolean isRegularFile;
try {
isRegularFile = nioPath != null &&
Files.readAttributes(nioPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS).isRegularFile();
}
catch (NoSuchFileException ignored) {
isRegularFile = true;
}
catch (IOException ignored) {
isRegularFile = false;
}
useNio = nioPath != null && isRegularFile;
if (useNio) {
var bundle = IoOverNioFileSystem.initializeStreamUsingNio(
this, nioFs, file, nioPath, optionsForChannel(imode), channelCleanable);
channel = bundle.channel();
fd = bundle.fd();
externalChannelHolder = bundle.externalChannelHolder();
} else {
fd = new FileDescriptor();
fd.attach(this);
open(name, imode);
FileCleanable.register(fd); // open sets the fd, register the cleanup
externalChannelHolder = null;
}
if (DEBUG.writeTraces()) {
System.err.printf("Created a RandomAccessFile for %s%n", file);
useNio = nioPath != null && isRegularFile;
if (useNio) {
var bundle = IoOverNioFileSystem.initializeStreamUsingNio(
this, nioFs, file, nioPath, optionsForChannel(imode), channelCleanable);
channel = bundle.channel();
fd = bundle.fd();
externalChannelHolder = bundle.externalChannelHolder();
} else {
fd = new FileDescriptor();
fd.attach(this);
open(name, imode);
FileCleanable.register(fd); // open sets the fd, register the cleanup
externalChannelHolder = null;
}
if (DEBUG.writeTraces()) {
System.err.printf("Created a RandomAccessFile for %s%n", file);
}
}
}

View File

@@ -131,7 +131,7 @@ public class FileChannelImpl
this.writable = writable;
this.direct = direct;
if (parent == null) {
parent = IoOverNio.PARENT_FOR_FILE_CHANNEL_IMPL.get();
parent = IoOverNio.ParentForFileChannelImplHolder.get();
}
this.parent = parent;
if (direct) {

View File

@@ -49,16 +49,19 @@ public class ManglingFileSystem extends FileSystem {
@Override
public void close() throws IOException {
ManglingFileSystemProvider.triggerSporadicFileAccess();
provider.defaultFs.close();
}
@Override
public boolean isOpen() {
ManglingFileSystemProvider.triggerSporadicFileAccess();
return provider.defaultFs.isOpen();
}
@Override
public boolean isReadOnly() {
ManglingFileSystemProvider.triggerSporadicFileAccess();
return provider.defaultFs.isReadOnly();
}
@@ -69,6 +72,7 @@ public class ManglingFileSystem extends FileSystem {
@Override
public Iterable<Path> getRootDirectories() {
ManglingFileSystemProvider.triggerSporadicFileAccess();
Iterable<Path> delegateRoots = provider.defaultFs.getRootDirectories();
List<Path> result = new ArrayList<>();
for (Path delegateRoot : delegateRoots) {
@@ -86,11 +90,13 @@ public class ManglingFileSystem extends FileSystem {
@Override
public Iterable<FileStore> getFileStores() {
ManglingFileSystemProvider.triggerSporadicFileAccess();
return provider.defaultFs.getFileStores();
}
@Override
public Set<String> supportedFileAttributeViews() {
ManglingFileSystemProvider.triggerSporadicFileAccess();
return provider.defaultFs.supportedFileAttributeViews();
}

View File

@@ -23,19 +23,12 @@
package testNio;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.AccessDeniedException;
import java.nio.file.AccessMode;
import java.nio.file.CopyOption;
import java.nio.file.DirectoryStream;
import java.nio.file.FileStore;
import java.nio.file.FileSystem;
import java.nio.file.LinkOption;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.DosFileAttributeView;
@@ -48,6 +41,7 @@ import java.nio.file.spi.FileSystemProvider;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
public class ManglingFileSystemProvider extends FileSystemProvider {
public static final String extraContent = "3x7r4";
@@ -168,6 +162,7 @@ public class ManglingFileSystemProvider extends FileSystemProvider {
@Override
public FileChannel newFileChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
triggerSporadicFileAccess();
return new ManglingFileChannel(defaultProvider.newFileChannel(unwrap(path), options, attrs));
}
@@ -178,11 +173,13 @@ public class ManglingFileSystemProvider extends FileSystemProvider {
@Override
public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
triggerSporadicFileAccess();
return new ManglingDirectoryStream(manglingFs, (ManglingPath) dir, defaultProvider.newDirectoryStream(unwrap(dir), filter));
}
@Override
public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
triggerSporadicFileAccess();
if (prohibitFileTreeModifications) {
throw new AccessDeniedException(dir.toString(), null, "Test: All file tree modifications are prohibited");
}
@@ -191,6 +188,7 @@ public class ManglingFileSystemProvider extends FileSystemProvider {
@Override
public void delete(Path path) throws IOException {
triggerSporadicFileAccess();
if (prohibitFileTreeModifications) {
throw new AccessDeniedException(path.toString(), null, "Test: All file tree modifications are prohibited");
}
@@ -199,6 +197,7 @@ public class ManglingFileSystemProvider extends FileSystemProvider {
@Override
public void copy(Path source, Path target, CopyOption... options) throws IOException {
triggerSporadicFileAccess();
if (prohibitFileTreeModifications) {
throw new AccessDeniedException(source.toString(), null, "Test: All file tree modifications are prohibited");
}
@@ -209,6 +208,7 @@ public class ManglingFileSystemProvider extends FileSystemProvider {
@Override
public void move(Path source, Path target, CopyOption... options) throws IOException {
triggerSporadicFileAccess();
if (prohibitFileTreeModifications) {
throw new AccessDeniedException(source.toString(), null, "Test: All file tree modifications are prohibited");
}
@@ -219,6 +219,7 @@ public class ManglingFileSystemProvider extends FileSystemProvider {
@Override
public boolean isSameFile(Path path, Path path2) throws IOException {
triggerSporadicFileAccess();
Path source2 = unwrap(path);
Path target2 = unwrap(path2);
return defaultProvider.isSameFile(source2, target2);
@@ -226,16 +227,19 @@ public class ManglingFileSystemProvider extends FileSystemProvider {
@Override
public boolean isHidden(Path path) throws IOException {
triggerSporadicFileAccess();
return defaultProvider.isHidden(unwrap(path));
}
@Override
public FileStore getFileStore(Path path) throws IOException {
triggerSporadicFileAccess();
return defaultProvider.getFileStore(unwrap(path));
}
@Override
public void checkAccess(Path path, AccessMode... modes) throws IOException {
triggerSporadicFileAccess();
defaultProvider.checkAccess(unwrap(path), modes);
if (denyAccessToEverything) {
throw new AccessDeniedException(path.toString(), null, "Test: No access rules to anything");
@@ -244,11 +248,13 @@ public class ManglingFileSystemProvider extends FileSystemProvider {
@Override
public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
triggerSporadicFileAccess();
return wrap(defaultProvider.getFileAttributeView(unwrap(path), type, options));
}
@Override
public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options) throws IOException {
triggerSporadicFileAccess();
return wrap(defaultProvider.readAttributes(unwrap(path), type, options));
}
@@ -259,6 +265,7 @@ public class ManglingFileSystemProvider extends FileSystemProvider {
@Override
public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException {
triggerSporadicFileAccess();
if (prohibitFileTreeModifications) {
throw new AccessDeniedException(path.toString(), null, "Test: All file tree modifications are prohibited");
}
@@ -267,11 +274,13 @@ public class ManglingFileSystemProvider extends FileSystemProvider {
@Override
public boolean exists(Path path, LinkOption... options) {
triggerSporadicFileAccess();
return defaultProvider.exists(unwrap(path), options);
}
@Override
public void createSymbolicLink(Path link, Path target, FileAttribute<?>... attrs) throws IOException {
triggerSporadicFileAccess();
if (prohibitFileTreeModifications) {
throw new AccessDeniedException(link.toString(), null, "Test: All file tree modifications are prohibited");
}
@@ -303,4 +312,26 @@ public class ManglingFileSystemProvider extends FileSystemProvider {
}
return view;
}
private static final AtomicLong counter = new AtomicLong();
static void triggerSporadicFileAccess() {
if (FileSystems.getDefault() != null) {
try {
var tempFile = new File(System.getProperty("java.io.tmpdir"), "test.tmp." + counter.incrementAndGet());
try {
try (var out = new java.io.FileOutputStream(tempFile)) {
out.write(1);
}
try (var in = new java.io.FileInputStream(tempFile)) {
in.read();
}
} finally {
tempFile.delete();
}
} catch (Exception e) {
throw new Error(e);
}
}
}
}

View File

@@ -166,6 +166,7 @@ public class ManglingPath implements Path {
@Override
public Path toRealPath(LinkOption... options) throws IOException {
ManglingFileSystemProvider.triggerSporadicFileAccess();
return new ManglingPath(fileSystem, delegate.toRealPath(options));
}