Isolating Per-Tenant Computation Using Dyalog APL Namespaces
Dyalog namespaces provide logical name isolation for per-tenant computation. This guide covers the architecture for implementing this isolation, its trust boundaries, and when to move to process-level isolation.
06 Sept 2025, 03:04 UTC

The Problem
Running multiple tenants within a single Dyalog interpreter requires a way to separate variables and functions without the overhead of restarting the process for every request. The critical takeaway is that Dyalog namespaces provide logical name isolation, not security isolation. They prevent accidental name collisions and provide a structured way to organize tenant data, but they do not create a sandbox that blocks access to system-level primitives or shared memory.
Requirements
The system must ensure that each tenant has a private symbol table for variables and user-defined functions. The interpreter must remain "warm" to avoid the latency of process initialization. While tenants should be isolated from each other's data, they may require read-only access to a shared set of supervisor services.
The primary constraint is the name resolution chain. In Dyalog, a namespace is a container with its own symbol table and a parent lookup chain. When a name is referenced, the interpreter searches the current namespace, then its parent, and so on, up to the global workspace. This chain defines the trust boundary.
Smallest Suitable Design
The minimal design involves creating a dedicated root namespace for each tenant and ensuring no tenant code ever executes directly in the global workspace.
/ Create a namespace for a specific tenant
tenantName ← 'TenantA'
⎕NS tenantName
To manage execution, implement a supervisor wrapper that switches the context to the tenant's namespace, evaluates the code, and restores the previous context. This prevents a tenant from accidentally leaving the interpreter in their namespace for the next request.
RunInTenant ← {tenantName code
prev ← ⎕NS ''
⎕NS tenantName
result ← code
⎕NS prev
result
}
To limit the impact of side-effecting primitives, wrap dangerous functions (like file I/O) in a trusted supervisor namespace. By defining a local version of a primitive in the tenant's namespace that calls a logged supervisor function, you can create an audit trail for ⎕NREAD, ⎕NWRITE, and the external call interface.
Trust and Data Boundaries
The boundary is based on name resolution, not OS-level memory protection. Because the lookup chain eventually reaches the global workspace, any code running in a tenant namespace can still access global system functions and ⎕FSE unless the interpreter's overall security settings restrict them.
Certain interpreter states are workspace-wide and cross all namespace boundaries. These include:
⎕WSID: The workspace identifier.⎕WXand⎕SS: Global state and system settings.⎕SE: The system error handler.
If a tenant modifies a variable that exists in a parent namespace without first defining a local version, the change may propagate upward, potentially affecting the supervisor or other tenants.
Operational Checks
To maintain the integrity of the isolation, implement the following checks:
- Context Verification: Log
⎕NS ''before and after every tenant execution to ensure the namespace was correctly switched and restored. - Path Auditing: Periodically inspect
⎕PATHto verify the namespace depth. A typical secure chain should beTenantRoot → Supervisor → Global. - Primitive Monitoring: Audit calls to the external call interface and file system primitives to detect attempts to escape the logical boundary.
- Resource Sampling: Monitor total workspace memory. Since Dyalog lacks preemptive scheduling for APL code, a single tenant allocating a massive array can cause a Denial of Service (DoS) for all other tenants.
Failure Modes
Namespace Escape: A tenant may attempt to manipulate ⎕NS or ⎕PATH to move their execution context into the supervisor's namespace or the global workspace.
Global State Corruption: Since ⎕SE and ⎕WX are not namespace-local, a tenant that successfully assigns a value to these globals changes the environment for every other tenant in that interpreter instance.
Resource Exhaustion: An infinite loop or a memory-intensive operation in one tenant's code will block the single-threaded interpreter, freezing all other tenants until the process is killed or the operation completes.
Conditions for Design Change
This namespace-based approach should be abandoned in favor of separate interpreter processes or containers (e.g., Docker) if any of the following become requirements:
- Hard Security Isolation: The need to run truly hostile code that must be blocked from all system primitives.
- Resource Quotas: The requirement for hard CPU or memory limits per tenant.
- Fault Containment: The need to ensure that a crash or segmentation fault in one tenant's code does not terminate the entire service.
Verification
To verify the logical isolation, run the following test sequence:
/ 1. Verify local vs parent visibility
⎕NS 'TenantA'
x ← 10
⎕NS 'TenantB'
/ This should error or return a different x
⎕WR x
Next, verify that child namespaces can see parent variables but cannot overwrite them globally by default:
⎕NS 'Supervisor'
sharedVal ← 100
⎕NS 'TenantA'
/ Should return 100
sharedVal
/ Should only change local copy
sharedVal ← 200
⎕NS 'Supervisor'
/ Should still be 100
sharedVal
Finally, attempt to call a restricted system function from within the tenant namespace to confirm that your supervisor wrappers or interpreter security settings are active.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.