GraalVM Polyglot API for embedding multiple programming languages in Java applications with secure language interoperability
—
I/O abstractions provide pluggable interfaces for controlled file system access, process execution, and message communication. These abstractions enable sandboxed environments with custom I/O behavior while maintaining security boundaries and enabling advanced use cases like virtual file systems and custom process handlers.
Custom file system implementations for controlled file access.
public interface FileSystem {
Path parsePath(URI uri);
Path parsePath(String path);
void checkAccess(Path path, Set<AccessMode> modes, LinkOption... linkOptions);
void createDirectory(Path dir, FileAttribute<?>... attrs);
void delete(Path path);
SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs);
DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter);
Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options);
void setAttribute(Path path, String attribute, Object value, LinkOption... options);
void copy(Path source, Path target, CopyOption... options);
void move(Path source, Path target, CopyOption... options);
Path toAbsolutePath(Path path);
Path toRealPath(Path path, LinkOption... linkOptions);
String getSeparator();
String getPathSeparator();
}Usage:
public class RestrictedFileSystem implements FileSystem {
private final Path allowedRoot;
private final FileSystem delegate;
public RestrictedFileSystem(Path allowedRoot) {
this.allowedRoot = allowedRoot.toAbsolutePath();
this.delegate = FileSystems.getDefault();
}
@Override
public Path parsePath(String path) {
Path parsed = delegate.getPath(path);
if (!isAllowed(parsed)) {
throw new SecurityException("Access denied: " + path);
}
return parsed;
}
@Override
public void checkAccess(Path path, Set<AccessMode> modes, LinkOption... linkOptions) {
if (!isAllowed(path)) {
throw new AccessDeniedException(path.toString());
}
delegate.checkAccess(path, modes, linkOptions);
}
@Override
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) {
if (!isAllowed(path)) {
throw new AccessDeniedException(path.toString());
}
return delegate.newByteChannel(path, options, attrs);
}
private boolean isAllowed(Path path) {
try {
Path absolute = path.toAbsolutePath();
return absolute.startsWith(allowedRoot);
} catch (Exception e) {
return false;
}
}
// ... implement other methods with similar access control
}
// Usage with context
FileSystem restrictedFS = new RestrictedFileSystem(Paths.get("/safe/directory"));
IOAccess ioAccess = IOAccess.newBuilder()
.fileSystem(restrictedFS)
.allowHostFileAccess(false)
.build();
Context context = Context.newBuilder("js")
.allowIO(ioAccess)
.build();
// JavaScript file operations are now restricted to /safe/directory
context.eval("js", "const fs = require('fs'); fs.readFileSync('/safe/directory/file.txt')"); // Works
// context.eval("js", "fs.readFileSync('/etc/passwd')"); // Throws SecurityExceptionCreate in-memory file systems for testing or sandboxing:
public class MemoryFileSystem implements FileSystem {
private final Map<String, byte[]> files = new ConcurrentHashMap<>();
private final Map<String, Set<String>> directories = new ConcurrentHashMap<>();
public MemoryFileSystem() {
directories.put("/", new ConcurrentSkipListSet<>());
}
@Override
public Path parsePath(String path) {
return Paths.get(path);
}
@Override
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) {
String pathStr = path.toString();
byte[] content = files.getOrDefault(pathStr, new byte[0]);
if (options.contains(StandardOpenOption.WRITE) || options.contains(StandardOpenOption.CREATE)) {
return new WritableMemoryChannel(pathStr, content, this::writeFile);
} else {
return new ReadOnlyMemoryChannel(content);
}
}
@Override
public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) {
String dirStr = dir.toString();
Set<String> children = directories.getOrDefault(dirStr, Collections.emptySet());
List<Path> paths = children.stream()
.map(child -> Paths.get(dirStr, child))
.filter(path -> {
try {
return filter == null || filter.accept(path);
} catch (IOException e) {
return false;
}
})
.collect(Collectors.toList());
return new MemoryDirectoryStream(paths);
}
private void writeFile(String path, byte[] content) {
files.put(path, content);
// Update parent directory
Path parent = Paths.get(path).getParent();
if (parent != null) {
String parentStr = parent.toString();
directories.computeIfAbsent(parentStr, k -> new ConcurrentSkipListSet<>())
.add(Paths.get(path).getFileName().toString());
}
}
// ... implement other methods
}Custom process execution for controlled subprocess management.
public interface ProcessHandler {
Process start(ProcessCommand command) throws IOException;
}
public final class ProcessCommand {
public static ProcessCommand create(List<String> command);
public List<String> getCommand();
public String getDirectory();
public Map<String, String> getEnvironment();
public boolean isRedirectErrorStream();
}Usage:
public class RestrictedProcessHandler implements ProcessHandler {
private final Set<String> allowedCommands;
private final Path allowedWorkingDir;
public RestrictedProcessHandler(Set<String> allowedCommands, Path allowedWorkingDir) {
this.allowedCommands = allowedCommands;
this.allowedWorkingDir = allowedWorkingDir;
}
@Override
public Process start(ProcessCommand command) throws IOException {
List<String> cmd = command.getCommand();
if (cmd.isEmpty()) {
throw new IOException("Empty command");
}
String executable = cmd.get(0);
if (!allowedCommands.contains(executable)) {
throw new SecurityException("Command not allowed: " + executable);
}
String workingDir = command.getDirectory();
if (workingDir != null && !Paths.get(workingDir).startsWith(allowedWorkingDir)) {
throw new SecurityException("Working directory not allowed: " + workingDir);
}
ProcessBuilder pb = new ProcessBuilder(cmd);
if (workingDir != null) {
pb.directory(new File(workingDir));
}
// Filter environment variables
Map<String, String> env = command.getEnvironment();
if (env != null) {
pb.environment().clear();
env.entrySet().stream()
.filter(entry -> isSafeEnvironmentVariable(entry.getKey()))
.forEach(entry -> pb.environment().put(entry.getKey(), entry.getValue()));
}
return pb.start();
}
private boolean isSafeEnvironmentVariable(String name) {
// Only allow safe environment variables
return !name.startsWith("SECRET_") && !name.contains("PASSWORD");
}
}
// Usage
Set<String> allowedCommands = Set.of("echo", "cat", "grep", "sed");
Path allowedDir = Paths.get("/tmp/sandbox");
ProcessHandler processHandler = new RestrictedProcessHandler(allowedCommands, allowedDir);
IOAccess ioAccess = IOAccess.newBuilder()
.processHandler(processHandler)
.allowHostSocketAccess(false)
.build();
Context context = Context.newBuilder("js")
.allowIO(ioAccess)
.build();
// JavaScript can only execute allowed commands in allowed directoriesCustom message transport for inter-language communication.
public interface MessageTransport {
MessageEndpoint open(URI uri, MessageEndpoint endpoint) throws IOException, MessageTransport.VetoException;
public static final class VetoException extends Exception {
public VetoException(String message);
}
}
public interface MessageEndpoint {
void sendText(String text) throws IOException;
void sendBinary(ByteBuffer data) throws IOException;
void sendPing(ByteBuffer data) throws IOException;
void sendPong(ByteBuffer data) throws IOException;
void sendClose() throws IOException;
}Usage:
public class CustomMessageTransport implements MessageTransport {
private final Map<String, MessageHandler> handlers = new HashMap<>();
public CustomMessageTransport() {
registerHandler("echo", new EchoHandler());
registerHandler("log", new LogHandler());
}
public void registerHandler(String protocol, MessageHandler handler) {
handlers.put(protocol, handler);
}
@Override
public MessageEndpoint open(URI uri, MessageEndpoint endpoint) throws IOException, VetoException {
String scheme = uri.getScheme();
MessageHandler handler = handlers.get(scheme);
if (handler == null) {
throw new VetoException("Unsupported protocol: " + scheme);
}
return handler.createEndpoint(uri, endpoint);
}
}
public interface MessageHandler {
MessageEndpoint createEndpoint(URI uri, MessageEndpoint clientEndpoint) throws IOException;
}
public class EchoHandler implements MessageHandler {
@Override
public MessageEndpoint createEndpoint(URI uri, MessageEndpoint clientEndpoint) {
return new EchoEndpoint(clientEndpoint);
}
}
public class EchoEndpoint implements MessageEndpoint {
private final MessageEndpoint client;
public EchoEndpoint(MessageEndpoint client) {
this.client = client;
}
@Override
public void sendText(String text) throws IOException {
// Echo back the text
client.sendText("Echo: " + text);
}
@Override
public void sendBinary(ByteBuffer data) throws IOException {
// Echo back the binary data
client.sendBinary(data);
}
// ... implement other methods
}
// Usage
MessageTransport transport = new CustomMessageTransport();
IOAccess ioAccess = IOAccess.newBuilder()
.messageTransport(transport)
.build();
Context context = Context.newBuilder("js")
.allowIO(ioAccess)
.build();
// JavaScript can now use custom message protocolsWork with binary data in a language-neutral way.
public interface ByteSequence {
int length();
byte byteAt(int index);
ByteSequence subSequence(int start, int end);
byte[] toByteArray();
}Usage:
public class FileByteSequence implements ByteSequence {
private final RandomAccessFile file;
private final long offset;
private final int length;
public FileByteSequence(RandomAccessFile file, long offset, int length) {
this.file = file;
this.offset = offset;
this.length = length;
}
@Override
public int length() {
return length;
}
@Override
public byte byteAt(int index) {
if (index < 0 || index >= length) {
throw new IndexOutOfBoundsException();
}
try {
file.seek(offset + index);
return file.readByte();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public ByteSequence subSequence(int start, int end) {
if (start < 0 || end > length || start > end) {
throw new IndexOutOfBoundsException();
}
return new FileByteSequence(file, offset + start, end - start);
}
@Override
public byte[] toByteArray() {
byte[] result = new byte[length];
try {
file.seek(offset);
file.readFully(result);
return result;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}Combine multiple file systems for complex access patterns:
public class LayeredFileSystem implements FileSystem {
private final List<FileSystem> layers;
public LayeredFileSystem(FileSystem... layers) {
this.layers = Arrays.asList(layers);
}
@Override
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) {
// Try each layer in order
for (FileSystem fs : layers) {
try {
return fs.newByteChannel(path, options, attrs);
} catch (NoSuchFileException e) {
continue; // Try next layer
}
}
throw new NoSuchFileException(path.toString());
}
// ... implement other methods with similar layering logic
}
// Usage: Create a file system that checks memory first, then disk
FileSystem layered = new LayeredFileSystem(
new MemoryFileSystem(),
new RestrictedFileSystem(Paths.get("/allowed"))
);Wrap file systems to log all access:
public class AuditFileSystem implements FileSystem {
private final FileSystem delegate;
private final Logger logger;
public AuditFileSystem(FileSystem delegate, Logger logger) {
this.delegate = delegate;
this.logger = logger;
}
@Override
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) {
logger.info("File access: {} with options: {}", path, options);
return delegate.newByteChannel(path, options, attrs);
}
@Override
public void delete(Path path) {
logger.warn("File deletion: {}", path);
delegate.delete(path);
}
// ... wrap other methods with logging
}Manage process execution with resource pooling:
public class PooledProcessHandler implements ProcessHandler {
private final ExecutorService executor;
private final Semaphore processLimit;
public PooledProcessHandler(int maxConcurrentProcesses) {
this.executor = Executors.newCachedThreadPool();
this.processLimit = new Semaphore(maxConcurrentProcesses);
}
@Override
public Process start(ProcessCommand command) throws IOException {
try {
processLimit.acquire();
} catch (InterruptedException e) {
throw new IOException("Process limit acquisition interrupted", e);
}
ProcessBuilder pb = new ProcessBuilder(command.getCommand());
Process process = pb.start();
// Release permit when process completes
executor.submit(() -> {
try {
process.waitFor();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
processLimit.release();
}
});
return process;
}
}I/O operations should handle errors appropriately:
public class SafeFileSystem implements FileSystem {
private final FileSystem delegate;
@Override
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) {
try {
return delegate.newByteChannel(path, options, attrs);
} catch (AccessDeniedException e) {
throw new PolyglotException("File access denied: " + path, e);
} catch (NoSuchFileException e) {
throw new PolyglotException("File not found: " + path, e);
} catch (IOException e) {
throw new PolyglotException("I/O error accessing file: " + path, e);
}
}
// ... similar error handling for other methods
}Install with Tessl CLI
npx tessl i tessl/maven-org-graalvm-polyglot--polyglot