Skip to content

sandbox grants are ignored inside a std.spec describe block #1704

Description

@paul-hammant

The same helper function, called from main, sees its sandbox grant honoured; called from inside a spec.describe block, the identical grant is denied.

Repro (~35 lines, no external deps)

import std.spec
import std.audit
import std.list
import std.os

fn add_perm(t: ptr, c: string, p: string) {
    list.add(t, c)
    list.add(t, p)
}

fn run_sandboxed(perms: ptr, code: fn) {
    sandbox_push(perms)
    sandbox_install()
    call(code, perms)
    sandbox_uninstall()
    sandbox_pop()
}

fn check(label: string) {
    audit.clear()
    perms = list.new()
    add_perm(perms, "env", "HOME")        // grant env:HOME
    w = | _ctx: ptr | { os_getenv("HOME") }
    run_sandboxed(perms, w)
    c, r, a = audit.entry(0)
    println("  ${label}: ${r} allowed=${a}")
}

main() {
    fw = spec.init()
    check("outside describe")
    spec.describe(fw, "d") {
        check("inside describe, not in it")
        spec.it("t") callback {
            check("inside it")
        }
    }
    spec.run_summary(fw)
}
  outside describe:            HOME allowed=1     <- grant honoured
  inside describe, not in it:  HOME allowed=0     <- same grant, denied
  inside it:                   HOME allowed=0

Measured on main @ 984236e, Linux x86_64.

What is and isn't the trigger

Bisected down from a failing test suite:

shape result
helper called from main allowed ✅
helper called from a plain fn allowed ✅
inside one plain trailing block (outer() callback { … }) allowed ✅
inside two nested plain trailing blocks allowed ✅
after spec.init() but outside describe allowed ✅
inside spec.describe(fw, "d") { … } denied
inside spec.it(...) callback { … } within a describe denied

So it is not trailing blocks as such, not closure nesting depth, and not spec.init. It is specifically the describe block — and it inherits it by being nested inside one. Note the third row shows the failure occurs in the describe body directly, before any it runs.

One structural difference worth pointing a debugger at: describe is declared -> ptr and returns a suite handle, so its trailing block is a value-producing closure, unlike the -> void wrappers in the passing rows. It also runs println and a ref_set on the framework's depth cell before the body executes.

Why it matters beyond tests

The failure mode is the dangerous direction: a grant that is silently ignored. Code that runs correctly at top level starts denying its own permitted operations when moved inside a describe, with no diagnostic — the audit trail records allowed=0 and the program takes its denial path.

That makes std.spec unable to test sandboxed behaviour at all, which is how this was found: writing the co-located std.audit suite for #1584. Any future suite covering std.audit, containment, or a sandboxed integration will hit the same wall.

Workaround

None found that keeps the assertions inside describe. A suite can drive the sandbox from main and assert on the recorded trail afterwards, but that gives up per-case isolation and the it reporting.

I have parked the std.audit suite rather than write it against behaviour I know to be wrong — std.audit is currently the only std module with no coverage anywhere, and this is why.

Environment

  • main @ 984236e, ae 0.567.0, Linux x86_64, gcc
  • Not yet checked on macOS or Windows

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions