AID-H-018.009: Model-Callable Filesystem Path Confinement
Applicability and outcome: This control applies when any model-callable path can reach a host or mounted filesystem. It is NOT_APPLICABLE only when no such path exists or a separately verified immutable-object or virtual-filesystem broker exposes no attacker-influenced path. PASS requires complete adapter and enabled-operation coverage for every claimed platform/filesystem profile plus independent negative-test evidence. Missing population or evidence is INSUFFICIENT_DATA; a required broker, policy, identity, OS primitive, or verifier failure is ERROR; and any unbrokered or incorrectly affected object is FAIL.
Pillars: app, infra
Lifecycle: building, operation
Scope boundary
Owns protocol-neutral rooted filesystem resolution and confinement for model-callable read, create, replace, rename, delete, archive-destination, working-directory, interpreter, script, module, and executable paths. It binds policy and operation to the object actually opened or affected, applies platform-specific link, mount, reparse, special-file, and race controls, and proves complete adapter coverage. It does not validate parameter schemas, decide business authorization, authorize code execution, isolate processes, admit skill packages, or govern MCP publication and output semantics.
AID-H-006.002: AID-H-006.002 sanitizes and releases model-generated text, markup, structured values, and URLs to a sink; AID-H-018.009 proves which filesystem object an applicable path value actually opens or affects. A sink-safe value can still escape through filesystem resolution, and a safely opened object does not sanitize other output sinks.
AID-H-018.001: AID-H-018.001 validates the path parameter's schema, type, format, and structural contract; AID-H-018.009 proves that filesystem resolution and use stay inside the authorized root and affect the verified object. A syntactically valid path can still escape through links, mounts, aliases, or replacement races.
AID-H-018.002: AID-H-018.002 decides whether the verified subject, tool, canonical resource, action, and context are authorized; AID-H-018.009 resolves the authorized filesystem scope and binds it to the object actually opened or affected. Business authorization does not make pathname resolution race-safe.
AID-H-018.006: AID-H-018.006 re-verifies authorization and live action context immediately before dispatch and after pause, retry, resume, or handoff; AID-H-018.009 owns descriptor- or handle-safe filesystem resolution and use. Fresh authorization and opened-object confinement are composed at use but scored separately.
AID-H-018.007: AID-H-018.007 defines and intercepts each skill's declared file-resource ceiling; AID-H-018.009 supplies the generic filesystem broker for skills and non-skill adapters. A manifest PASS does not prove that every opened or affected object stayed inside its declared root.
AID-H-025.002: AID-H-025.002 selects and configures the restricted interpreter and authorizes code evaluation; AID-H-018.009 confines any model-influenced interpreter, script, module, working-directory, or executable filesystem target. Execution admission and filesystem-target confinement are separate results and neither PASS proves the other.
AID-H-030.004: AID-H-030.004 safely parses and normalizes candidate skill packages before deeper admission; AID-H-018.009 confines model-callable runtime and reusable filesystem operations. Package parsing and runtime filesystem confinement retain separate evidence and outcomes.
AID-H-034.004: AID-H-034.004 governs which MCP tool, resource, prompt, schema, and UI contracts a server may publish; AID-H-018.009 confines a local filesystem object used by an applicable model-callable capability. MCP publication approval does not prove local path confinement.
AID-H-034.005: AID-H-034.005 owns MCP server output, resource-URI, template, prompt, completion, and elicitation semantics; AID-H-018.009 owns the reusable local-filesystem opened-object result for an applicable handler. The MCP control may consume the broker receipt but must not score the same confinement result twice.
AID-I-001: AID-I-001 contains a process, container, microVM, WASM, or other workload boundary; AID-H-018.009 confines permitted filesystem capabilities, including in-process tools that never enter a subprocess sandbox. Isolation and opened-object confinement are independent controls and neither PASS proves the other.
Warning
Medium to High on Filesystem Compatibility; Low to Medium on Runtime Overhead
Filesystem guarantees vary by operating system, filesystem, mount topology, reparse behavior, and deployment permissions. Linux openat2 beneath/no-link/no-mount resolution does not eliminate pre-existing hard links; the baseline therefore relies on a broker-owned tree and uses atomic replacement, while complete hard-link denial is a selectable higher-assurance profile. The included Windows reference is read-only and excludes UNC/SMB and unverified filesystems; unsupported mutation or process-launch classes must remain disabled. Strong confinement can reject legitimate linked or mounted paths. Never fall back to realpath, raw string-prefix checks, or sensitive-path denylists as the primary boundary.
Implementation guidance
- AID-H-018.009-G001: Broker model-callable reads through a trusted root descriptor or Windows file handle, bind policy to the opened regular-file identity, and consume bytes only through that descriptor or handle.
- AID-H-018.009-G002: Apply operation-specific Linux atomic mutation and execution-target resolution; fail closed for mutation or executable profiles whose platform invariants are not implemented and verified.
- AID-H-018.009-G003: Reconcile every model-callable filesystem adapter and independently verify rooted object confinement for each enabled operation and claimed platform/filesystem profile.
AID-H-018.009-G001
Applicability and root invariant
Inventory every model-callable read and local-resource adapter. Load a versioned, authenticated policy that binds verified caller, tool, operation, root ID and root identity, object-type and byte limits, hard-link profile, platform/filesystem profile, and expiry. The baseline root is owned and mutated only by the broker or an administrator; untrusted workloads cannot write, rename, link, mount, or replace entries. Open the trusted root once at broker bootstrap and retain its descriptor or handle. A separate, higher-assurance profile may deny every object with multiple hard links; link count is evidence and a policy choice, not a universal baseline.
Linux 5.6+ descriptor implementation
Pin an organization-approved golang.org/x/sys release and use openat2 from the retained root descriptor. The kernel resolution flags are the boundary; lexical checks reject ambiguity early but never replace the descriptor check.
//go:build linux
package pathbroker
import (
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"sync"
"golang.org/x/sys/unix"
)
type HardLinkProfile uint8
const (
ProtectedTreeAllowsLinks HardLinkProfile = iota
DenyMultipleLinks
)
type Identity struct {
Device uint64
Inode uint64
Links uint64
Size int64
Mode uint32
}
type Broker struct {
rootFD int
rootIdentity Identity
hardLinkProfile HardLinkProfile
exclusiveMutation bool
mutationMu sync.Mutex
}
func OpenBrokerRoot(
absoluteRoot string,
hardLinks HardLinkProfile,
exclusiveMutation bool,
) (*Broker, error) {
if !filepath.IsAbs(absoluteRoot) {
return nil, fmt.Errorf("broker root must be absolute")
}
fd, err := unix.Open(
absoluteRoot,
unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW,
0,
)
if err != nil {
return nil, fmt.Errorf("open broker root: %w", err)
}
var st unix.Stat_t
if err := unix.Fstat(fd, &st); err != nil {
unix.Close(fd)
return nil, fmt.Errorf("inspect broker root: %w", err)
}
if st.Mode&unix.S_IFMT != unix.S_IFDIR {
unix.Close(fd)
return nil, fmt.Errorf("broker root is not a directory")
}
if st.Mode&0o022 != 0 {
unix.Close(fd)
return nil, fmt.Errorf("broker root is group/world writable")
}
if hardLinks != ProtectedTreeAllowsLinks && hardLinks != DenyMultipleLinks {
unix.Close(fd)
return nil, fmt.Errorf("unknown hard-link profile")
}
return &Broker{
rootFD: fd,
rootIdentity: Identity{
Device: uint64(st.Dev), Inode: st.Ino,
Links: uint64(st.Nlink), Size: st.Size, Mode: st.Mode,
},
hardLinkProfile: hardLinks,
exclusiveMutation: exclusiveMutation,
}, nil
}
func (b *Broker) Close() error {
if b == nil || b.rootFD < 0 {
return nil
}
err := unix.Close(b.rootFD)
b.rootFD = -1
return err
}
func validateRelative(rel string) error {
if rel == "" || rel == "." || path.IsAbs(rel) ||
strings.ContainsRune(rel, '\x00') ||
strings.Contains(rel, "\\") ||
path.Clean(rel) != rel {
return fmt.Errorf("path is not a canonical relative Linux path")
}
for _, component := range strings.Split(rel, "/") {
if component == "" || component == "." || component == ".." {
return fmt.Errorf("unsafe path component")
}
}
return nil
}
func identity(st *unix.Stat_t) Identity {
return Identity{
Device: uint64(st.Dev), Inode: st.Ino, Links: uint64(st.Nlink),
Size: st.Size, Mode: st.Mode,
}
}
func (b *Broker) inspectRegular(fd int, maximumBytes int64) (Identity, error) {
var st unix.Stat_t
if err := unix.Fstat(fd, &st); err != nil {
return Identity{}, fmt.Errorf("inspect opened object: %w", err)
}
id := identity(&st)
if st.Mode&unix.S_IFMT != unix.S_IFREG || st.Size < 0 || st.Size > maximumBytes {
return Identity{}, fmt.Errorf("opened object violates type or size policy")
}
if b.hardLinkProfile == DenyMultipleLinks && st.Nlink != 1 {
return Identity{}, fmt.Errorf("multiple links denied by selected profile")
}
return id, nil
}
func (b *Broker) openRegular(
rel string, flags int, mode uint32, maximumBytes int64,
) (int, Identity, error) {
if b == nil || b.rootFD < 0 || maximumBytes <= 0 {
return -1, Identity{}, fmt.Errorf("open policy is invalid")
}
if err := validateRelative(rel); err != nil {
return -1, Identity{}, err
}
how := &unix.OpenHow{
Flags: uint64(flags | unix.O_CLOEXEC | unix.O_NOFOLLOW),
Mode: uint64(mode),
Resolve: uint64(
unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS |
unix.RESOLVE_NO_MAGICLINKS | unix.RESOLVE_NO_XDEV,
),
}
var fd int
var err error
for attempt := 0; attempt < 3; attempt++ {
fd, err = unix.Openat2(b.rootFD, rel, how)
if !errors.Is(err, unix.EAGAIN) {
break
}
}
if err != nil {
return -1, Identity{}, fmt.Errorf("rooted open denied: %w", err)
}
id, inspectErr := b.inspectRegular(fd, maximumBytes)
if inspectErr != nil {
unix.Close(fd)
return -1, Identity{}, inspectErr
}
return fd, id, nil
}
func (b *Broker) OpenRegularRead(
rel string, maximumBytes int64,
) (*os.File, Identity, error) {
fd, id, err := b.openRegular(rel, unix.O_RDONLY, 0, maximumBytes)
if err != nil {
return nil, Identity{}, err
}
file := os.NewFile(uintptr(fd), "brokered-read")
if file == nil {
unix.Close(fd)
return nil, Identity{}, fmt.Errorf("bind opened descriptor")
}
return file, id, nil
}
func ReadBounded(file *os.File, maximumBytes int64) ([]byte, error) {
if file == nil || maximumBytes <= 0 {
return nil, fmt.Errorf("file and positive byte limit are required")
}
value, err := io.ReadAll(io.LimitReader(file, maximumBytes+1))
if err != nil {
return nil, fmt.Errorf("read opened descriptor: %w", err)
}
if int64(len(value)) > maximumBytes {
return nil, fmt.Errorf("opened object exceeded byte policy")
}
return value, nil
}Read only from the returned *os.File; never reopen the name. RESOLVE_NO_XDEV makes each intentionally allowed mount a separate policy root. The protected-tree baseline permits hard links but prevents untrusted identities from introducing or replacing them. Select DenyMultipleLinks when the environment requires complete hard-link exclusion. If byte stability matters despite an authorized writer, bind and verify an immutable snapshot or content digest.
Windows local-volume read profile
The following C# broker is intentionally read-only. It rejects ambiguous names, opens the root and candidate with CreateFileW, obtains normalized final paths plus volume/file identity from the handles, rejects a resolved object outside the root on a path-segment boundary, and reads only through that verified handle. Pre-existing links that resolve to an object still inside the root are allowed by the protected-tree baseline; the optional link-count profile is stricter. It fails closed when normalized final paths or stable IDs are unavailable; that excludes UNC/SMB and untested filesystems from this profile.
// File: FilesystemBroker/WindowsReadBroker.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Win32.SafeHandles;
namespace Aidefend.FilesystemBroker
{
public sealed class ReadResult
{
public byte[] Bytes { get; private set; }
public string RootFinalPath { get; private set; }
public string TargetFinalPath { get; private set; }
public uint LinkCount { get; private set; }
public uint VolumeSerialNumber { get; private set; }
public ulong FileId { get; private set; }
public long Size { get; private set; }
public ReadResult(
byte[] bytes,
string rootFinalPath,
string targetFinalPath,
uint linkCount,
uint volumeSerialNumber,
ulong fileId,
long size)
{
Bytes = bytes;
RootFinalPath = rootFinalPath;
TargetFinalPath = targetFinalPath;
LinkCount = linkCount;
VolumeSerialNumber = volumeSerialNumber;
FileId = fileId;
Size = size;
}
}
public static class WindowsReadBroker
{
private const uint GenericRead = 0x80000000;
private const uint FileListDirectory = 0x0001;
private const uint FileShareRead = 0x00000001;
private const uint FileShareWrite = 0x00000002;
private const uint FileShareDelete = 0x00000004;
private const uint OpenExisting = 3;
private const uint FileFlagBackupSemantics = 0x02000000;
private const uint FileFlagSequentialScan = 0x08000000;
private const uint FileAttributeDirectory = 0x00000010;
private const uint FileNameNormalized = 0;
private const uint VolumeNameDos = 0;
[StructLayout(LayoutKind.Sequential)]
private struct FileTime
{
public uint LowDateTime;
public uint HighDateTime;
}
[StructLayout(LayoutKind.Sequential)]
private struct ByHandleFileInformation
{
public uint FileAttributes;
public FileTime CreationTime;
public FileTime LastAccessTime;
public FileTime LastWriteTime;
public uint VolumeSerialNumber;
public uint FileSizeHigh;
public uint FileSizeLow;
public uint NumberOfLinks;
public uint FileIndexHigh;
public uint FileIndexLow;
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern SafeFileHandle CreateFileW(
string fileName,
uint desiredAccess,
uint shareMode,
IntPtr securityAttributes,
uint creationDisposition,
uint flagsAndAttributes,
IntPtr templateFile);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern uint GetFinalPathNameByHandleW(
SafeFileHandle file,
StringBuilder path,
uint pathLength,
uint flags);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetFileInformationByHandle(
SafeFileHandle file,
out ByHandleFileInformation information);
private static Win32Exception LastError(string operation)
{
return new Win32Exception(Marshal.GetLastWin32Error(), operation);
}
private static string FinalPath(SafeFileHandle handle)
{
uint capacity = 512;
while (capacity <= 32768)
{
StringBuilder buffer = new StringBuilder((int)capacity);
uint length = GetFinalPathNameByHandleW(
handle, buffer, capacity, FileNameNormalized | VolumeNameDos);
if (length == 0)
throw LastError("GetFinalPathNameByHandleW");
if (length < capacity)
return NormalizeFinalPath(buffer.ToString());
capacity = checked(length + 1);
}
throw new PathTooLongException("resolved handle path exceeds the broker limit");
}
private static string NormalizeFinalPath(string value)
{
if (value.StartsWith(@"\\?\UNC\", StringComparison.OrdinalIgnoreCase))
return @"\\" + value.Substring(8);
if (value.StartsWith(@"\\?\", StringComparison.OrdinalIgnoreCase))
return value.Substring(4);
return value;
}
private static bool IsWithin(string root, string candidate)
{
string cleanRoot = root.TrimEnd('\\');
return candidate.Equals(cleanRoot, StringComparison.OrdinalIgnoreCase) ||
candidate.StartsWith(
cleanRoot + "\\", StringComparison.OrdinalIgnoreCase);
}
private static string ValidateRelative(string value)
{
if (String.IsNullOrWhiteSpace(value) || Path.IsPathRooted(value) ||
value.IndexOf('\0') >= 0 || value.IndexOf(':') >= 0)
throw new ArgumentException("invalid relative Windows path");
string[] parts = value.Replace('/', '\\').Split('\\');
foreach (string part in parts)
{
if (part.Length == 0 || part == "." || part == ".." ||
part.EndsWith(" ", StringComparison.Ordinal) ||
part.EndsWith(".", StringComparison.Ordinal))
throw new ArgumentException("ambiguous Windows path component");
string stem = Path.GetFileNameWithoutExtension(part).ToUpperInvariant();
if (Regex.IsMatch(stem, @"^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$"))
throw new ArgumentException("reserved Windows path component");
}
return String.Join("\\", parts);
}
private static SafeFileHandle Open(
string path, uint access, uint share, uint flags)
{
SafeFileHandle handle = CreateFileW(
path, access, share, IntPtr.Zero, OpenExisting, flags, IntPtr.Zero);
if (handle.IsInvalid)
{
Win32Exception error = LastError("CreateFileW");
handle.Dispose();
throw error;
}
return handle;
}
public static ReadResult ReadBounded(
string brokerOwnedRoot,
string relativePath,
int maximumBytes,
bool denyMultipleLinks)
{
if (maximumBytes <= 0)
throw new ArgumentOutOfRangeException("maximumBytes");
string relative = ValidateRelative(relativePath);
using (SafeFileHandle rootHandle = Open(
brokerOwnedRoot,
FileListDirectory,
FileShareRead | FileShareWrite | FileShareDelete,
FileFlagBackupSemantics))
{
string rootFinal = FinalPath(rootHandle);
if (rootFinal.StartsWith(@"\\", StringComparison.Ordinal))
throw new PlatformNotSupportedException(
"UNC and remote filesystems are outside this profile");
string volumeRoot = Path.GetPathRoot(rootFinal);
if (String.IsNullOrEmpty(volumeRoot) ||
!String.Equals(
new DriveInfo(volumeRoot).DriveFormat,
"NTFS",
StringComparison.OrdinalIgnoreCase))
throw new PlatformNotSupportedException(
"this profile supports local NTFS only");
string candidate = Path.Combine(brokerOwnedRoot, relative);
using (SafeFileHandle targetHandle = Open(
candidate,
GenericRead,
FileShareRead,
FileFlagSequentialScan))
{
string targetFinal = FinalPath(targetHandle);
if (!IsWithin(rootFinal, targetFinal))
throw new UnauthorizedAccessException(
"opened object escaped the broker root");
ByHandleFileInformation information;
if (!GetFileInformationByHandle(targetHandle, out information))
throw LastError("GetFileInformationByHandle");
if ((information.FileAttributes & FileAttributeDirectory) != 0)
throw new UnauthorizedAccessException(
"directories are outside the read-file profile");
if (denyMultipleLinks && information.NumberOfLinks != 1)
throw new UnauthorizedAccessException(
"multiple hard links denied by the selected profile");
using (FileStream stream = new FileStream(
targetHandle, FileAccess.Read, 4096, false))
{
if (stream.Length > maximumBytes)
throw new IOException(
"opened file exceeds the policy byte limit");
byte[] result = new byte[stream.Length];
int offset = 0;
while (offset < result.Length)
{
int count = stream.Read(
result, offset, result.Length - offset);
if (count == 0)
throw new EndOfStreamException(
"opened file changed during bounded read");
offset += count;
}
ulong fileId =
((ulong)information.FileIndexHigh << 32) |
information.FileIndexLow;
return new ReadResult(
result,
rootFinal,
targetFinal,
information.NumberOfLinks,
information.VolumeSerialNumber,
fileId,
stream.Length);
}
}
}
}
}
}Deploy the Windows broker under a root ACL that the model/tool identity cannot mutate. Record root and candidate volume/file ID, final path digest, link count, size, policy version, operation, caller, and bytes consumed. The code records but does not universally reject multiple links. Windows mutation and pathname-based process launch are not covered by this read-only reference profile.
Safe failure
ELOOP and EXDEV are policy denials. Exhausted EAGAIN, ENOSYS, unsupported flags, root-identity or ACL drift, unavailable Windows normalized-name/file-ID queries, policy or identity failure, and ambiguous filesystem behavior are ERROR and block use. Sensitive-path denylists and reduced ambient secrets are defense in depth, not the rooted-object boundary.
AID-H-018.009-G002
Separate authority by operation
Policy separately grants read, create, replace, rename, delete, load, and execution-target resolution. This reference profile deliberately disables working-directory changes because its regular-file broker does not open directories. The model/tool identity receives no direct mutation permission. On Linux, set exclusiveMutation only after deployment evidence proves that the broker owns and serializes the tree and untrusted identities cannot rename, link, mount, or replace entries. The following code publishes create/replace data by same-directory temporary object plus atomic rename, uses no-follow identity readback for delete and rename, and resolves an execution target to a descriptor and expected digest without executing it.
//go:build linux
package pathbroker
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"path"
"strings"
"golang.org/x/sys/unix"
)
func sameObject(left, right *unix.Stat_t) bool {
return left.Dev == right.Dev && left.Ino == right.Ino
}
const brokerMaximumWriteBytes int64 = 1 << 30
type MutationReceipt struct {
OperationID string
Operation string
State string
Identity Identity
ByteCount int64
SHA256 string
}
type IndeterminateAfterEffectError struct {
OperationID string
Operation string
RelativePath string
Expected Identity
ByteCount int64
SHA256 string
Cause error
}
func (e *IndeterminateAfterEffectError) Error() string {
return fmt.Sprintf(
"%s %s is indeterminate after effect: %v",
e.Operation, e.OperationID, e.Cause,
)
}
func (e *IndeterminateAfterEffectError) Unwrap() error { return e.Cause }
func requireOperationID(value string) error {
if strings.TrimSpace(value) == "" || len(value) > 128 {
return fmt.Errorf("operation ID is invalid")
}
return nil
}
func (b *Broker) requireExclusiveMutation() error {
if b == nil || b.rootFD < 0 || !b.exclusiveMutation {
return fmt.Errorf("mutation profile is disabled")
}
return nil
}
func (b *Broker) openParent(rel string) (int, string, error) {
if err := validateRelative(rel); err != nil {
return -1, "", err
}
base := path.Base(rel)
parent := path.Dir(rel)
if parent == "." {
fd, err := unix.Dup(b.rootFD)
return fd, base, err
}
how := &unix.OpenHow{
Flags: uint64(unix.O_RDONLY | unix.O_DIRECTORY | unix.O_CLOEXEC),
Resolve: uint64(
unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS |
unix.RESOLVE_NO_MAGICLINKS | unix.RESOLVE_NO_XDEV,
),
}
fd, err := unix.Openat2(b.rootFD, parent, how)
if err != nil {
return -1, "", fmt.Errorf("open protected parent: %w", err)
}
return fd, base, nil
}
func temporaryName() (string, error) {
value := make([]byte, 16)
if _, err := rand.Read(value); err != nil {
return "", fmt.Errorf("temporary-name entropy: %w", err)
}
return ".aidefend-" + hex.EncodeToString(value) + ".tmp", nil
}
func writeTemporary(
parentFD int, data []byte, mode uint32, maximumBytes int64,
) (string, unix.Stat_t, error) {
if mode == 0 || mode&^uint32(0o660) != 0 ||
maximumBytes <= 0 || maximumBytes > brokerMaximumWriteBytes ||
int64(len(data)) > maximumBytes {
return "", unix.Stat_t{}, fmt.Errorf("create mode or policy byte bound is invalid")
}
name, err := temporaryName()
if err != nil {
return "", unix.Stat_t{}, err
}
fd, err := unix.Openat(
parentFD, name,
unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_CLOEXEC|unix.O_NOFOLLOW,
mode,
)
if err != nil {
return "", unix.Stat_t{}, fmt.Errorf("create temporary object: %w", err)
}
cleanup := true
defer func() {
unix.Close(fd)
if cleanup {
unix.Unlinkat(parentFD, name, 0)
}
}()
remaining := data
for len(remaining) > 0 {
count, writeErr := unix.Write(fd, remaining)
if writeErr != nil {
return "", unix.Stat_t{}, fmt.Errorf("write temporary object: %w", writeErr)
}
if count <= 0 {
return "", unix.Stat_t{}, fmt.Errorf("short write without error")
}
remaining = remaining[count:]
}
if err := unix.Fsync(fd); err != nil {
return "", unix.Stat_t{}, fmt.Errorf("sync temporary object: %w", err)
}
var st unix.Stat_t
if err := unix.Fstat(fd, &st); err != nil {
return "", unix.Stat_t{}, fmt.Errorf("inspect temporary object: %w", err)
}
if st.Mode&unix.S_IFMT != unix.S_IFREG ||
st.Nlink != 1 ||
st.Size != int64(len(data)) {
return "", unix.Stat_t{}, fmt.Errorf("temporary object identity or byte count is invalid")
}
cleanup = false
return name, st, nil
}
func (b *Broker) AtomicWrite(
rel string, data []byte, mode uint32, replace bool,
maximumBytes int64, operationID string,
) (MutationReceipt, error) {
if err := requireOperationID(operationID); err != nil {
return MutationReceipt{}, err
}
if err := b.requireExclusiveMutation(); err != nil {
return MutationReceipt{}, err
}
if maximumBytes <= 0 || maximumBytes > brokerMaximumWriteBytes ||
int64(len(data)) > maximumBytes {
return MutationReceipt{}, fmt.Errorf("write exceeds the policy byte bound")
}
digest := sha256.Sum256(data)
digestHex := hex.EncodeToString(digest[:])
b.mutationMu.Lock()
defer b.mutationMu.Unlock()
parentFD, base, err := b.openParent(rel)
if err != nil {
return MutationReceipt{}, err
}
defer unix.Close(parentFD)
var before unix.Stat_t
beforeErr := unix.Fstatat(parentFD, base, &before, unix.AT_SYMLINK_NOFOLLOW)
if replace {
if beforeErr != nil || before.Mode&unix.S_IFMT != unix.S_IFREG {
return MutationReceipt{}, fmt.Errorf("replace target is not a regular file")
}
} else if beforeErr == nil || !errors.Is(beforeErr, unix.ENOENT) {
return MutationReceipt{}, fmt.Errorf("create target already exists or is ambiguous")
}
temporary, written, err := writeTemporary(parentFD, data, mode, maximumBytes)
if err != nil {
return MutationReceipt{}, err
}
published := false
defer func() {
if !published {
unix.Unlinkat(parentFD, temporary, 0)
}
}()
flags := uint(0)
if !replace {
flags = unix.RENAME_NOREPLACE
}
if err := unix.Renameat2(parentFD, temporary, parentFD, base, flags); err != nil {
return MutationReceipt{}, fmt.Errorf("publish object atomically: %w", err)
}
published = true
pending := &IndeterminateAfterEffectError{
OperationID: operationID, Operation: "atomic_write",
RelativePath: rel, Expected: identity(&written),
ByteCount: int64(len(data)), SHA256: digestHex,
}
if err := unix.Fsync(parentFD); err != nil {
pending.Cause = fmt.Errorf("sync parent directory: %w", err)
return MutationReceipt{}, pending
}
var after unix.Stat_t
if err := unix.Fstatat(parentFD, base, &after, unix.AT_SYMLINK_NOFOLLOW); err != nil {
pending.Cause = fmt.Errorf("read back published object: %w", err)
return MutationReceipt{}, pending
}
if !sameObject(&written, &after) ||
after.Mode&unix.S_IFMT != unix.S_IFREG ||
after.Size != int64(len(data)) {
pending.Cause = fmt.Errorf("published object identity or byte count differs")
return MutationReceipt{}, pending
}
return MutationReceipt{
OperationID: operationID, Operation: "atomic_write", State: "succeeded",
Identity: identity(&after), ByteCount: int64(len(data)), SHA256: digestHex,
}, nil
}
func (b *Broker) ReconcileAtomicWrite(
pending *IndeterminateAfterEffectError, maximumBytes int64,
) (MutationReceipt, error) {
if pending == nil || pending.Operation != "atomic_write" ||
pending.ByteCount < 0 || pending.ByteCount > maximumBytes ||
maximumBytes <= 0 || maximumBytes > brokerMaximumWriteBytes {
return MutationReceipt{}, fmt.Errorf("reconciliation scope is invalid")
}
file, id, err := b.OpenRegularRead(pending.RelativePath, maximumBytes)
if err != nil {
return MutationReceipt{}, fmt.Errorf("reconciliation readback: %w", err)
}
defer file.Close()
if id != pending.Expected || id.Size != pending.ByteCount {
return MutationReceipt{}, fmt.Errorf("reconciliation identity or byte count differs")
}
hasher := sha256.New()
copied, err := io.Copy(hasher, io.LimitReader(file, maximumBytes+1))
if err != nil || copied != pending.ByteCount {
return MutationReceipt{}, fmt.Errorf("reconciliation byte readback differs")
}
if hex.EncodeToString(hasher.Sum(nil)) != pending.SHA256 {
return MutationReceipt{}, fmt.Errorf("reconciliation digest differs")
}
return MutationReceipt{
OperationID: pending.OperationID, Operation: pending.Operation,
State: "succeeded_after_reconciliation", Identity: id,
ByteCount: copied, SHA256: pending.SHA256,
}, nil
}
func (b *Broker) DeleteRegular(rel, operationID string) (Identity, error) {
if err := requireOperationID(operationID); err != nil {
return Identity{}, err
}
if err := b.requireExclusiveMutation(); err != nil {
return Identity{}, err
}
b.mutationMu.Lock()
defer b.mutationMu.Unlock()
parentFD, base, err := b.openParent(rel)
if err != nil {
return Identity{}, err
}
defer unix.Close(parentFD)
var before unix.Stat_t
if err := unix.Fstatat(parentFD, base, &before, unix.AT_SYMLINK_NOFOLLOW); err != nil {
return Identity{}, fmt.Errorf("inspect delete target: %w", err)
}
if before.Mode&unix.S_IFMT != unix.S_IFREG {
return Identity{}, fmt.Errorf("delete target is not a regular file")
}
if b.hardLinkProfile == DenyMultipleLinks && before.Nlink != 1 {
return Identity{}, fmt.Errorf("multiple links denied by selected profile")
}
if err := unix.Unlinkat(parentFD, base, 0); err != nil {
return Identity{}, fmt.Errorf("delete directory entry: %w", err)
}
if err := unix.Fsync(parentFD); err != nil {
return Identity{}, &IndeterminateAfterEffectError{
OperationID: operationID, Operation: "delete",
RelativePath: rel, Expected: identity(&before),
Cause: fmt.Errorf("sync deleted directory entry: %w", err),
}
}
var after unix.Stat_t
if err := unix.Fstatat(parentFD, base, &after, unix.AT_SYMLINK_NOFOLLOW); !errors.Is(err, unix.ENOENT) {
return Identity{}, &IndeterminateAfterEffectError{
OperationID: operationID, Operation: "delete",
RelativePath: rel, Expected: identity(&before),
Cause: fmt.Errorf("delete readback did not prove absence"),
}
}
return identity(&before), nil
}
func (b *Broker) RenameRegularNoReplace(
from, to, operationID string,
) (Identity, error) {
if err := requireOperationID(operationID); err != nil {
return Identity{}, err
}
if err := b.requireExclusiveMutation(); err != nil {
return Identity{}, err
}
b.mutationMu.Lock()
defer b.mutationMu.Unlock()
sourceParent, sourceBase, err := b.openParent(from)
if err != nil {
return Identity{}, err
}
defer unix.Close(sourceParent)
targetParent, targetBase, err := b.openParent(to)
if err != nil {
return Identity{}, err
}
defer unix.Close(targetParent)
var source unix.Stat_t
if err := unix.Fstatat(
sourceParent, sourceBase, &source, unix.AT_SYMLINK_NOFOLLOW,
); err != nil || source.Mode&unix.S_IFMT != unix.S_IFREG {
return Identity{}, fmt.Errorf("rename source is not a regular file")
}
if b.hardLinkProfile == DenyMultipleLinks && source.Nlink != 1 {
return Identity{}, fmt.Errorf("multiple links denied by selected profile")
}
if err := unix.Renameat2(
sourceParent, sourceBase, targetParent, targetBase,
unix.RENAME_NOREPLACE,
); err != nil {
return Identity{}, fmt.Errorf("rename inside protected root: %w", err)
}
late := func(cause error) error {
return &IndeterminateAfterEffectError{
OperationID: operationID, Operation: "rename",
RelativePath: to, Expected: identity(&source),
ByteCount: source.Size, Cause: cause,
}
}
if err := unix.Fsync(sourceParent); err != nil {
return Identity{}, late(fmt.Errorf("sync source parent: %w", err))
}
if targetParent != sourceParent {
if err := unix.Fsync(targetParent); err != nil {
return Identity{}, late(fmt.Errorf("sync target parent: %w", err))
}
}
var target unix.Stat_t
if err := unix.Fstatat(
targetParent, targetBase, &target, unix.AT_SYMLINK_NOFOLLOW,
); err != nil || !sameObject(&source, &target) {
return Identity{}, late(fmt.Errorf("rename identity readback differs"))
}
return identity(&target), nil
}
func (b *Broker) ResolveExecutionTarget(
rel string, maximumBytes int64, expectedSHA256 string,
) (*os.File, Identity, error) {
expected, err := hex.DecodeString(expectedSHA256)
if err != nil || len(expected) != sha256.Size {
return nil, Identity{}, fmt.Errorf("expected executable digest is invalid")
}
file, id, err := b.OpenRegularRead(rel, maximumBytes)
if err != nil {
return nil, Identity{}, err
}
closeFile := true
defer func() {
if closeFile {
file.Close()
}
}()
if id.Mode&0o022 != 0 {
return nil, Identity{}, fmt.Errorf("execution target is group/world writable")
}
hasher := sha256.New()
copied, err := io.Copy(hasher, io.LimitReader(file, maximumBytes+1))
if err != nil || copied > maximumBytes {
return nil, Identity{}, fmt.Errorf("hash execution target")
}
if !equalBytes(hasher.Sum(nil), expected) {
return nil, Identity{}, fmt.Errorf("execution target digest differs")
}
if _, err := file.Seek(0, io.SeekStart); err != nil {
return nil, Identity{}, fmt.Errorf("rewind execution target: %w", err)
}
closeFile = false
return file, id, nil
}
func equalBytes(left, right []byte) bool {
if len(left) != len(right) {
return false
}
var difference byte
for index := range left {
difference |= left[index] ^ right[index]
}
return difference == 0
}AtomicWrite(..., replace=true) rejects non-positive, over-limit, or evaluator-ceiling-exceeding payloads before it creates a temporary object, and replaces the directory entry with a new broker-created inode instead of modifying an existing hard-linked inode in place. This prevents an outside hard link from receiving the write. Delete and rename affect only the verified entry inside the protected tree. If the exclusive-mutation invariant cannot be proved, every mutation method stays disabled. After a rename or unlink succeeds, a durability or readback error is IndeterminateAfterEffectError, bound to the operation ID and expected object; callers must reconcile through broker readback and must not blindly retry or compensate. Only errors returned before the publication/unlink/rename syscall prove no target effect.
Execution boundary
ResolveExecutionTarget proves only the filesystem identity, mutability posture, and digest of a native binary, interpreter, script, or module. It does not open or authorize a working directory in this reference profile, and it does not decide whether code may run. AID-H-025.002 owns interpreter and code-execution admission, and AID-I-001 owns runtime isolation. The execution adapter must consume the verified descriptor where its platform supports descriptor-based dispatch; scripts require separately approved interpreter and script identities. Never search an attacker-controlled PATH, working directory, shell expansion, or environment variable. A deployment that needs chdir must add a separately reviewed rooted directory-open plus descriptor-based fchdir profile and evidence before enabling it.
Honest Windows mutation profile
This framework source supplies a compile-tested Windows read profile, not a general Windows mutation or process-launch implementation. Signed policy must disable Windows create, replace, rename, delete, working-directory, module-load, and executable-dispatch classes unless the deployment provides a separately reviewed native broker that walks existing parents without following reparse points, uses handle-based mutation, enforces a broker-exclusive ACL, reads back file ID and final path, and passes the G003 population. An attempted unsupported operation returns ERROR before effect; disabling an unused class is allowed, but unsupported enforcement never becomes PASS or NOT_APPLICABLE.
Mutation regression fixtures
Exercise zero, negative, evaluator-ceiling-exceeding, and payload-over-policy byte bounds; require rejection before temporary-object creation. Inject parent-sync and post-effect readback failures for create/replace, rename, and delete; require the typed indeterminate state, stable operation ID, and broker reconciliation before retry. Under DenyMultipleLinks, require a multi-link rename source to fail before effect. Receipts bind exact byte count and SHA-256 for successful or reconciled writes; old replaced-target inode lifecycle remains an external retention/backup concern, not a claim that the broker erased every prior hard link.
AID-H-018.009-G003
Build the complete population
Generate an adapter manifest from the production-equivalent build. Include built-in Read/Write tools, skill and plugin interceptors, MCP local-resource handlers, generated-code runners, archive and loader destinations, ordinary APIs, working-directory changes, module/library loaders, and every interpreter or executable selector. For each adapter record release/image digest, platform/filesystem profile, operation classes, canonical broker entry point, direct filesystem permissions, and test-case set. Missing adapters, operations, or applicable profiles are INSUFFICIENT_DATA; an identified bypass is FAIL.
Test policy and object reality
For every applicable adapter, operation, and claimed profile, run benign cases and adversarial cases for .., sibling prefixes, absolute/device paths, alternate separators, percent/double encoding, Unicode and case variants, symlinks, junctions, reparse points, hard links, mount/bind mount, magic links, rename/replacement races, alternate streams, /proc/self/environ and credential files, sockets/pipes/devices, oversized or changing files, read/write/execute confusion, attacker-controlled PATH/working directory/interpreter, and direct in-process bypass. Inject policy, identity, broker, syscall/API, and verifier failure. A negative case passes only when denial or ERROR occurs before an unauthorized effect; later sandbox containment does not turn a wrong open into a path-confinement PASS.
Verify independently
Do not accept a handler-returned Boolean. Verify the broker receipt against OS file identity and an independently observed sentinel or directory readback. Bind the case manifest, expected operation/object, adapter/build digest, policy/root digest, platform/filesystem profile, broker receipt digest, actual effect, and policy freshness. Keep test-run state outside the broker identity and reconcile expected operations that emitted no receipt.
Evaluate the declared matrix
Require every applicable adapter, enabled operation, claimed platform/filesystem profile, and policy-required case to produce a current broker receipt and an independent observation of the exact object and effect. Any bypass, escape, wrong object, forbidden special object, unverified execution target, missing population member, stale evidence, or indeterminate required dependency prevents a successful control result. Emit one outcome under AID-H-018.009; this reconciliation method is evidence for that control and is not independently scoreable.
Re-run after adapter, OS/kernel, filesystem, runtime, policy, root-permission, or broker changes. Evidence from one platform/filesystem profile cannot prove another.
Open-source tools
- golang.org/x/sys/unix (BSD-3-Clause low-level Linux syscall bindings)
- Go os.Root (Go 1.24+; BSD-3-Clause; traversal-resistant rooted filesystem operations; object-identity receipts, hard-link and mount profiles, no-replace behavior, and durability semantics require additional broker logic)
- libpathrs (MPL-2.0 OR LGPL-3.0-or-later; Linux handle-based rooted path resolution with Rust, C, and Go APIs)
- cap-std (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT; capability-oriented rooted filesystem operations for Rust; control-specific receipts and mutation semantics require additional integration)
Threat mappings
MITRE ATLAS
- AML.T0037 Data from Local System
- AML.T0050 Command and Scripting Interpreter (approved target identity blocks path substitution; interpreter admission and sandboxing remain separate)
- AML.T0053 AI Agent Tool Invocation
- AML.T0086 Exfiltration via AI Agent Tool Invocation (rooted resolution prevents reads of out-of-scope local objects but does not police the outbound channel)
- AML.T0098 AI Agent Tool Credential Harvesting (credential files outside authorized roots remain unreachable; ambient-credential minimization remains separate)
- AML.T0101 Data Destruction via AI Agent Tool Invocation (operation-specific rooted mutation prevents destruction outside authorized roots)
- AML.T0089 Enterprise Environment Discovery (brokered path resolution denies model-directed reads of host configuration and process metadata paths)
- AML.T0075 Enterprise Resource Discovery (brokered path resolution denies model-directed directory enumeration outside approved roots)
- AML.T0126 Automated Collection (brokered reads confine model-directed bulk file collection to approved roots)
- AML.T0127 Data Staged (archive and working-directory writes are confined to approved roots, denying staging locations)
MAESTRO
- Agent Tool Misuse (L7)
- Integration Risks (L7)
- Privilege Escalation (Cross-Layer)
- Data Leakage (Cross-Layer)
OWASP LLM Top 10 2026
- LLM02:2026 Sensitive Information Disclosure
- LLM03:2026 Excessive Agency
- LLM10:2026 Improper Output Handling
OWASP ML Top 10 2023
- N/A
OWASP Top 10 for Agentic Applications 2026
- ASI02:2026 Tool Misuse and Exploitation
- ASI03:2026 Identity and Privilege Abuse
- ASI05:2026 Unexpected Code Execution (RCE)
NIST Adversarial Machine Learning 2025
- NISTAML.039 Compromising connected resources
Cisco Integrated AI Security and Safety Framework
- AISubtech-12.1.1 Parameter Manipulation
- AISubtech-12.1.3 Unsafe System / Browser / File Execution
- AISubtech-14.1.1 Credential Theft (confines access to host files and credential-bearing filesystem objects)
- AISubtech-14.1.2 Insufficient Access Controls (confines access to host files and credential-bearing filesystem objects)
- AISubtech-8.2.3 Data Exfiltration via Agent Tooling
- AISubtech-8.3.2 System Information Exposure
- AISubtech-9.1.2 Unauthorized or Unsolicited System Access
- AITech-12.1 Tool Exploitation
- AITech-12.2 Insecure Output Handling
- AITech-14.1 Unauthorized Access (confines access to host files and credential-bearing filesystem objects)
- AITech-8.2 Data Exfiltration / Exposure
- AITech-8.3 Information Disclosure
- AITech-9.1 Model or Agentic System Manipulation
- AISubtech-11.1.2 Tool-Scoped Evasion (opened-object confinement blocks tool-specific filesystem payload effects)
Google Secure AI Framework 2.0 - Risks
- IIC: Insecure Integrated Component
- SDD: Sensitive Data Disclosure
- RA: Rogue Actions
Databricks AI Security Framework 3.0
- Agents - Core 13.2: Tool Misuse
- Agents - Core 13.3: Privilege Compromise
- Agents - Core 13.11: Unexpected RCE and Code Attacks