Skip to main content

Blocking File System Access (AOP)

Simple Story

This page follows one paper being picked up, the whole way through.

The pupil reaches for it, the teacher gets there first, looks at which paper it is and what is about to be done with it, checks that against the permitted list, and then either allows it or takes it away. Every step of that is written down here.

1. Ares 2 AOP File System Access Control: High-Level Overview

This document explains how Ares 2 decides whether student code may access the file system through a set of monitored file system methods. It checks:

  • The caller of the monitored file system method
  • The operations the monitored file system method wants to conduct
  • The paths the monitored file system method wants to access

Summary for Programming Instructors (TL;DR)

What does Ares do?

  • ✅ Monitors a broad set of file system APIs automatically (Read, Write, Create, Delete, Execute)
  • ✅ Blocks student code from accessing forbidden paths
  • Configurable via YAML - You determine which paths are allowed
  • ✅ Works without code changes to student code (via AOP)
  • ✅ Provides clear error messages with exact source (which method, which path, which test)

When do you need this?

  • When students should practice file operations (e.g., reading/writing files)
  • But you want to prevent them from reading sensitive files or deleting system files
  • Example: Allow /tmp for exercises, block /etc and /home

How does it work (simplified)?

  1. Student calls Files.readString("/etc/passwd")
  2. Ares intercepts the call (AOP) and checks:
    • Does this come from student code? ✓ Yes
    • Is /etc/passwd in the allowlist? ✗ No
  3. Ares blocks and throws a meaningful exception

Comparison: AOP vs. Architecture

AspectAspect-oriented programming (AOP), through Byte Buddy or AspectJArchitecture (ArchUnit, or the T. J. Watson Libraries for Analysis, WALA)
Analysis TimeDuring execution (runtime)Before execution (static)
DetectionIntercepts method callsAnalyses code structure
GranularityPath-based permissionsBinary (allowed/forbidden)
Performance ImpactRuntime overhead on every callAnalysis overhead only
False PositivesNone (only executed code checked)Possible (unreachable code)
CoverageOnly executed pathsAll code paths
ConfigurationPath-level permissionsClass-level exemptions; package permissions only affect the separate import rule
Use CaseRuntime security enforcementPre-submission validation
Error TimingProduction executionTest phase

1.1 How Does The UML Activity Diagram look like?

Below is a general overview of the process for deciding whether to allow or block file access as a Unified Modeling Language (UML) activity diagram. Throughout this document, you will find the following symbols:

  • 🔴 Red = File access blocked (security policy violation detected)
  • 🌕 Yellow = Intermediate condition met → continue to the next verification step
  • 🟢 Green = File access permitted (no security policy violation detected)

File System Security Validation Flow


1.2 What Is AOP?

AOP (Aspect-Oriented Programming) is a technique that automatically runs security checks before certain methods execute, without modifying the student code. Think of it like a security guard checking IDs before people enter a building - the building code does not change, but everyone gets checked automatically when interacting with the building.

Concrete Example:

Without AOP: You would have to manually write security checks before every file access (if that is even possible).

public void readFile(String path) {
if (!isAllowed(path)) throw new SecurityException(); // Security check happens manually and has to be stated explicitly!
Files.readString(Path.of(path)); // Actual code
}

With AOP: Ares automatically inserts this check before EVERY Files.readString(), so no code changes are required.

public void readFile(String path) {
Files.readString(Path.of(path)); // Actual code, Security check happens automatically in the background!
}

1.3 Which AOP Modes / Implementations Are There?

Ares automatically monitors file system operations by intercepting specific Java methods using one of two AOP implementations:

  • Byte Buddy (Instrumentation Mode): Automatically adds security checks when Java loads classes (called bytecode manipulation).
  • AspectJ (AspectJ Mode): Automatically adds security checks in a second compilation step (called weaving).

Both implementations set up "checkpoints" that activate before the file operation happens, giving Ares a chance to verify whether the operation should be allowed or blocked. The validation logic is identical in both modes, but interception coverage differs slightly (AspectJ uses explicit pointcuts; instrumentation uses type-hierarchy maps).


1.4 What Are The Internal Configuration Settings?

Instructors define file system access policies in a policy file, and Ares 2 translates them into the following runtime settings (allowlists are folder prefixes; only paths below them are permitted):

SettingTypeDescriptionExample
aopModeStringThe used AOP implementation"INSTRUMENTATION" (Byte Buddy) or "ASPECTJ"
restrictedPackageStringThe package containing the student code (the code to be monitored)"de.student."
allowedListedClassesString[]The list of classes (usually test classes) that are exempt from supervision["de.student.util.Helper"]
pathsAllowedToBeReadString[]The list of folders that the student code can read files from["/tmp", "/home/student/input"]
pathsAllowedToBeOverwrittenString[]The list of folders that the student code can write files to["/tmp", "/home/student/input"]
pathsAllowedToBeCreatedString[]The list of folders that the student code can create files in["/tmp", "/home/student/input"]
pathsAllowedToBeExecutedString[]The list of folders that the student code can execute files in["/tmp", "/home/student/input"]
pathsAllowedToBeDeletedString[]The list of folders that the student code can delete files in["/tmp", "/home/student/input"]

1.5 When Is File Access Generally Blocked?

Access is BLOCKED 🔴 if ALL of the following conditions apply:

  1. Security enabled: aopMode is set to "INSTRUMENTATION" or "ASPECTJ"
  2. Student code detected: The call stack contains classes in restrictedPackage and not in allowedListedClasses
  3. Derived actions: The actions are derived from the intercepted method and any StandardOpenOption values (may include multiple actions)
  4. Path violation found: After method-specific parameter filtering, at least one extracted path (from parameters or attributes) does not match the list of allowed paths for its allowed actions
  5. Not exempt infrastructure access: The violating path is not an internal configuration/resource file of Ares and does not fall under one of the JVM-infrastructure exemptions (class-loading .class reads, system JAR reads, JDK-internal reads, native-library loads, JCE crypto-policy files, archive entry reads, root /). These exemptions apply at all check sites (parameters, receiver, and attributes).

Plain-language summary: if student code triggers a monitored file method and the path is outside the allowlist for the needed action, Ares blocks the access.

Access is ALLOWED 🟢 if ANY of the aforementioned conditions do not apply

In summary, Ares trusts code when:

  • It is located outside of the restrictedPackage
  • It is located inside of the restrictedPackage, but its classes are listed in allowedListedClasses within the student package
  • It is listed as Ares internal code

Security Assumptions:

  • Student code cannot modify Ares security settings (guaranteed by making settings private; reflection is disabled for student code)
  • Student code cannot interfere with security monitoring (guaranteed by making settings private; reflection is disabled for student code)
  • Student code executes after Ares is initialised (guaranteed by build pipeline)

2. Ares 2 AOP File System Access Control: Monitored File System Methods

2.1 Which Operations Does Ares 2 AOP File System Access Control Monitor?

Ares classifies file system interactions into five action types. These labels drive which allowlist is checked.

  • READ: Accessing file contents or metadata without modifying them (streams, read APIs, attribute queries).
  • OVERWRITE: Writing or mutating existing content/attributes (write/append/truncate, metadata setters).
  • CREATE: Creating new files, directories, or links (create* APIs, file system creation/open).
  • DELETE: Removing files or scheduling deletion/trash operations.
  • EXECUTE: In Ares, 'Execute' is a broad category covering execution-like file actions such as loading native libraries and opening files or URIs in external applications (for example, Runtime.load(...), System.loadLibrary(...), or Desktop.open(...)). Command execution APIs such as Runtime.exec(...) and ProcessBuilder.start(...) belong to the Command System.

Some APIs can appear under multiple actions because they imply more than one permission (for example, copy/move or StandardOpenOption combinations).


2.2 What Are The Monitored READ Operations?

Security Component: Read operation monitor

Monitored APIs:

Read APIs listed below access file contents or metadata without modifying them.

Note on "Tested by RP" column: A ✅ means that this application programming interface (API) is the primary target of a dedicated test in the Reproducibility Package. For example, if a test uses BufferedInputStream to wrap a FileInputStream, only the wrapper (BufferedInputStream.<new>) is marked as ✅, not the underlying FileInputStream.<new> which is merely a helper call in that context.

Reads any formatted file fully

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.io.FileInputStream<new>
java.io.BufferedInputStream<new>
java.io.RandomAccessFile<new>
java.nio.channels.AsynchronousFileChannelread❌ (triggers Thread security)
java.nio.channels.AsynchronousFileChannelopen❌ (triggers Thread security)
java.nio.channels.FileChannelopen
java.nio.channels.FileChannelmap
java.nio.file.FilesnewByteChannel
java.nio.file.FilesnewInputStream
java.nio.file.FilesreadAllBytes
java.lang.ClassLoadergetResourceAsStream❌ (triggers Reflection security)

Reads UTF-8 text/tokens fully

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.io.Reader<new>
java.nio.file.FilesnewBufferedReader
java.nio.file.FilesreadString
java.nio.file.Fileslines
java.nio.file.FilesreadAllLines
java.util.Scanner<new>

Reads only specifically formatted files fully

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.io.DataInputread
java.io.DataInputreadBoolean
java.io.DataInputreadByte
java.io.DataInputreadChar
java.io.DataInputreadDouble
java.io.DataInputreadFloat
java.io.DataInputreadFully
java.io.DataInputreadInt
java.io.DataInputreadLine
java.io.DataInputreadLong
java.io.DataInputreadShort
java.io.DataInputreadUTF
java.io.DataInputreadUnsignedByte
java.io.DataInputreadUnsignedShort
javax.imageio.ImageIOcreateImageInputStream
javax.imageio.ImageIOread
javax.sound.sampled.AudioSystemgetAudioInputStream
javax.xml.bind.Unmarshallerunmarshal
javax.xml.parsers.DocumentBuilderparse
javax.xml.parsers.SAXParserparse
java.awt.ToolkitcreateImage
java.awt.ToolkitgetImage
javax.imageio.ImageIOgetImageReaders
javax.sound.midi.MidiSystemgetSoundbank
java.awt.FontcreateFont
java.awt.FontcreateFonts
javax.imageio.stream.FileCacheImageInputStream<new>
javax.imageio.stream.FileImageInputStream<new>

Reads archive files (ZIP/JAR/GZIP)

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.util.zip.ZipInputStream<new>
java.util.zip.ZipInputStreamgetNextEntry
java.util.jar.JarInputStream<new>
java.util.jar.JarInputStreamgetNextJarEntry
java.util.zip.GZIPInputStream<new>
java.util.zip.ZipFile<new>
java.util.zip.ZipFileentries
java.util.zip.ZipFilegetInputStream
java.util.jar.JarFile<new>
java.util.jar.JarFileentries
java.util.jar.JarFilegetInputStream

Reads configuration/properties files

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.util.Propertiesload
java.util.PropertiesloadFromXML

Reads only specific parts of a file

Note: Generic InputStream.read() and Reader.read() calls are not monitored by either backend. These streams/readers are already validated at construction time (FileInputStream.<new>, Reader.<new>), so monitoring every subsequent read() call would be redundant. RandomAccessFile.read is intercepted by AspectJ, but Byte Buddy explicitly excludes it via ignoredMethodsByClass to avoid recursive self-interception during class and JAR loading in instrumentation mode; the RandomAccessFile.<new> constructor pointcut still covers the access in both backends.

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.io.RandomAccessFileread❌ (excluded, covered by <new>)
java.nio.channels.SeekableByteChannelread

Only reads the file hierarchy

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.io.FilenormalizedList
java.io.Filelist
java.io.FilelistFiles
java.io.FilelistRoots
java.nio.file.Filesfind
java.nio.file.Fileslist
java.nio.file.FilesnewDirectoryStream
java.nio.file.Fileswalk
java.nio.file.FileswalkFileTree
java.nio.file.spi.FileSystemProvidernewDirectoryStream

2.3 What Are The Monitored WRITE Operations?

Security Component: Write operation monitor

Monitored APIs:

Write APIs listed below modify existing content or attributes.

Note on "Tested by RP" column: A ✅ means that this API is the primary target of a dedicated test in the Reproducibility Package. For example, if a test uses BufferedWriter to wrap a FileWriter, only the wrapper (BufferedWriter.<new>) is marked as ✅, not the underlying FileWriter.<new> which is merely a helper call in that context.

Writes any format fully to a file

Note: FileChannel.open, AsynchronousFileChannel.open, and FileChannel.map do NOT have dedicated WRITE pointcuts. These methods are monitored via READ pointcuts, and the actual operation type is determined dynamically:

  • FileChannel.open / AsynchronousFileChannel.open: Classified based on OpenOption parameters via deriveActionChecks()
  • FileChannel.map: Classified based on MapMode parameter (e.g., READ_WRITE vs READ_ONLY)

Note on generic write/close/flush methods: Generic write(), close(), and flush() methods on stream classes (e.g., OutputStream.write(), Writer.flush()) are intentionally NOT monitored. Reason: System.out and System.err internally call these methods, which would cause false positives. File access is already blocked at the constructor level, making these further checks redundant.

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.io.FileOutputStream<new>
java.io.BufferedOutputStream<new>
java.io.RandomAccessFile<new>
java.nio.channels.AsynchronousFileChannelwrite
java.nio.channels.AsynchronousFileChannelopen❌ (via OpenOptions)❌ (via OpenOptions)
java.nio.channels.FileChannelopen❌ (via OpenOptions)❌ (via OpenOptions)
java.nio.channels.FileChannelmap❌ (via MapMode)❌ (via MapMode)
java.nio.channels.FileChannelwrite
java.nio.file.FilesnewByteChannel❌ (via OpenOptions)❌ (via OpenOptions)
java.nio.file.FilesnewOutputStream
java.nio.file.Fileswrite

Writes UTF-8 text/tokens fully

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.io.Writer<new>
java.nio.file.FilesnewBufferedWriter
java.nio.file.FileswriteString

Writes only specifically formatted files fully

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.io.DataOutputwriteBoolean
java.io.DataOutputwriteByte
java.io.DataOutputwriteBytes
java.io.DataOutputwriteChar
java.io.DataOutputwriteChars
java.io.DataOutputwriteDouble
java.io.DataOutputwriteFloat
java.io.DataOutputwriteInt
java.io.DataOutputwriteLong
java.io.DataOutputwriteShort
java.io.DataOutputwriteUTF
javax.imageio.ImageIOwrite
javax.imageio.ImageIOcreateImageOutputStream
javax.sound.sampled.AudioSystemwrite
javax.xml.bind.Marshallermarshal
javax.xml.transform.Transformertransform
java.io.PrintStream<new>
java.util.logging.FileHandler<new>
java.util.logging.FileHandlerpublish
java.util.logging.FileHandlerclose
java.util.zip.InflaterOutputStream<new>
javax.print.DocPrintJobprint

Writes archive files (ZIP/JAR/GZIP)

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.util.zip.ZipOutputStream<new>
java.util.zip.ZipOutputStreamputNextEntry
java.util.jar.JarOutputStream<new>
java.util.jar.JarOutputStreamputNextEntry
java.util.zip.GZIPOutputStream<new>
java.util.zip.ZipOutputStreamcloseEntry
java.util.jar.JarOutputStreamcloseEntry

Writes configuration/properties files

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.util.Propertiesstore
java.util.PropertiesstoreToXML
java.util.Formatter<new>

Writes only specific parts to a file

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.nio.channels.AsynchronousFileChanneltruncate
java.nio.channels.FileChanneltruncate
java.nio.channels.FileChanneltransferTo
java.nio.file.attribute.UserDefinedFileAttributeViewwrite

Only writes the file hierarchy (metadata/attributes)

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.io.FilesetExecutable
java.io.FilesetLastModified
java.io.FilesetReadOnly
java.io.FilesetReadable
java.io.FilesetWritable
java.io.FilerenameTo
java.nio.file.Filescopy
java.nio.file.Filesmove
java.nio.file.FilessetAttribute
java.nio.file.FilessetLastModifiedTime
java.nio.file.FilessetOwner
java.nio.file.FilessetPosixFilePermissions
java.nio.file.spi.FileSystemProvidercopy
java.nio.file.spi.FileSystemProvidermove
java.nio.file.spi.FileSystemProvidersetAttribute

2.4 What Are The Monitored CREATE Operations?

Security Component: Create operation monitor

Monitored APIs:

Link creation APIs and conditional creates (e.g., FileChannel.open with create options) are listed under Creates files.

Creates files

Note: FileChannel.open and AsynchronousFileChannel.open do NOT have dedicated CREATE pointcuts in either AspectJ or Byte Buddy. These methods are monitored via READ pointcuts, and the actual operation type (read/write/create) is determined dynamically by analysing the OpenOption parameters via deriveActionChecks(). When called with CREATE or CREATE_NEW options, they are classified as create operations at runtime.

Note: The two backends differ for buffered wrappers: AspectJ has BufferedOutputStream.<new> in its fileCreateMethods pointcut, whereas Byte Buddy monitors it only via the OVERWRITE map. BufferedWriter.<new> has no dedicated CREATE pointcut in either backend; it is intercepted through the Writer.<new> OVERWRITE pointcut (BufferedWriter extends Writer).

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.io.FilecreateNewFile
java.io.FilecreateTempFile
java.nio.file.FilescreateFile
java.nio.file.FilescreateTempFile
java.nio.file.FilescreateLink
java.nio.file.FilescreateSymbolicLink
java.io.BufferedOutputStream<new>❌ (via OVERWRITE pointcut)
java.io.BufferedWriter<new>❌ (via Writer.<new> OVERWRITE pointcut)❌ (via Writer.<new> OVERWRITE pointcut)
java.io.FileOutputStream<new>
java.io.FileWriter<new>
java.io.PrintWriter<new>
java.io.RandomAccessFile<new>
java.nio.file.FilesnewBufferedWriter
java.nio.file.FilesnewOutputStream
java.nio.channels.AsynchronousFileChannelopen❌ (via OpenOptions)❌ (via OpenOptions)
java.nio.channels.FileChannelopen❌ (via OpenOptions)❌ (via OpenOptions)

Creates folders

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.io.Filemkdir
java.io.Filemkdirs
java.nio.file.FilescreateDirectories
java.nio.file.FilescreateDirectory
java.nio.file.FilescreateTempDirectory
java.nio.file.spi.FileSystemProvidercreateDirectory

2.5 What Are The Monitored DELETE Operations?

Security Component: Delete operation monitor

Monitored APIs:

Delete APIs listed below can remove files and empty directories.

Delete files

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.io.Filedelete
java.nio.file.Filesdelete
java.nio.file.FilesdeleteIfExists
java.nio.file.spi.FileSystemProviderdelete
org.apache.commons.io.FileUtilsforceDelete
java.awt.DesktopmoveToTrash
java.io.FiledeleteOnExit

Delete folders

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.io.Filedelete
java.nio.file.Filesdelete
java.nio.file.FilesdeleteIfExists
java.nio.file.spi.FileSystemProviderdelete
org.apache.commons.io.FileUtilsforceDelete
java.awt.DesktopmoveToTrash
java.io.FiledeleteOnExit

Monitored in delete pointcuts too (can delete source file)

Note: Files.move is monitored under both WRITE and DELETE because it writes the destination and deletes the source. Files.copy is intentionally not in the delete pointcuts of either backend, because copying does not delete the source; it is monitored under WRITE only.

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.nio.file.Filesmove

2.6 What Are The Monitored EXECUTE Operations?

What does "Execute" mean? File system actions that trigger execution-like behaviour such as loading native libraries or opening files with their default programmes (e.g., Runtime.load(...), System.loadLibrary(...), or Desktop.open(...)). Process spawning (Runtime.exec(...), ProcessBuilder.start(...)) is handled by the Command System.

Security Component: Execute operation monitor

Monitored APIs:

Execute APIs listed below trigger execution-like behaviour on files.

Executes the file on the console (command line execution)

Note: ProcessBuilder.start() and Runtime.exec() are handled by the Command System rather than the File System in both AspectJ and Byte Buddy modes, as they execute commands rather than individual files. ProcessBuilder.startPipeline() has no direct pointcut in either the file system or the command system; it is only caught indirectly (an attribute-filter rule exists for it, and its internal thread creation triggers the Thread system). The File System pointcuts for execute only cover library loading (load/loadLibrary) and Desktop launches.

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.lang.Runtimeload
java.lang.RuntimeloadLibrary
java.lang.Systemload
java.lang.SystemloadLibrary

Opens files with default applications (Desktop integration)

Class (fully qualified)MethodPointcut in AspectJPointcut in Byte BuddyTested by RP
java.awt.Desktopopen
java.awt.Desktopedit
java.awt.Desktopprint
java.awt.Desktopbrowse
java.awt.DesktopbrowseFileDirectory

Note: Other Desktop methods such as mail, openHelpViewer, setDefaultMenuBar, setOpenFileHandler, and setOpenURIHandler are not intercepted by either backend. Both backends monitor exactly open, edit, print, browse, and browseFileDirectory (plus moveToTrash under DELETE).


3. Ares 2 AOP File System Access Control: Student Code Triggers the Access Control Check

When student code (any code within the configured restricted package) calls one of these monitored methods, Ares automatically performs a security check before the file operation executes.

Example:

// Student Code
package de.student.solution;

import java.nio.file.Files;
import java.nio.file.Path;

public class StudentSolution {
public void readFile() throws Exception {
// This call triggers JavaInstrumentationReadPathMethodAdvice
String content = Files.readString(Path.of("/etc/passwd"));
}
}

When the Files.readString(Path.of("/etc/passwd")) method is called, Ares intercepts the call:

  • Byte Buddy: Automatically runs a security check before the method executes (technical implementation: JavaInstrumentationReadPathMethodAdvice.onEnter())
  • AspectJ: Automatically runs a security check before the method executes (technical implementation: before() advice in JavaAspectJFileSystemAdviceDefinitions.aj)

Ares then checks whether the student is allowed to access Path.of("/etc/passwd") before the file is read.


4. Ares 2 AOP File System Access Control: Collected Information About the File Access

The security monitor collects information about what is happening: Which method is being called, what file path is being accessed, and where in the student code this is happening.

Collection Mechanisms:

  • Byte Buddy: Uses special Java annotations (@Advice) to automatically capture information about the intercepted method (technical implementation: JavaInstrumentationReadPathMethodAdvice.onEnter())
  • AspectJ: Receives method information automatically through a parameter object called JoinPoint (technical implementation: checkFileSystemInteraction() method)

Both approaches collect the same information:

What is collected:

  • Method information: Which method was called
  • Object state: Internal state of the object
  • Parameters: Values passed to the method

Why do we need all three types of information?

File paths can appear in different places depending on how the method is used:

  1. Method information is needed to identify which operation is attempted and apply special handling rules

    • Example: Some methods like Files.copy() need both source and destination paths checked
  2. Object state is needed because paths can be stored inside objects

    • Example: file.delete() - The path is in file.path field, not passed as parameter
  3. Parameters are needed because paths are often passed as method arguments

    • Example: Files.readString(Path.of("/etc/passwd")) - The path "/etc/passwd" is a parameter

What is NOT collected here: Whether the access is allowed or blocked - that determination happens in the next step (Section 5: Ares Validates the File Access)


4.1 What Is The Signature Of The Monitored File System Method?

1. What Information Do We Collect:

InformationTypeDescription
declaringTypeNameStringClass name where the method is defined. Example: "java.io.FileInputStream".
methodNameStringMethod name. Example: "read" or "<init>" for constructors.
methodSignatureStringMethod signature with parameter types. Example: "(Ljava/lang/String;)V" for a constructor taking a String. Reading signatures: (Ljava/lang/String;)V means "takes a String parameter, returns void (nothing)" - like a function signature: constructor(String fileName) → returns nothing.

💡 Method Signature Explained: (Ljava/lang/String;)V

  • ( = Parameter list begins
  • Ljava/lang/String; = Parameter of type String
  • ) = Parameter list ends
  • V = "void" (no return value)

More Examples:

  • ()V = no parameters, void return
  • (II)I = two int parameters, returns int
  • (Ljava/nio/file/Path;)Ljava/lang/String; = Path parameter, returns String

2. How Do We Collect This Information:

Byte Buddy (Instrumentation) Mode:

@Advice.OnMethodEnter
public static void onEnter(
@Advice.Origin("#t") String declaringTypeName, // Class name
@Advice.Origin("#m") String methodName, // Method name
@Advice.Origin("#s") String methodSignature // Full signature
) {
// Information is now available for validation
}

AspectJ Mode:

public void checkFileSystemInteraction(
String action,
JoinPoint thisJoinPoint
) {
String declaringTypeName = thisJoinPoint.getSignature().getDeclaringTypeName();
String methodName = thisJoinPoint.getSignature().getName();
String fullMethodSignature = formatSignature(thisJoinPoint.getSignature());
// Information is now available for validation
}

3. How All Method Information Is Used:

  • Identify which file operation was attempted
  • Look up special handling rules for specific methods
  • Distinguish between different versions of the same method (overloading)
  • Example: java.io.FileInputStream.<init>(Ljava/lang/String;)V → Identifies a FileInputStream constructor taking a String parameter

💡 Why formatSignature()? AspectJ's raw Signature.toLongString() prepends Java modifiers (e.g. "public transient ") and omits .<init> for constructors. The helper formatSignature() normalises the AspectJ join-point signature to the same <init>-style shape that the Byte Buddy (Instrumentation) side produces, so both backends report identical signatures.


4.2 What Are The Attribute Values Of The Object Of The Monitored File System Method?

1. What Information Do We Collect:

InformationTypeDescription
instanceObjectThe object on which the method is called (the this reference). null for constructors since the object does not exist yet.
attributesObject[]Array of the object's internal field values. The actual values stored in each field. Note: Empty for constructors since object does not exist yet.

2. How Do We Collect This Information:

Byte Buddy (Instrumentation) Mode:

@Advice.OnMethodEnter
public static void onEnter(
@Advice.This(optional = true) Object instance // The object on which method is called
) {
// Extract attributes using reflection (see below)
}

AspectJ Mode:

public void checkFileSystemInteraction(
String action,
JoinPoint thisJoinPoint
) {
Object instance = thisJoinPoint.getTarget(); // Get the object instance
// Extract attributes using reflection (see below)
}

Both modes then use identical attribute extraction (using Java's built-in ability to inspect object contents):

// For constructors, instance is null (object doesn't exist yet)
if (instance == null) {
return; // Skip for constructors
}

// Ares uses Java's inspection capabilities to access private fields
final Field[] fields = instance.getClass().getDeclaredFields();
final Object[] attributes = new Object[fields.length];

for (int i = 0; i < fields.length; i++) {
try {
fields[i].setAccessible(true); // Make private fields accessible
attributes[i] = fields[i].get(instance); // Read the value
} catch (InaccessibleObjectException | IllegalAccessException | SecurityException
| IllegalArgumentException | NullPointerException | ExceptionInInitializerError e) {
// Skip an unreadable field rather than aborting the whole interaction:
// a JDK-internal field that throws on read must not turn a JDK-side
// reflection limit into an Ares SecurityException. The check still runs
// over the parameters and the readable fields.
continue;
}
}

3. How All Object State Information Is Used:

  • Extract file paths that might be stored inside the object
  • Check object fields for security violations
  • Access internal state even if not passed as parameters
  • Example: File object with path field = "/etc/passwd" → Path extracted from attributes array → Checked against allowed paths

4.3 What Are The Parameter Values Of The Monitored File System Method?

1. What Information Do We Collect:

InformationTypeDescription
parametersObject[]Method arguments - the values passed to the method when it was called.

2. How Do We Collect This Information:

Byte Buddy (Instrumentation) Mode:

@Advice.OnMethodEnter
public static void onEnter(
@Advice.AllArguments Object[] parameters // All method arguments
) {
// Parameters array contains all values passed to the method
}

AspectJ Mode:

public void checkFileSystemInteraction(
String action,
JoinPoint thisJoinPoint
) {
Object[] parameters = thisJoinPoint.getArgs(); // Get method arguments
}

3. How All Parameter Information Is Used:

  • Extract file paths from method arguments
  • Convert paths to standard format (Path.normalize().toAbsolutePath())
  • Check extracted paths against allowed paths list
  • Example: Files.readString(Path.of("/etc/passwd")) → parameters = [Path.of("/etc/passwd")] → Checked against allowed paths

4.4 Which Information Is Passed To The Respective Security Validator?

After collecting this information, Ares passes it to the security validation component.

💡 Concrete Example: Files.readString(Path.of("/etc/passwd"))

Collected Information:
├─ action: "read"
├─ declaringTypeName: "java.nio.file.Files"
├─ methodName: "readString"
├─ methodSignature: "(Ljava/nio/file/Path;)Ljava/lang/String;"
├─ parameters: [Path.of("/etc/passwd")]
├─ attributes: [] (not applicable for static method)
└─ instance: null (static method has no instance)

Where does the action type come from?

The action type (e.g., "read") is automatically determined based on which method was intercepted:

Intercepted Method ExampleAction Type
FileInputStream.<new>, Files.readString()"read"
FileChannel.write(), Files.write()"overwrite"
File.createNewFile(), Files.createFile()"create"
File.delete(), Files.delete()"delete"
Runtime.load(), Desktop.open()"execute"

Note: Streams are checked at construction time (FileInputStream.<new>), not on every read()/write() call. Command execution (Runtime.exec(), ProcessBuilder.start()) is handled by the separate Command System, not by the file system's "execute" action.

For File System Operations:

checkFileSystemInteraction(
"read", // What type of operation? (from table above)
declaringTypeName, // Which class?
methodName, // Which method?
methodSignature, // Exact signature?
attributes, // Object's internal field values (Object[])
parameters, // Values passed to method (Object[])
instance // The object instance (for additional context)
)

How is the action determined?

The action type is hardcoded based on which methods are intercepted:

Byte Buddy (Instrumentation Mode) - Separate advice classes:

  • JavaInstrumentationReadPathMethodAdvice → Uses "read"
  • JavaInstrumentationOverwritePathMethodAdvice → Uses "overwrite"
  • JavaInstrumentationCreatePathMethodAdvice → Uses "create"
  • JavaInstrumentationDeletePathMethodAdvice → Uses "delete"
  • JavaInstrumentationExecutePathMethodAdvice → Uses "execute"

AspectJ Mode - Multiple before() advice in one aspect:

  • before(): fileReadMethods() → Uses "read"
  • before(): fileWriteMethods() → Uses "overwrite"
  • before(): fileCreateMethods() → Uses "create"
  • before(): fileDeleteMethods() → Uses "delete"
  • before(): fileExecuteMethods() → Uses "execute"

Possible action values:

  • "read" - Reading from files
  • "overwrite" - Writing to or modifying files
  • "create" - Creating new files or directories
  • "delete" - Deleting files or directories
  • "execute" - Executing files or opening them with external programs

5. Ares 2 AOP File System Access Control: Blocking Or Allowing The File Access

The security validator performs a series of checks to decide whether the file operation should be allowed or blocked.

The 5 Checks in Order (stops at first "Allow"):

  1. Is Security Enabled? → If no: 🟢
  2. Does the Call Come from Student Code? → If no: 🟢
  3. Which Permissions Need to Be Checked? → Determine permission list
  4. Are All Affected Paths Allowed? → If yes: 🟢, If no: 🔴
  5. Block and Throw Error → Determine error message

5.1 Check 1: Is A Respective AOP Mode Enabled Or Is AOP Fully Disabled?

1. Purpose

Verify that file system security monitoring is turned on. This check ensures that Ares only performs security validations when explicitly enabled. Without this check, the security system would either always run (causing unnecessary overhead) or never run (leaving the system unprotected). The configuration-based approach allows instructors to enable or disable security monitoring as needed.

2. How it works

Each backend checks the aopMode setting against its own value only, so at most one backend is ever active:

Byte Buddy (Instrumentation) Mode:

String aopMode = getValueFromSettings("aopMode");
if (aopMode == null || aopMode.isEmpty() || !aopMode.equals("INSTRUMENTATION")) {
return; // This backend is not active, allow everything
}
String restrictedPackage = getValueFromSettings("restrictedPackage");
if (restrictedPackage == null || restrictedPackage.isEmpty()) {
return; // No student package configured yet
}
if (isProjectSourcesFinderInProgress()) {
return; // Ares itself is reading framework support files
}

AspectJ Mode:

String aopMode = getValueFromSettings("aopMode");
if (aopMode == null || !aopMode.equals("ASPECTJ")) {
return; // This backend is not active, allow everything
}
if (isProjectSourcesFinderInProgress()) {
return; // Ares itself is reading framework support files
}

The instrumentation backend returns early when restrictedPackage is null or empty; the AspectJ backend instead null-guards restrictedPackage later, before the call-stack check. Both backends skip validation while Ares's own trusted setup utilities are reading framework support files (isProjectSourcesFinderInProgress()).

3. Used variables

  • aopMode (String): Configuration setting that determines whether security monitoring is active. Must be set to "INSTRUMENTATION" (Byte Buddy) or "ASPECTJ" (AspectJ) to enable file system security checks. Retrieved from the configuration settings.

4. Result

  • Security enabled → 🌕 Continue to Check 2
  • Security disabled → 🟢 Allow operation (no restrictions - analysis terminated)

5.2 Check 2: Is the Caller Of The Monitored File System Method The Monitored Student Code?

This check determines whether the file operation was triggered by restricted student code or by trusted framework code. It consists of three sub-steps:

5.2.1 Load Configuration

1. Purpose

Load the security configuration that defines which code is considered "student code" and which helper classes are trusted. This configuration is essential because not all code within a student project should be restricted - some utility classes provided by instructors should remain accessible. The configuration allows instructors to customise the security boundaries for each exercise.

2. How it works

String restrictedPackage = getValueFromSettings("restrictedPackage");
String[] allowedClasses = getValueFromSettings("allowedListedClasses");

3. Used variables

  • restrictedPackage (String): The Java package prefix where student code is located (e.g., "de.student."). Any code within this package is considered restricted unless explicitly allowed.
  • allowedClasses (String[]): List of trusted helper class names that students can use even though they are in the restricted package (e.g., ["de.student.util.SafeHelper"]). These classes are pre-approved by instructors.

4. Result

Configuration loaded → 🌕 Continue to 5.2.2

5.2.2 Analyse the Call Chain

1. Purpose

Walk through the call history to find if restricted student code triggered the file operation. This is like following breadcrumbs backwards to see how we got here. This is the core security check that distinguishes between legitimate framework operations (e.g., JUnit loading test classes) and potentially malicious student code (e.g., trying to read sensitive files).

💡 Analogy: Like a detective following footprints backwards:

  • Crime Scene: Files.readString("/etc/passwd") [we are here now]
  • Step Back: StudentCode.readSecretFile() [AHA! Student code found! 🔴]
  • Further Back: TestClass.testStudent() [this is the test]
  • Origin: JUnit Framework [trustworthy ✓]

Result: Student code attempted to read a forbidden file!

Visual Example - Walking the Call History:

[Top] Files.readString("/etc/passwd") ← Current method being called
[...] StudentCode.readSecretFile() ← Found student code! ✓
[...] StudentCode.exploit() ← Still in student package
[...] TestClass.testStudent() ← Test method (outside student package)
[Bottom] JUnit framework

Result: Student code detected at StudentCode.readSecretFile()

2. How it works

String violatingMethod = checkIfCallstackCriteriaIsViolated(
restrictedPackage, allowedClasses, declaringTypeName, methodName);
if (violatingMethod == null) {
return; // Not from student code
}

Detailed steps:

  1. Walk the Call History Once (Lazily): Instead of materializing a full StackTraceElement[] via Thread.currentThread().getStackTrace(), both backends use a cached StackWalker that streams the frames lazily and stops as soon as the needed frames are found. The single-pass helper inspectCallstackOnce(restrictedPackage, allowedClasses) finds both the violating student frame and the first non-ignored caller above the first restricted frame in one walk:

    String[] inspection = inspectCallstackOnce(restrictedPackage, allowedClasses);
  2. Skip Ares Internal Code, Class Loading, and Reflection Trampolines: Inside the walk, every frame whose class name starts with an IGNORE_CALLSTACK prefix is skipped:

    boolean ignorable = false;
    for (String ignore : IGNORE_CALLSTACK) {
    if (className.startsWith(ignore)) {
    ignorable = true;
    break;
    }
    }
    if (ignorable) {
    continue; // Part of Ares, class loading, debugging, or reflection internals
    }
  3. Check if a Frame is Student Code:

    boolean inRestricted = className.startsWith(restrictedPackage);
  4. Check if it is an Allowed Helper Class (inline prefix loop):

    boolean allowed = false;
    if (allowedClasses != null) {
    for (String allowedClass : allowedClasses) {
    if (className.startsWith(allowedClass)) {
    allowed = true;
    break;
    }
    }
    }
    if (!allowed) {
    violation = className + "." + frame.getMethodName(); // Restricted student code
    }
  5. Cache the Result Per Thread: The result (violating frame plus the caller above the first restricted frame) is stored in a per-thread cache so the immediately following lookup of the calling test method (5.2.3) does not need a second stack walk. If no restricted frame is found, null is returned (no student code in the call chain).

3. Used variables

  • restrictedPackage (String): From 5.2.1 - defines student code boundary
  • allowedClasses (String[]): From 5.2.1 - list of trusted helper classes
  • violatingMethod (String): Returns the fully qualified method name of the student code that triggered the file operation, or null if no student code found
  • declaringTypeName, methodName (String): The intercepted class and method, passed through to checkIfCallstackCriteriaIsViolated(restrictedPackage, allowedClasses, declaringTypeName, methodName) for diagnostics
  • STACK_WALKER (StackWalker): Cached walker that streams the call chain lazily instead of materializing a full StackTraceElement[]
  • IGNORE_CALLSTACK (List): Identical in both backends: ["java.lang.ClassLoader", "de.tum.cit.ase.ares.api.", "com.intellij.rt.debugger.", "jdk.internal.loader.", "jdk.internal.reflect."]
  • className (String): The fully qualified class name for each method in the call stack

4. Result

  • Found student code calling the file operation → Returns method name like "de.student.StudentCode.exploit" → 🌕 Continue to 5.2.3
  • No student code found in call chain → Returns null → 🟢 Allow operation (called from test framework or test code - analysis terminated)

5.2.3 Find Which Test Called the Student Code

1. Purpose

Identify which test method triggered the student code. This helps instructors know which test method revealed the security violation.

2. How it works

String testMethod = findFirstMethodOutsideOfRestrictedPackage(restrictedPackage);

Continue walking backwards through the call history (from 5.2.2) to find the first method outside the student package - this is the test method that called the student code.

Example from the visual diagram above:

[...] StudentCode.exploit() ← Student code (found in 5.2.2)
[...] TestClass.testStudent() ← FOUND: First method outside student package

Result: "org.junit.TestClass.testStudent" - this is the test method that invoked the student code

3. Used variables

  • restrictedPackage (String): From 5.2.1 - used to identify where student code ends and test code begins
  • testMethod (String): The fully qualified name of the test method that invoked the student code (e.g., "org.junit.TestClass.testStudent")

4. Result

Test method identified → Stored for error message → 🌕 Continue to Check 3


5.3 Check 3: Which Operations Does The Monitored File System Method Wants To Conduct?

1. Purpose

Determine which security actions to validate based on method parameters. Real-world analogy: Like a door that needs both a key AND a fingerprint scan - some file operations need multiple permissions checked simultaneously.

Most methods need just one permission (e.g., "read"), but some need multiple (e.g., a READ+WRITE channel is validated against both the read and the overwrite allowlist). This check analyses the operation mode to determine which permission types need validation.

2. How it works

List<Map.Entry<String, Boolean>> actionsToValidate = deriveActionChecks(action, declaringTypeName, parameters);

How It Works:

  1. Search for StandardOpenOption in parameters:

    • If found, map each option to corresponding actions
    • Apply semantic prioritisation rules (see below) so the checked actions match the caller's intent
  2. If no StandardOpenOption found:

    • Use the action parameter from the advice class (e.g., "read", "overwrite")
    • Special handling: RandomAccessFile mode strings (see Section 6.2) and legacy append=false booleans (FileWriter, FileOutputStream, PrintWriter), which turn "create" into "overwrite"
  3. Return list of actions with non-existence flags:

    • Each entry: (action, canBeNonExistent)
    • Example: [("overwrite", true)]

Semantic prioritisation rules:

  • DELETE_ON_CLOSE always wins: whenever it is present, the operation is treated as a pure ("delete", false) check, regardless of any other options or of which pointcut intercepted the call.
  • CREATE_NEW + write option → create only: the primary intent is creating a new file; the WRITE option is just an implementation detail, so the result is [("create", true)] and no "overwrite" check is added.
  • CREATE + write option → single overwrite check: "write to file, creating it if absent" is semantically an overwrite, so "create" and "overwrite" are merged into one ("overwrite", true) check (the true keeps non-existing paths validated).
  • READ is always validated separately: a READ+WRITE channel can read file contents, so a file that is overwrite-allowed but not read-allowed must not be readable through it.

Example with StandardOpenOption:

Files.write(path, data, StandardOpenOption.CREATE, StandardOpenOption.WRITE);

CREATE and WRITE are first mapped to "create" and "overwrite", then merged into a single ("overwrite", true) check by the create+overwrite rule above.

Example without StandardOpenOption:

Files.readString(path);

This uses the default action "read" from JavaInstrumentationReadPathMethodAdvice.

💡 For Beginners: In 90% of cases, only one permission is checked (derived from the method name, e.g., Files.readString() → "read"). Only for complex operations like Files.write() with multiple modes are multiple permissions checked simultaneously.

Mapping Rules with Everyday Examples:

File Opening ModePermission NeededCan Path Be Non-Existent?Everyday Example
CREATE, CREATE_NEW"create"YesCreate new Word file
WRITE, APPEND, TRUNCATE_EXISTING"overwrite"NoEdit/overwrite existing file
READ"read"NoOpen file for reading
DELETE_ON_CLOSE"delete"NoTemporary file deleted on close

Why Can CREATE Paths Be Non-Existent? When creating a new file, the file does not exist yet. The security check must validate the path before the file is created.

Multiple Permissions: If multiple modes are specified, all corresponding permissions are checked after the semantic prioritisation rules above have been applied (e.g., DELETE_ON_CLOSE collapses everything to "delete", and create+overwrite is merged into a single "overwrite" check).

Default: If no StandardOpenOption found, uses the action parameter passed to checkFileSystemInteraction() based on which method was intercepted (e.g., FileInputStream"read").

3. Used variables

  • action (String): The base action type from section 4.4 (e.g., "read", "overwrite", "create", "delete", "execute")
  • parameters (Object[]): Method parameters that may contain StandardOpenOption values
  • actionsToValidate (List<Map.Entry<String, Boolean>>): List of action-permission pairs to check. The Boolean indicates whether the path can be non-existent for this action.

4. Result

List of actions to validate (e.g., [("overwrite", true)] for CREATE+WRITE, or [("read", false), ("overwrite", false)] for READ+WRITE) → 🌕 Continue to Check 4


5.4 Check 4: Which Paths Does The Monitored File System Method Wants To Access?

This check finds all file paths involved in the operation and validates them against the allowed paths list. It consists of five sub-steps:

Overview of the 5 Steps:

  1. 5.4.1 Load list of allowed paths (e.g., ["/tmp", "/home/student/output"])
  2. 5.4.2 Apply special method rules (ignore some parameters)
  3. 5.4.3 Extract paths from parameters and check against list
  4. 5.4.4 Extract paths from object state and check against list
  5. 5.4.5 Exception for Ares-internal files (so Ares itself can function)

5.4.1 Load List of Allowed Paths

1. Purpose

Load the configuration that specifies which file paths are allowed for the current operation type. Each action type (read, write, create, delete, execute) has its own allowlist of permitted paths. This separation allows instructors to grant fine-grained permissions - for example, students might be allowed to read from /input but only write to /output. Without action-specific path lists, the system would need to either allow all paths (insecure) or use one restrictive list for all operations (too limiting).

2. How it works

String[] allowedPaths = getValueFromSettings(
switch (action) {
case "read" -> "pathsAllowedToBeRead";
case "overwrite" -> "pathsAllowedToBeOverwritten";
case "create" -> "pathsAllowedToBeCreated";
case "execute" -> "pathsAllowedToBeExecuted";
case "delete" -> "pathsAllowedToBeDeleted";
}
);

3. Used variables

  • action (String): The action type from 5.3 (e.g., "read", "overwrite", "create", "delete", "execute")
  • allowedPaths (String[]): Array of file path prefixes that are allowed for this action type. Paths from configuration like ["/tmp", "/home/student/output"]

4. Result

Allowed paths list loaded → 🌕 Continue to 5.4.2

5.4.2 Apply Special Rules for Specific Methods

1. Purpose

Apply method-specific rules to determine which parameters or object fields should be checked. Some methods have complex signatures where not all parameters represent file paths. These special rules prevent false positives while maintaining security.

2. How it works

There are two ignore maps: one for the object's attributes and one for the method parameters.

// Attribute side
IgnoreValues attributeIgnoreRule = FILE_SYSTEM_IGNORE_ATTRIBUTES_EXCEPT.getOrDefault(
declaringTypeName + "." + methodName,
IgnoreValues.NONE
);
Object[] filteredAttributes = filterVariables(attributes, attributeIgnoreRule);

// Parameter side
IgnoreValues parameterIgnoreRule = FILE_SYSTEM_IGNORE_PARAMETERS_EXCEPT.getOrDefault(
declaringTypeName + "." + methodName,
IgnoreValues.NONE
);
Object[] filteredParameters = filterVariables(parameters, parameterIgnoreRule);

Current file system special cases (attribute-based, FILE_SYSTEM_IGNORE_ATTRIBUTES_EXCEPT):

MethodWhat We CheckWhy
File.delete()Only the path field of the File objectThe path is stored in the File object, not passed as a parameter
File.deleteOnExit()Only the path field of the File objectThe path is stored in the File object, not passed as a parameter
File.createNewFile()Only the path field of the File objectThe path is stored in the File object, not passed as a parameter
ProcessBuilder.start()Only the command fieldOnly the command field carries the executable path; other fields (environment, redirects) are irrelevant
ProcessBuilder.startPipeline()Only the command fieldSame as ProcessBuilder.start()

Current file system special cases (parameter-based, FILE_SYSTEM_IGNORE_PARAMETERS_EXCEPT):

MethodWhat We CheckWhy
Files.createTempFile(dir, prefix, suffix, ...)Only parameter 0 (the directory Path)Prefix/suffix strings are not paths
Files.writeString(path, csq, ...)Only parameter 0 (the Path)The written content is not a path
Files.write(path, bytes, ...)Only parameter 0 (the Path)The written content is not a path
Files.readString(path, cs)Only parameter 0 (the Path)The charset is not a path
File.createTempFile(prefix, suffix)Nothing (all parameters ignored)There is no path parameter at all
Runtime.exec(cmd, ...)Only parameter 0 (the command)Flags like "-c" are not paths
RandomAccessFile.<new>(file, mode)Only parameter 0 (the file)The mode string ("r"/"rw"/...) is not a path
DataOutputStream.writeUTF/writeChars/writeBytes(String)Nothing (all parameters ignored)The argument is payload, never a path; the underlying file was validated when the FileOutputStream was opened
PrintStream.<new>(sink, ...)Only parameter 0 (the sink)A charset name like "UTF-8" is not a path

4. Result

Filtered variables ready for path validation → 🌕 Continue to 5.4.3

5.4.3 Check Method Parameters for File Paths

1. Purpose

Extract and validate all file paths from method parameters. This step systematically extracts paths from all parameter types and validates each against the allowed paths list.

2. How it works

// 1. Filter parameters based on special rules (FILE_SYSTEM_IGNORE_PARAMETERS_EXCEPT, see 5.4.2)
Object[] filteredVariables = filterVariables(parameters, parameterIgnoreRule);

// 2. Extract a path candidate from each parameter
for (Object variable : filteredVariables) {
Path actualPath = variableToPath(variable); // String, Path, File, or file:// URI/URL
if (actualPath == null) {
continue; // Not path-like (arrays/Lists are recursed into element by element)
}

// 3. Canonicalise the candidate FIRST (symlink resolution, TOCTOU defence)
Path candidate;
if (Files.exists(actualPath, LinkOption.NOFOLLOW_LINKS)) {
// Follow ALL symlinks to the real target - deliberately WITHOUT
// NOFOLLOW_LINKS, so a symlink inside an allowed folder cannot
// point outside the sandbox undetected.
candidate = actualPath.toRealPath();
} else {
// Non-existing target (e.g. a file about to be created): resolve
// symlinks in the deepest EXISTING ancestor and re-append the
// remaining segments, so a symlinked parent directory cannot
// redirect the create/overwrite outside the allowlist.
candidate = resolveExistingAncestorRealPath(actualPath.normalize().toAbsolutePath());
}

// 4. Compare against every allowed path prefix
boolean hasAllowedPrefix = false;
for (String allowedPathString : allowedPaths) {
Path allowedPath = variableToPath(allowedPathString);
// Allowed paths are canonicalised the same way: toRealPath() when they
// exist, otherwise via their deepest existing ancestor. Non-existing
// policy entries are NOT skipped - a rule like "allow protected/file.txt"
// must hold even before the file is created.
if (pathMatches(candidate, allowedPath, allowNonExistingPaths)) {
hasAllowedPrefix = true;
break;
}
}
if (!hasAllowedPrefix) {
// Path is NOT allowed → Record violation
}
}

3. Used variables

  • parameters (Object[]): From section 4.3 - all method parameters
  • parameterIgnoreRule (IgnoreValues): From 5.4.2 (FILE_SYSTEM_IGNORE_PARAMETERS_EXCEPT) - determines which parameters to check
  • filteredVariables (Object[]): From 5.4.2 - subset of parameters to validate
  • allowedPaths (String[]): From 5.4.1 - list of allowed path prefixes
  • candidate (Path): Canonicalised path of the file being accessed (all symlinks resolved via toRealPath(), or via the deepest existing ancestor when the target does not exist yet)
  • allowNonExistingPaths (boolean): Whether missing paths are allowed for this action (e.g., create/delete)
  • allowedPath (Path): Canonicalised allowed path prefix (same resolution rules as the candidate)
  • variableToPath() (method): Helper that converts String, Path, File, and URI/URL values with a file scheme to a normalised absolute Path; other types (and non-file URIs/URLs) are ignored
  • resolveExistingAncestorRealPath() (method): Helper that resolves symlinks in the deepest existing ancestor of a non-existing path and re-appends the remaining segments
  • pathMatches() (method): Helper that canonicalises the allowed path and checks whether the candidate starts with it

4. Result

  • All paths allowed → 🌕 Continue to 5.4.4
  • Forbidden path found → Record violation → 🌕 Continue to 5.4.5 (check if Ares internal)

5.4.4 Check Object State for File Paths

1. Purpose

Extract and validate all file paths from the object's internal state. This step systematically extracts paths from all object's internal state types and validates each against the allowed paths list.

2. How it works (attribute-based violations only)

Same process as checking parameters (Section 5.4.3 above), but we examine the object's internal field values (from Section 4.2) instead of method parameters. The path normalisation and validation logic is identical.

3. Used variables

  • attributes (Field[]): From section 4.2 - the object's internal fields
  • attributeValues (Object[]): From section 4.2 - values of the object's fields
  • ignoreRule (IgnoreValues): From 5.4.2 - determines which object fields to check
  • allowedPaths (String[]): From 5.4.1 - list of allowed path prefixes

4. Result

  • All paths allowed → 🌕 Continue to 5.4.5
  • Forbidden path found → Record violation → 🌕 Continue to 5.4.5 (check if Ares internal)

5.4.5 Allow Ares Internal Files

1. Purpose

Allow Ares to access its own configuration and resource files. Ares needs to read its own files (localization messages, configuration, internal classes) to function properly. Without this exception, Ares would block itself from accessing necessary resources. This allowlist ensures that only genuine Ares internal files are exempted, not files that students might name similarly to bypass security.

2. How it works

boolean isInternalAllowed = false;
for (String suffix : INTERNAL_PATH_SUFFIXES) {
if (pathViolation.endsWith(suffix)) {
isInternalAllowed = true;
break;
}
}

if (!isInternalAllowed) {
throw SecurityException;
}

This exemption is applied at all three check sites: parameter-based, receiver-based, and attribute-based violations.

Ares Internal Files (INTERNAL_PATH_SUFFIXES, 6 entries):

  • "ares/api/localization/Messages.class"
  • "ares/api/localization/messages.class"
  • "ares/api/localization/messages.properties"
  • "ares/api/util/LruCache.class"
  • "ares/api/configuration/essentialFiles/java/EssentialPackages.yaml"
  • "ares/api/configuration/essentialFiles/java/EssentialClasses.yaml"

Further infrastructure exemptions: Besides Ares's own files, a flagged path is allowed when the access is Java Virtual Machine (JVM)/library infrastructure rather than student file access:

  • .class reads performed by the class-loading machinery (a class-loader frame is on the stack, or the caller is Class.forName/ClassLoader)
  • .jar reads from system infrastructure, meaning the Maven local repository or the Java Development Kit (JDK) installation under java.home
  • JDK-internal reads under java.home and native-library loads (.dylib/.jnilib/.so/.dll)
  • JCE crypto-policy files read during Transport Layer Security (TLS)/cryptography initialisation
  • Entry reads on an already-open JarFile/ZipFile (the constructor is NOT exempt and still validates its path)
  • The root path "/" when found in object attributes (a side effect of class resolution)

3. Used variables

  • pathViolation (String): The file path that was flagged as forbidden in 5.4.3 (parameters), the receiver check, or 5.4.4 (attributes)
  • INTERNAL_PATH_SUFFIXES (Set): Predefined list of Ares internal file path suffixes that should always be allowed
  • isInternalAllowed (boolean): true if the path ends with an Ares internal file suffix or matches one of the infrastructure exemptions, false otherwise

4. Result

  • Forbidden path found (not an Ares internal file) → 🔴 Block and throw security exception (analysis terminated - forbidden file access detected)
  • All paths allowed → 🟢 Allow the file operation (analysis terminated - no forbidden access detected)

5.5 Check 5: Block Access with Detailed Error Message

🔴 Security Exception Thrown - Analysis Terminated

1. Purpose

Block the forbidden file operation and provide a comprehensive error message. When a security violation is detected, it is crucial to give instructors and students clear information about what went wrong, where it happened, and which test triggered it. A generic "access denied" message would be unhelpful for debugging. The detailed message helps instructors identify the exact violation and helps students understand which part of their code caused the security issue.

2. How it works

throw new SecurityException(localize(
"security.advice.illegal.file.execution",
violatingMethod, // de.student.StudentCode.exploit
messageAction, // "read" (the reported verb; "create" is aliased to
// "overwrite" for truncating writers, see below)
violatingPath, // "/etc/passwd"
fullMethodSignature // "java.io.FileInputStream.<init>(Ljava/lang/String;)V"
+ (studentCalledMethod == null ? "" : " (called by " + studentCalledMethod + ")")
+ " | " + buildDenialReason(noAllowRuleConfigured)
));

3. Used variables

  • violatingMethod (String): From 5.2.2 - the student method that attempted the file access (e.g., "de.student.StudentCode.exploit")
  • messageAction (String): The reported operation verb. Usually the checked action from 5.3, but when a "create" pointcut intercepts a class/method that semantically truncates/overwrites (FileOutputStream, FileWriter, PrintWriter, or Files.newBufferedWriter/newOutputStream without append=true), "create" is aliased to "overwrite" in the message
  • violatingPath (String): From 5.4.3/5.4.4 - the forbidden file path that was accessed (e.g., "/etc/passwd")
  • fullMethodSignature (String): From section 4.1 - complete method signature showing exactly which method was called (e.g., "java.io.FileInputStream.<init>(Ljava/lang/String;)V")
  • studentCalledMethod (String): From 5.2.3 - the test method that invoked the student code (e.g., "org.junit.TestClass.testStudent"); omitted from the message when null
  • buildDenialReason() (method): Appends why the access was denied - either no allow rule was configured for this action at all, or a rule exists but does not permit this path
  • localize() (method): Translates the error message to the configured language

4. Result

Example Error Message (a single line, from the security.advice.illegal.file.execution template "... %s tried to illegally %s File %s via %s but was blocked by Ares."):

Ares Security Error (Reason: Student-Code; Stage: Execution): de.student.StudentCode.exploit tried to illegally read File /etc/passwd via java.nio.file.Files.readString(java.nio.file.Path) (called by org.junit.TestClass.testStudent) | <denial reason> but was blocked by Ares.

🔴 SecurityException thrown - Analysis terminated, file operation blocked


6. Ares 2 AOP File System Access Control: Operation Type Classification

This section explains why the detected operation type may differ from the intuitively expected operation based on the API being tested. Understanding these categories is essential for correctly configuring security expectations in test scenarios.

6.1 Category A: OpenOptions Prioritisation

Problem: When multiple StandardOpenOption values are passed to NIO methods, certain options take precedence over others in the deriveActionChecks() method.

Technical Background: The deriveActionChecks() method first applies two overriding rules, then maps the remaining options individually:

  1. DELETE_ON_CLOSE has the highest priority: when present, the operation is always classified as ("delete", false), regardless of any other options or of which pointcut intercepted the call.
  2. CREATE_NEW + a write option (WRITE/APPEND/TRUNCATE_EXISTING) → create only: the primary intent is creating a new file, so the result is [("create", true)].
  3. Otherwise, each option is mapped individually: CREATE/CREATE_NEW → "create" (non-existing allowed), WRITE/APPEND/TRUNCATE_EXISTING → "overwrite", READ → "read", DELETE_ON_CLOSE → "delete". Afterwards, a resulting create+overwrite combination (e.g. from plain CREATE+WRITE) is merged into a single ("overwrite", true) check.

The AspectJ backend (JavaAspectJFileSystemAdviceDefinitions.aj) implements the per-option mapping with a switch statement and Map.merge:

for (StandardOpenOption option : options) {
switch (option) {
case CREATE:
case CREATE_NEW:
actions.merge("create", true, Boolean::logicalOr);
break;
case WRITE:
case APPEND:
case TRUNCATE_EXISTING:
actions.merge("overwrite", false, Boolean::logicalOr);
break;
case READ:
actions.merge("read", false, Boolean::logicalOr);
break;
case DELETE_ON_CLOSE:
actions.merge("delete", false, Boolean::logicalOr);
break;
default:
break;
}
}

The Byte Buddy backend (JavaInstrumentationAdviceFileSystemToolbox.java) implements the identical logic with an if-else chain over option.name() plus a mergeBoolean() helper instead of switch/Map.merge. This is deliberate: a switch on an enum and method references would generate synthetic inner classes (SwitchMap, lambda classes) that may not be present in the agent JAR and would cause NoClassDefFoundError at runtime.

Consequence: The reported operation reflects the derived intent, not the raw options. CREATE_NEW+WRITE yields only a "create" check; plain CREATE+WRITE yields a single "overwrite" check (with non-existing paths still validated); DELETE_ON_CLOSE always yields a "delete" check.

Affected Test Cases:

TestExpected IntentDetected OperationReason
FileSystemCreateAccess#7createcreateFiles.newByteChannel(CREATE_NEW, WRITE) - CREATE_NEW+WRITE is prioritised as create
FileSystemCreateAccess#8createcreateFileChannel.open(CREATE_NEW, WRITE) - CREATE_NEW+WRITE is prioritised as create
FileSystemDeleteAccess#7deletedeleteFiles.newByteChannel(DELETE_ON_CLOSE, WRITE) - DELETE_ON_CLOSE has highest priority
FileSystemDeleteAccess#8deletedeleteFileChannel.open(DELETE_ON_CLOSE, WRITE) - DELETE_ON_CLOSE has highest priority
FileSystemWriteAccess#14overwritereadMappedByteBuffer requires FileChannel.open(READ) first

6.2 Category B: RandomAccessFile Mode Detection

Problem: RandomAccessFile uses mode strings ("r", "rw", "rws", "rwd") instead of StandardOpenOption, requiring special handling.

Technical Background: The getRandomAccessFileModeAction() method maps mode strings to security actions:

private static String getRandomAccessFileModeAction(Object[] parameters, String defaultAction) {
if (parameters == null || parameters.length < 2) {
return null;
}
// RandomAccessFile constructor: (File/String file, String mode)
// The mode is always the second parameter
Object modeParam = parameters[1];
if (modeParam instanceof String) {
String mode = (String) modeParam;
if ("r".equals(mode)) {
return "read";
} else if ("rw".equals(mode) || "rws".equals(mode) || "rwd".equals(mode)) {
// The mode parameter takes priority over the pointcut context
// (defaultAction) because it explicitly declares the user's intent.
return "overwrite";
}
}
return null;
}

Consequence: Any mode containing write capability ("rw", "rws", "rwd") is classified as "overwrite", regardless of whether the file already exists or will be created.

Affected Test Cases:

TestExpected IntentDetected OperationReason
FileSystemCreateAccess#11createoverwritenew RandomAccessFile(file, "rw") - "rw" mode → overwrite
FileSystemDeleteAccess#11deleteoverwriteRandomAccessFile with "rw" blocks before delete can occur

6.3 Category C: Preparatory Operations

Problem: Some test methods require preparatory file system operations before the main intended operation. Ares blocks the first forbidden operation encountered.

Technical Background: When a test method calls multiple file system APIs in sequence, Ares intercepts each call independently. If the first call is forbidden, execution stops before reaching the intended operation.

Common Patterns:

  1. ensureParentDirectory: Files.createTempFile() internally calls Files.createDirectories() to ensure the parent directory exists.

  2. prepareLinkTarget: Methods testing Files.createSymbolicLink() or Files.createLink() first create the target file using Files.writeString().

  3. createTempFile + delete: Delete tests that need to create temporary files first are blocked at the create step.

Affected Test Cases:

TestExpected IntentDetected OperationBlocked APIReason
FileSystemCreateAccess#4createcreateFiles.createDirectoriesensureParentDirectory() called first
FileSystemCreateAccess#12createoverwriteFiles.writeStringprepareLinkTarget() creates target file
FileSystemCreateAccess#13createoverwriteFiles.writeStringprepareLinkTarget() creates target file
FileSystemDeleteAccess#2deletecreateFile.createTempFileTemp file must be created before delete
FileSystemDeleteAccess#4deletecreateFiles.createDirectoriesensureParentDirectory() for temp file
FileSystemDeleteAccess#12deleteoverwriteFiles.writeStringprepareLinkTarget() before link creation
FileSystemDeleteAccess#13deleteoverwriteFiles.writeStringprepareLinkTarget() before link creation
FileSystemExecuteAccess#4executecreateFile.createTempFilecreateTempOutputFile() for redirect
FileSystemExecuteAccess#5executecreateFile.createTempFilecreateTempOutputFile() for inheritIO

6.4 Category D: Wrong Subsystem

Problem: Some file system operations trigger security checks in other subsystems (e.g., Thread system) before the file system check can occur.

Technical Background: ProcessBuilder.start() and ProcessBuilder.startPipeline() internally create threads via ThreadPoolExecutor.execute(). Ares's Thread security subsystem intercepts this before the file system subsystem can check the execute operation.

Consequence: The exception message contains "Thread" instead of file system details, and the operation is classified as "create" (thread creation) rather than "execute" (file execution).

Affected Test Cases:

TestExpected IntentDetected OperationBlocked APISubsystem
FileSystemExecuteAccess#2executecreateThreadPoolExecutor.executeThread
FileSystemExecuteAccess#3executecreateThreadPoolExecutor.executeThread

Identification: These cases can be identified by messageContains: "Thread" in the expected exceptions configuration.


7. Ares 2 AOP File System Access Control: Conclusion

7.1 Technical Details

The file system security mechanism provides comprehensive protection through:

  1. Extensive API Coverage: Broad interception across file system operations
  2. Call Stack Analysis: Distinguishes trusted framework code from untrusted student code
  3. Path-Based Validation: Strict enforcement of allowed file paths
  4. Detailed Error Messages: Precise violation reporting with full call context
  5. Flexible Configuration: YAML-based security policies

The system operates transparently using AOP techniques, requiring no modifications to student code, and enforces policies before dangerous operations execute.

💡 Byte Buddy vs. AspectJ: For most use cases the validation flow is the same, but interception coverage differs slightly because AspectJ uses explicit pointcuts while instrumentation uses type-hierarchy maps.

Implementation Differences:

AspectByte Buddy (Instrumentation)AspectJ
Weaving TimeRuntime (when classes are loaded)Compile-time or load-time
ConfigurationaopMode = "INSTRUMENTATION"aopMode = "ASPECTJ"
Advice StructureSeparate class per operation typeSingle aspect with multiple before() advice
Method Info Access@Advice.Origin annotationsJoinPoint.getSignature()
Instance Access@Advice.This annotationJoinPoint.getTarget()
Parameters Access@Advice.AllArguments annotationJoinPoint.getArgs()
Validation LogicDelegates to JavaInstrumentationAdviceFileSystemToolboxImplements in JavaAspectJFileSystemAdviceDefinitions

Both implementations provide the same validation flow and permission checks; intercepted APIs differ slightly by mode.