Sabline 8.8.0

What the operating system enforces

This is the what the operating system enforces section of the threat model, on a page of its own so that neither page outgrows the size a page of the site may be, as known-open.md is. The threat model says what Sabline defends against; this page says, item by item, how much of that the kernel holds too, on each of the three systems and in each of the two runtimes.

From 8.4 the process that runs a program asks the operating system to hold the budget it was given, before the program's first statement runs, so that a fault in Sabline itself is a crash inside a box and not an escape. The derivation is one function, os_policy in sabline/confine.py: budget in, OS policy out, reading nothing of the machine. This table is that module's ENFORCES, and check_confine.py fails when the two differ.

Budget itemLinux (Landlock, seccomp-bpf)macOS (a sandbox profile)Windows (job object, token, integrity level)
no fs grantedLandlock: no file read, written, made or removed but what the interpreter itself needs (below)writes refused everywhere but the private temporary directory; reads refused under the home directory and /Volumes, but for the names in the working directory and in each directory above it, which getcwd readslow integrity level: no write to anything of the user's; reads not held
fs:read:DIRLandlock: reads beneath DIR (resolved), and nothing else of the user's; when DIR does not exist yet - a file the program writes and reads back - beneath the nearest directory that does, and the level is partialreads beneath DIR allowed; outside it, refused only under the home directory and /Volumesnot held: the low integrity level does not stop a read
fs:write:DIRLandlock: files written, made and truncated beneath DIR; when DIR does not exist yet, beneath the nearest directory that does, and the level is partialwrites beneath DIR, as Linuxnot held: a write grant keeps the process at medium integrity, because lowering it would need the granted directory relabelled
fs, fs:read, fs:write with no paththat direction is not restricted, as the budget saysas Linuxas Linux
a credential location under a broad grant (E318)not held: Landlock cannot take a path out of a hierarchy it allowsnot heldnot held
no net grantedseccomp: socket, socketpair, connect, bind, listen and accept refused (EPERM); Landlock ABI 4 and later also refuses TCP(deny network*)not held: it needs an AppContainer, which this Python cannot start in (below)
net:HOST, net:HOST:PORTIPv4, IPv6 and netlink sockets allowed, and no Unix socket; with Landlock ABI 4 and later, and a port on every grant, TCP connections to those ports and 53 only. The host is held by the language alone, so the level is partialthe network allowed; the host is held by the language alone (partial)not held
net with no hostany host, as the budget says; still no Unix socket, unless a granted Python module widened the policy to any hostnot restricted, as the budget saysas macOS
@N countsthe languagethe languagethe language
iothe descriptors the process was started with; not restrictedas Linuxas Linux
envnot held: the environment is in the process's own memorynot heldnot held
clock, rand, declassifynot held: reading the clock or the kernel's randomness reaches nothing outside the process, and declassify is a rule of the type systemnot heldnot held
toolnot held: a tool call is a line written to the standard output the process was started with and an answer read from its standard input; what the tool then does happens in the host's process, which this policy does not reachnot heldnot held
starting a process (never a budget item)seccomp: execve, execveat, fork, vfork, clone without CLONE_THREAD refused, clone3 answered ENOSYS; Landlock refuses execute(deny process-fork) (deny process-exec)job object: one active process; and the clipboard, the desktop, global atoms and other processes' USER handles
the rest of the deny-list (never a budget item)seccomp: ptrace, mount and its new calls, pivot_root, chroot, unshare, setns, kernel modules, kexec, bpf, perf_event_open, process_vm_readv and writev, keyrings, io_uring, userfaultfd, open_by_handle_at, setting the clock, reboot, swapon, acct, quotactl, personality; a signal, by kill, tgkill or sigqueue, to any process but this one; input pushed at the terminal (TIOCSTI, TIOCLINUX)what (deny process-fork) and the denial of writes imply; no list of system callsevery privilege but SeChangeNotifyPrivilege removed from the token
ffi:MODULEwidened to what FFI_WIDENS names for MODULE: nothing, any path, any host, or nothing enforced; a module not in the table, ffi:os, ffi:subprocess and plain ffi widen to nothing enforced, and the level is noneas Linuxas Linux
ffi:MODULE under --confine strictwidened to nothing: the policy is the budget's fs: and net: grants and what the interpreter itself reads, whatever modules are granted, and the rows above hold a module's own calls - native code, ctypes, os.system - as they hold the runtime's; a module that needs a path or a host needs it in the budgetas Linux, with the profile's limits: writes, the network and processes held, reads only under the home directory and /Volumesas Linux, with this table's limits: no second process, and no write to anything of the user's when the budget grants no write
--require-confinement full or partial (never a budget item)a run whose level, once applied, is below the one asked for is refused before its first statement (E326), and its receipt records the level it gotas Linux; every run is partial here at best - the level says that Apple has deprecated sandbox profiles - so full refuses every runas Linux; a budget that restricts reads or the network - io itself does - gives partial here, so full refuses it
time and memory limitsRLIMIT_AS, and the parent's clock (as before 8.4)the parent's clock; RLIMIT_AS is best-effortthe job object's memory limit, and the parent's clock

Through sabline-rt, which enters a container Windows' Python cannot #

The table above is the Python package's own runtime. From 9.0 a run may go through sabline-rt (SABLINE_REFERENCE_RUNTIME=rust), whose own process is the run's - so it can be started inside a Windows AppContainer, where a running python.exe cannot enter one and a fresh one started in one cannot read its own installation. Everything else is the same table: sabline-rt os-policy --enforces writes how much of each row above that runtime's kernel holds, and check_confine.py holds it to be at least the package's everywhere, with a reason on every cell that differs. These are all of them.

Budget itemSystemThe packageThrough sabline-rtWhy it differs
no fs grantedwindowspartlyyeswindows: the run's own process is started inside an AppContainer with an ACE for nothing but the files it reads, so a read or a write of anything of the user's is refused by the kernel and not only a write
fs:read:DIRwindowsnoyeswindows: the container is given one ACE for DIR, resolved, and one for each file the run itself reads; a read of anything else of the user's is refused
fs:write:DIRwindowsnoyeswindows: the container is given an ACE for DIR that carries write and delete, and the integrity level is not lowered, because the ACE is what holds the write
no net grantedwindowsnoyeswindows: the container is made with no capability, so a socket to anything outside the machine is refused with WSAEACCES
net:HOST, net:HOST:PORTwindowsnopartlywindows: the container is made with internetClient, which admits every host, so the host is held by the language alone, as on Linux and macOS
ffi:MODULE under --confine strictwindowspartlyyeswindows: under strict the policy is the budget's own grants, and the container holds a module's own calls - native code, ctypes - as it holds the runtime's

A container is left off for a run whose budget it would refuse something the budget allows, and the receipt's confinement_reason says which: a budget granting a loopback host, since Windows isolates a container from the loopback address unless an administrator exempts it; a budget that restricts neither reads nor writes, since a container reaches only what it is given an ACE for; and a system where a process inside a container cannot resolve a path as the process that started it does, which is asked before a run is handed to one. A Windows Server is the last of those: a process inside a container there can open the file it was granted and still not be told what path that file has, so every fs: grant - which is checked against what a path resolves to - would refuse a file the budget allows. The three cells that say yes for Windows below are therefore yes on a Windows 11, and on a Server the rows are the package's own. Such a run gets the table above, as it did before 9.0. A budget that grants any host keeps its container: it does not say it needs the loopback address, which is then the one address the run itself cannot reach - its own network calls are made by the process that started it, which can.

What the interpreter itself must read is allowed beside the grants, on Linux by name: Python's own installation and every directory it imports from, the sabline package and the standard library beside it, the system's shared libraries, /dev/null, /dev/urandom and the /proc and /sys entries Python asks for, the time zone data, the files the program was read from, a private temporary directory - and, under a net grant only, the resolver's configuration and the TLS roots. @N counts stay in the language.

A granted ffi module widens the OS policy to what that module needs, named per module (FFI_WIDENS). A module that is not in this table widens to nothing enforced - what it needs is not known, and a run that worked before 8.4 is not refused - and the audit's confinement.widened_by names every module that widened the policy and to what.

A granted modulewidens the OS policy to
abc, array, ast, base64, binascii, bisect, calendar, cmath, collections, colorsys, copy, csv, dataclasses, datetime, decimal, difflib, email, enum, fnmatch, fractions, functools, gc, hashlib, heapq, hmac, html, ipaddress, itertools, json, keyword, math, numbers, operator, pprint, random, re, reprlib, secrets, shlex, statistics, string, struct, textwrap, threading, time, tomllib, typing, unicodedata, uuid, zlib, zoneinfonothing
bz2, codecs, configparser, dbm, fileinput, glob, gzip, io, lzma, pathlib, shutil, sqlite3, tarfile, tempfile, xml, zipfileany path
ftplib, http, imaplib, poplib, select, selectors, smtplib, socket, ssl, urllib, xmlrpcany host
aiohttp, httpx, logging, requests, urllib3any path and any host
asyncio, builtins, code, concurrent, ctypes, importlib, multiprocessing, nt, os, pdb, pickle, pkgutil, platform, posix, pty, runpy, shelve, signal, site, subprocess, sys, venv, webbrowser, winreg, zipimportnothing enforced

The level is what was applied and held, not what was asked for. full: every file, network and process limit of this run's budget is held by the kernel. partial: some are, and confinement_reason names each that is not. none: nothing is - --no-confine, a module that widens to nothing enforced, a system that offers nothing, or a run that is exempt:

Not confinedWhy
sabline.run() with no timeout and no max_memory_mbit runs in the caller's process, which Sabline must not confine: Landlock, seccomp, a sandbox profile and a lowered token cannot be taken off again. Pass a limit, or use a Pool.
the REPL, sabline test, sabline benchthey run many programs in one process, each read after the last; a policy applied for the first would hold the rest
a Pool made without import_root, on readsits workers compile each program they are sent, and an import may name any .vel file the process can read, so reads are not held and the level is partial; writes, the network and processes are held
compiling, proving and native code generationdone before the policy is applied in a single run, since imports are read from wherever they are and Z3 and LLVM load their libraries; the program's first statement runs after it