Acquis 10 - Permissions
The pledge function provides capability-based sandboxing for Lus scripts. Permissions must be explicitly granted before sensitive operations like file access, network requests, or command execution can occur.
Basic usage
Grant permissions with the pledge function:
-- Grant filesystem access
pledge("fs")
-- Grant network access
pledge("network")
-- Grant command execution
pledge("exec")
Without the appropriate permission, operations are denied:
-- Without pledge("fs"):
local ok, err = catch fs.list("/tmp")
assert(not ok) -- permission denied
-- With pledge("fs"):
pledge("fs")
local files = fs.list("/tmp") -- works
Permission structure
Permissions use a hierarchical naming scheme:
-- Base permission: grants all sub-permissions
pledge("fs") -- grants fs:read, fs:write, etc.
pledge("network") -- grants network:tcp, network:http, etc.
-- Specific sub-permission: grants only that capability
pledge("fs:read") -- only reading, not writing
pledge("network:http") -- only HTTP, not raw TCP
Value restrictions
Permissions can include path or URL restrictions:
-- Grant read access only to /tmp
pledge("fs:read=/tmp")
-- Grant read access to all paths under /home
pledge("fs:read=/home/*")
-- Grant HTTP access to specific domain
pledge("network:http=api.example.com/*")
Glob patterns are supported for path matching. On systems with symlinks, paths are canonicalized before matching (e.g., /tmp resolves to /private/tmp on macOS).
Permission rejection
Use the ~ prefix to permanently reject a permission:
-- Reject network access
pledge("~network")
-- Later: cannot grant what was rejected
local ok = pledge("network") -- returns false
This is useful for host applications that want to restrict scripts from ever acquiring certain capabilities.
Sealing permissions
The seal permission prevents any future permission changes:
pledge("fs")
pledge("network")
pledge("seal")
-- After seal, new permissions are denied
local ok = pledge("exec") -- returns false
This provides defense-in-depth by locking down capabilities early in script execution.
Permission checks
The pledge function returns true on success, false on failure:
if pledge("fs") then
print("Filesystem access granted")
else
print("Filesystem access denied")
end
For sealed or rejected permissions, pledge returns false without erroring. For invalid permission names, it raises an error.
CLI usage
Grant permissions from the command line:
# Grant single permission
lus -Pfs script.lus
lus --pledge=fs script.lus
# Grant multiple permissions
lus -Pfs -Pnetwork script.lus
# Grant with restrictions
lus -Pfs:read=/tmp script.lus
# Reject permissions
lus -P~exec script.lus
Scripts can then request only what was already granted:
-- script.lus (run with: lus -Pfs:read=/tmp script.lus)
pledge("fs:read=/tmp") -- granted
pledge("fs:write") -- denied, not in CLI grant
Coroutine inheritance
When a coroutine is created, it receives a copy of its parent’s permissions. This copy is independent; changes in the coroutine don’t affect the parent, and vice versa.
pledge("fs")
pledge("network")
local co = coroutine.create(function()
fs.list("/tmp") -- inherits fs permission
-- Restrict our copy further
pledge("~network") -- reject network in this coroutine
pledge("seal") -- seal this coroutine's permissions
-- network is now denied in this coroutine
local ok = pledge("network")
assert(not ok)
end)
coroutine.resume(co)
-- Parent still has network access
pledge("network:http") -- works fine
Sandboxing with coroutines
This copy-on-create behavior enables sandboxing patterns where untrusted code runs with restricted permissions:
-- Parent has broad permissions
pledge("fs")
pledge("network")
pledge("exec")
local function run_sandboxed(fn, allowed_perms)
local co = coroutine.create(function()
-- Reject everything first
pledge("~fs")
pledge("~network")
pledge("~exec")
-- Then grant only what's allowed
for _, perm in ipairs(allowed_perms) do
pledge(perm)
end
pledge("seal") -- lock it down
fn() -- run the untrusted code
end)
return coroutine.resume(co)
end
-- Run untrusted code with only fs:read access
run_sandboxed(function()
local files = fs.list("/tmp") -- allowed
local ok = catch fs.remove("/tmp/foo") -- denied
end, {"fs:read"})
Progressive restriction
Coroutines can progressively restrict permissions as they delegate to less trusted code:
pledge("fs")
pledge("network")
-- First level: can read/write files
local level1 = coroutine.create(function()
pledge("~exec") -- no command execution
pledge("~network") -- no network
-- Second level: read-only filesystem
local level2 = coroutine.create(function()
pledge("~fs:write") -- remove write capability
pledge("seal")
-- Can only read files now
local data = io.open("/etc/hosts"):read("*a")
local ok = catch io.open("/tmp/x", "w") -- denied
end)
coroutine.resume(level2)
end)
coroutine.resume(level1)
Note: Permission changes in a coroutine are completely isolated. The parent thread’s permissions remain unchanged regardless of what the coroutine does with its copy.
Available permissions
| Permission | Description |
|---|---|
fs | All filesystem operations |
fs:read | Reading files and directories |
fs:write | Creating, modifying, deleting files |
load | Loading code at runtime (load, loadfile, dofile, require) |
exec | Running external commands (os.execute, io.popen) |
network | All network operations |
network:tcp | TCP socket connections |
network:udp | UDP socket operations |
network:http | HTTP/HTTPS requests |
The
loadfile,dofile, andrequirefunctions require bothfs:read(to read the file) andload(to execute the code). Theloadfunction requires onlyload.
Motivation
Lus scripts often handle untrusted input or run in sandboxed environments. The permission system provides controlled access to system resources.
Defense in depth
Scripts explicitly declare their needs, making it clear what capabilities are required:
-- Script header shows requirements
pledge("fs:read")
pledge("network:http")
-- Rest of script uses only these capabilities
Principle of least privilege
Restrict access to only what’s needed:
-- Only read from specific directory
pledge("fs:read=/var/log/app/*")
-- Script can read logs but nothing else
local logs = fs.list("/var/log/app")
catch fs.list("/etc") -- denied
Host control
Host applications can pre-reject capabilities before running scripts:
-- Host application
pledge("~exec") -- scripts cannot run commands
pledge("~network") -- scripts cannot access network
pledge("seal") -- lock it down
dofile("untrusted.lus") -- runs with restrictions