GraalVM Polyglot API for multi-language runtime environments with host-guest interoperability and security controls.
—
The GraalVM Polyglot API provides comprehensive I/O virtualization capabilities, allowing fine-grained control over file system access, network operations, and byte-level data handling. This enables secure execution environments with custom I/O policies and virtual file systems.
IOAccess controls the I/O operations that polyglot contexts can perform, providing security boundaries for file system and network access.
public final class IOAccess {
// Full I/O access - all file system and network operations allowed
public static final IOAccess ALL;
// No I/O access - all I/O operations denied
public static final IOAccess NONE;
}// Factory method for custom I/O configuration
public static IOAccess.Builder newBuilder();The IOAccess.Builder provides granular control over I/O operations:
public static final class IOAccess.Builder {
/**
* Controls host socket access for network operations.
* @param enabled true to allow socket access
* @return this builder
*/
public IOAccess.Builder allowHostSocketAccess(boolean enabled);
/**
* Controls host file access for file system operations.
* @param enabled true to allow file access
* @return this builder
*/
public IOAccess.Builder allowHostFileAccess(boolean enabled);
/**
* Sets a custom file system implementation.
* @param fileSystem the custom file system
* @return this builder
*/
public IOAccess.Builder fileSystem(FileSystem fileSystem);
/**
* Builds the IOAccess configuration.
* @return the configured IOAccess
*/
public IOAccess build();
}IOAccess Configuration Examples:
// Allow file access but deny network access
IOAccess fileOnlyAccess = IOAccess.newBuilder()
.allowHostFileAccess(true)
.allowHostSocketAccess(false)
.build();
Context fileContext = Context.newBuilder("js")
.allowIO(fileOnlyAccess)
.build();
// Allow network but deny file system
IOAccess networkOnlyAccess = IOAccess.newBuilder()
.allowHostFileAccess(false)
.allowHostSocketAccess(true)
.build();
Context networkContext = Context.newBuilder("js")
.allowIO(networkOnlyAccess)
.build();
// Custom file system with restricted access
IOAccess customFSAccess = IOAccess.newBuilder()
.fileSystem(new RestrictedFileSystem("/allowed/path"))
.allowHostSocketAccess(false)
.build();
Context restrictedContext = Context.newBuilder("js")
.allowIO(customFSAccess)
.build();The FileSystem interface provides a complete abstraction for file system operations, enabling custom file system implementations.
public interface FileSystem {
/**
* Parses a URI to a Path.
* @param uri the URI to parse
* @return the path
*/
Path parsePath(URI uri);
/**
* Parses a string to a Path.
* @param path the path string
* @return the path
*/
Path parsePath(String path);
/**
* Checks access permissions for a path.
* @param path the path to check
* @param modes the access modes to check
* @param linkOptions link handling options
* @throws IOException if access check fails
*/
void checkAccess(Path path, Set<? extends AccessMode> modes, LinkOption... linkOptions) throws IOException;
/**
* Creates a directory.
* @param dir the directory to create
* @param attrs file attributes
* @throws IOException if creation fails
*/
void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException;
/**
* Deletes a file or directory.
* @param path the path to delete
* @throws IOException if deletion fails
*/
void delete(Path path) throws IOException;
/**
* Opens a byte channel for reading/writing.
* @param path the file path
* @param options open options
* @param attrs file attributes
* @return the byte channel
* @throws IOException if opening fails
*/
SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException;
/**
* Creates a directory stream for listing directory contents.
* @param dir the directory
* @param filter optional filter for entries
* @return directory stream
* @throws IOException if listing fails
*/
DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException;
/**
* Reads file attributes.
* @param path the file path
* @param attributes attribute pattern to read
* @param options link handling options
* @return map of attributes
* @throws IOException if reading fails
*/
Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException;
/**
* Sets a file attribute.
* @param path the file path
* @param attribute the attribute name
* @param value the attribute value
* @param options link handling options
* @throws IOException if setting fails
*/
void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException;
/**
* Copies a file.
* @param source source path
* @param target target path
* @param options copy options
* @throws IOException if copying fails
*/
void copy(Path source, Path target, CopyOption... options) throws IOException;
/**
* Moves a file.
* @param source source path
* @param target target path
* @param options move options
* @throws IOException if moving fails
*/
void move(Path source, Path target, CopyOption... options) throws IOException;
/**
* Converts to absolute path.
* @param path the path to convert
* @return absolute path
*/
Path toAbsolutePath(Path path);
/**
* Resolves to real path (following links).
* @param path the path to resolve
* @param linkOptions link handling options
* @return real path
* @throws IOException if resolution fails
*/
Path toRealPath(Path path, LinkOption... linkOptions) throws IOException;
/**
* Gets the path separator.
* @return path separator string
*/
String getSeparator();
/**
* Gets the path separator for PATH-style variables.
* @return path separator string
*/
String getPathSeparator();
/**
* Detects MIME type of a file.
* @param path the file path
* @return MIME type or null
*/
String getMimeType(Path path);
/**
* Detects text encoding of a file.
* @param path the file path
* @return encoding name or null
*/
String getEncoding(Path path);
/**
* Gets the temporary directory.
* @return temp directory path
*/
Path getTempDirectory();
/**
* Checks if two paths refer to the same file.
* @param path1 first path
* @param path2 second path
* @param options link handling options
* @return true if same file
* @throws IOException if comparison fails
*/
boolean isSameFile(Path path1, Path path2, LinkOption... options) throws IOException;
}import org.graalvm.polyglot.io.FileSystem;
import java.nio.file.*;
import java.io.IOException;
import java.util.*;
public class RestrictedFileSystem implements FileSystem {
private final Path allowedRoot;
private final Set<String> allowedExtensions;
private final FileSystem delegate;
public RestrictedFileSystem(String allowedPath) {
this.allowedRoot = Paths.get(allowedPath).toAbsolutePath();
this.allowedExtensions = Set.of(".txt", ".json", ".csv", ".log");
this.delegate = FileSystems.getDefault();
}
@Override
public Path parsePath(String path) {
Path parsed = delegate.getPath(path);
return validatePath(parsed);
}
@Override
public Path parsePath(URI uri) {
Path parsed = Paths.get(uri);
return validatePath(parsed);
}
private Path validatePath(Path path) {
Path absolute = path.toAbsolutePath();
// Ensure path is within allowed root
if (!absolute.startsWith(allowedRoot)) {
throw new SecurityException("Access denied: path outside allowed directory");
}
// Check file extension
String filename = absolute.getFileName().toString();
if (filename.contains(".") && allowedExtensions.stream()
.noneMatch(filename.toLowerCase()::endsWith)) {
throw new SecurityException("Access denied: file type not allowed");
}
return absolute;
}
@Override
public void checkAccess(Path path, Set<? extends AccessMode> modes, LinkOption... linkOptions) throws IOException {
Path validatedPath = validatePath(path);
Files.checkAccess(validatedPath, modes.toArray(new AccessMode[0]));
}
@Override
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
Path validatedPath = validatePath(path);
// Log file access
System.out.println("File access: " + validatedPath);
return Files.newByteChannel(validatedPath, options, attrs);
}
@Override
public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
Path validatedDir = validatePath(dir);
return Files.newDirectoryStream(validatedDir, entry -> {
try {
Path validated = validatePath(entry);
return filter == null || filter.accept(validated);
} catch (SecurityException e) {
return false; // Skip inaccessible files
} catch (IOException e) {
return false;
}
});
}
@Override
public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
Path validatedDir = validatePath(dir);
Files.createDirectory(validatedDir, attrs);
}
@Override
public void delete(Path path) throws IOException {
Path validatedPath = validatePath(path);
Files.delete(validatedPath);
}
// Additional methods implementation...
@Override
public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
Path validatedPath = validatePath(path);
return Files.readAttributes(validatedPath, attributes, options);
}
@Override
public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException {
Path validatedPath = validatePath(path);
Files.setAttribute(validatedPath, attribute, value, options);
}
@Override
public void copy(Path source, Path target, CopyOption... options) throws IOException {
Path validatedSource = validatePath(source);
Path validatedTarget = validatePath(target);
Files.copy(validatedSource, validatedTarget, options);
}
@Override
public void move(Path source, Path target, CopyOption... options) throws IOException {
Path validatedSource = validatePath(source);
Path validatedTarget = validatePath(target);
Files.move(validatedSource, validatedTarget, options);
}
@Override
public Path toAbsolutePath(Path path) {
return validatePath(path);
}
@Override
public Path toRealPath(Path path, LinkOption... linkOptions) throws IOException {
Path validatedPath = validatePath(path);
return validatedPath.toRealPath(linkOptions);
}
@Override
public String getSeparator() {
return delegate.getSeparator();
}
@Override
public String getPathSeparator() {
return File.pathSeparator;
}
@Override
public String getMimeType(Path path) {
try {
Path validatedPath = validatePath(path);
return Files.probeContentType(validatedPath);
} catch (Exception e) {
return null;
}
}
@Override
public String getEncoding(Path path) {
// Simple encoding detection based on file extension
String filename = path.getFileName().toString().toLowerCase();
if (filename.endsWith(".json") || filename.endsWith(".js")) {
return "UTF-8";
}
return null;
}
@Override
public Path getTempDirectory() {
// Return temp directory within allowed root
return allowedRoot.resolve("temp");
}
@Override
public boolean isSameFile(Path path1, Path path2, LinkOption... options) throws IOException {
Path validatedPath1 = validatePath(path1);
Path validatedPath2 = validatePath(path2);
return Files.isSameFile(validatedPath1, validatedPath2);
}
}
// Usage
RestrictedFileSystem restrictedFS = new RestrictedFileSystem("/safe/sandbox");
Context context = Context.newBuilder("js")
.allowIO(IOAccess.newBuilder()
.fileSystem(restrictedFS)
.allowHostSocketAccess(false)
.build())
.build();ByteSequence provides an abstraction for byte data that can be used across polyglot boundaries.
public interface ByteSequence {
/**
* Returns the length of the byte sequence.
* @return the length
*/
int length();
/**
* Returns the byte at the specified index.
* @param index the index
* @return the byte value
*/
byte byteAt(int index);
/**
* Creates a subsequence.
* @param startIndex start index (inclusive)
* @param endIndex end index (exclusive)
* @return the subsequence
*/
ByteSequence subSequence(int startIndex, int endIndex);
/**
* Converts to byte array.
* @return byte array copy
*/
byte[] toByteArray();
}The polyglot API provides implementations for common use cases:
Creating ByteSequence from various sources:
import org.graalvm.polyglot.io.ByteSequence;
// From byte array
byte[] data = {0x48, 0x65, 0x6C, 0x6C, 0x6F}; // "Hello" in ASCII
ByteSequence fromArray = ByteSequence.create(data);
// From string with encoding
ByteSequence fromString = ByteSequence.create("Hello World", StandardCharsets.UTF_8);
// From InputStream
try (InputStream input = new FileInputStream("data.bin")) {
ByteSequence fromStream = ByteSequence.create(input);
}
// Usage with Source
Source binarySource = Source.newBuilder("js", fromArray, "binary-data.js").build();
Context context = Context.create("js");
context.getBindings("js").putMember("binaryData", fromArray);
context.eval("js", """
console.log('Length:', binaryData.length);
console.log('First byte:', binaryData.byteAt(0));
// Convert to string (if text data)
let text = '';
for (let i = 0; i < binaryData.length; i++) {
text += String.fromCharCode(binaryData.byteAt(i));
}
console.log('Text:', text); // "Hello"
""");public class MemoryMappedByteSequence implements ByteSequence {
private final MappedByteBuffer buffer;
private final int offset;
private final int length;
public MemoryMappedByteSequence(Path file) throws IOException {
try (RandomAccessFile randomAccessFile = new RandomAccessFile(file.toFile(), "r");
FileChannel channel = randomAccessFile.getChannel()) {
this.buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
this.offset = 0;
this.length = (int) channel.size();
}
}
private MemoryMappedByteSequence(MappedByteBuffer buffer, int offset, int length) {
this.buffer = buffer;
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("Index: " + index + ", Length: " + length);
}
return buffer.get(offset + index);
}
@Override
public ByteSequence subSequence(int startIndex, int endIndex) {
if (startIndex < 0 || endIndex > length || startIndex > endIndex) {
throw new IndexOutOfBoundsException();
}
return new MemoryMappedByteSequence(buffer, offset + startIndex, endIndex - startIndex);
}
@Override
public byte[] toByteArray() {
byte[] result = new byte[length];
for (int i = 0; i < length; i++) {
result[i] = byteAt(i);
}
return result;
}
}The polyglot API provides interfaces for message-based communication, useful for implementing client-server protocols or inter-process communication.
public interface MessageTransport {
/**
* Opens a message endpoint for communication.
* @param uri the endpoint URI
* @param peerEndpoint the peer endpoint for bidirectional communication
* @return the opened message endpoint
* @throws IOException if opening fails
*/
MessageEndpoint open(URI uri, MessageEndpoint peerEndpoint) throws IOException;
}public interface MessageEndpoint extends AutoCloseable {
/**
* Sends a text message.
* @param text the text message
* @throws IOException if sending fails
*/
void sendText(String text) throws IOException;
/**
* Sends binary data.
* @param data the binary data
* @throws IOException if sending fails
*/
void sendBinary(ByteBuffer data) throws IOException;
/**
* Sends a ping message.
* @param data optional ping data
* @throws IOException if sending fails
*/
void sendPing(ByteBuffer data) throws IOException;
/**
* Sends a pong response.
* @param data optional pong data
* @throws IOException if sending fails
*/
void sendPong(ByteBuffer data) throws IOException;
/**
* Sends close message and closes the endpoint.
* @throws IOException if closing fails
*/
void sendClose() throws IOException;
/**
* Closes the endpoint.
*/
@Override
void close() throws IOException;
}Message Transport Example:
public class WebSocketTransport implements MessageTransport {
@Override
public MessageEndpoint open(URI uri, MessageEndpoint peerEndpoint) throws IOException {
return new WebSocketEndpoint(uri, peerEndpoint);
}
private static class WebSocketEndpoint implements MessageEndpoint {
private final URI uri;
private final MessageEndpoint peer;
private volatile boolean closed = false;
public WebSocketEndpoint(URI uri, MessageEndpoint peer) {
this.uri = uri;
this.peer = peer;
}
@Override
public void sendText(String text) throws IOException {
if (closed) throw new IOException("Endpoint is closed");
// Log message
System.out.printf("Sending text to %s: %s%n", uri, text);
// Simulate sending to peer
if (peer != null) {
// In real implementation, this would go through network
// peer.receiveText(text);
}
}
@Override
public void sendBinary(ByteBuffer data) throws IOException {
if (closed) throw new IOException("Endpoint is closed");
System.out.printf("Sending %d bytes to %s%n", data.remaining(), uri);
if (peer != null) {
// peer.receiveBinary(data);
}
}
@Override
public void sendPing(ByteBuffer data) throws IOException {
if (closed) throw new IOException("Endpoint is closed");
System.out.printf("Sending ping to %s%n", uri);
// Automatic pong response
if (peer != null) {
peer.sendPong(data);
}
}
@Override
public void sendPong(ByteBuffer data) throws IOException {
if (closed) throw new IOException("Endpoint is closed");
System.out.printf("Sending pong to %s%n", uri);
}
@Override
public void sendClose() throws IOException {
close();
}
@Override
public void close() throws IOException {
if (!closed) {
closed = true;
System.out.printf("Closed connection to %s%n", uri);
}
}
}
}
// Usage in Context configuration
Context context = Context.newBuilder("js")
.allowIO(IOAccess.newBuilder()
.allowHostSocketAccess(true)
.build())
.option("js.webSocket", "true") // Example: enable WebSocket support
.build();ProcessHandler provides an abstraction for creating and managing external processes.
public interface ProcessHandler {
/**
* Starts an external process.
* @param command the process command configuration
* @return the started process
* @throws IOException if process creation fails
*/
Process start(ProcessCommand command) throws IOException;
}public final class ProcessCommand {
// Process command builder methods
public static ProcessCommand.Builder create(String... command);
public static ProcessCommand.Builder create(List<String> command);
public static final class Builder {
public Builder arguments(String... args);
public Builder arguments(List<String> args);
public Builder environment(Map<String, String> env);
public Builder directory(Path workingDirectory);
public Builder redirectInput(ProcessHandler.Redirect redirect);
public Builder redirectOutput(ProcessHandler.Redirect redirect);
public Builder redirectError(ProcessHandler.Redirect redirect);
public ProcessCommand build();
}
}Process Handler Example:
public class RestrictedProcessHandler implements ProcessHandler {
private final Set<String> allowedCommands;
private final Path allowedDirectory;
public RestrictedProcessHandler(Set<String> allowedCommands, Path allowedDirectory) {
this.allowedCommands = allowedCommands;
this.allowedDirectory = allowedDirectory;
}
@Override
public Process start(ProcessCommand command) throws IOException {
List<String> commandList = command.getCommand();
if (commandList.isEmpty()) {
throw new IOException("Empty command");
}
String executable = commandList.get(0);
if (!allowedCommands.contains(executable)) {
throw new SecurityException("Command not allowed: " + executable);
}
Path workingDir = command.getDirectory();
if (workingDir != null && !workingDir.startsWith(allowedDirectory)) {
throw new SecurityException("Working directory not allowed: " + workingDir);
}
// Log process creation
System.out.printf("Starting process: %s in %s%n", commandList, workingDir);
ProcessBuilder builder = new ProcessBuilder(commandList);
if (workingDir != null) {
builder.directory(workingDir.toFile());
}
// Restrict environment
builder.environment().clear();
builder.environment().put("PATH", "/usr/bin:/bin");
return builder.start();
}
}
// Usage
Set<String> allowedCommands = Set.of("echo", "cat", "ls", "grep");
Path sandboxDir = Paths.get("/safe/sandbox");
ProcessHandler processHandler = new RestrictedProcessHandler(allowedCommands, sandboxDir);
Context context = Context.newBuilder("js")
.allowIO(IOAccess.newBuilder()
.allowHostFileAccess(true)
.build())
.allowCreateProcess(true)
.option("js.processHandler", processHandler) // Example configuration
.build();public class SecureFileSystem implements FileSystem {
private final Path rootPath;
private Path securePath(Path path) {
Path normalized = rootPath.resolve(path).normalize();
if (!normalized.startsWith(rootPath)) {
throw new SecurityException("Path traversal attempt: " + path);
}
return normalized;
}
@Override
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
Path securePath = securePath(path);
// Audit file access
AuditLogger.logFileAccess(securePath, options);
return Files.newByteChannel(securePath, options, attrs);
}
}public class QuotaFileSystem implements FileSystem {
private final AtomicLong bytesRead = new AtomicLong(0);
private final AtomicLong bytesWritten = new AtomicLong(0);
private final long maxBytesRead;
private final long maxBytesWritten;
public QuotaFileSystem(long maxBytesRead, long maxBytesWritten) {
this.maxBytesRead = maxBytesRead;
this.maxBytesWritten = maxBytesWritten;
}
@Override
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
SeekableByteChannel channel = Files.newByteChannel(path, options, attrs);
return new QuotaByteChannel(channel, bytesRead, bytesWritten, maxBytesRead, maxBytesWritten);
}
}
class QuotaByteChannel implements SeekableByteChannel {
private final SeekableByteChannel delegate;
private final AtomicLong totalBytesRead;
private final AtomicLong totalBytesWritten;
private final long maxRead;
private final long maxWrite;
// Implementation that tracks and limits I/O operations...
}public class MonitoringFileSystem implements FileSystem {
private final FileSystem delegate;
private final IOMonitor monitor;
public MonitoringFileSystem(FileSystem delegate, IOMonitor monitor) {
this.delegate = delegate;
this.monitor = monitor;
}
@Override
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
monitor.onFileAccess(path, options);
long startTime = System.nanoTime();
try {
SeekableByteChannel channel = delegate.newByteChannel(path, options, attrs);
monitor.onFileOpenSuccess(path, System.nanoTime() - startTime);
return channel;
} catch (IOException e) {
monitor.onFileOpenFailure(path, e, System.nanoTime() - startTime);
throw e;
}
}
}
interface IOMonitor {
void onFileAccess(Path path, Set<? extends OpenOption> options);
void onFileOpenSuccess(Path path, long durationNanos);
void onFileOpenFailure(Path path, IOException error, long durationNanos);
}Install with Tessl CLI
npx tessl i tessl/maven-org-graalvm-polyglot--graalvm-sdk