CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-graalvm-polyglot--graalvm-sdk

GraalVM Polyglot API for multi-language runtime environments with host-guest interoperability and security controls.

Pending
Overview
Eval results
Files

io-filesystem.mddocs/

I/O and File System Virtualization

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.

I/O Access Control

IOAccess controls the I/O operations that polyglot contexts can perform, providing security boundaries for file system and network access.

Predefined IOAccess Policies

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;
}

Custom IOAccess Configuration

// 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();

Virtual File System Interface

The FileSystem interface provides a complete abstraction for file system operations, enabling custom file system implementations.

FileSystem Interface

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;
}

Custom FileSystem Implementation Example

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 Interface

ByteSequence provides an abstraction for byte data that can be used across polyglot boundaries.

ByteSequence Interface

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();
}

ByteSequence Implementations

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"
    """);

Custom ByteSequence Implementation

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;
    }
}

Message Transport and Communication

The polyglot API provides interfaces for message-based communication, useful for implementing client-server protocols or inter-process communication.

MessageTransport Interface

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;
}

MessageEndpoint Interface

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();

Process Handler Interface

ProcessHandler provides an abstraction for creating and managing external processes.

ProcessHandler Interface

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;
}

ProcessCommand Configuration

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();

I/O Security Best Practices

1. Path Traversal Prevention

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);
    }
}

2. Resource Quotas

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...
}

3. I/O Monitoring and Logging

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

docs

context-management.md

index.md

io-filesystem.md

language-execution.md

monitoring-management.md

proxy-system.md

security-access.md

value-interop.md

tile.json