skip to content

Explain how AccessController.doPrivileged worked together with checkPermission and the call-stack-based permission model.

level: seniorimportance: nice to knowfreq 25%

answer

  1. Stack walk = intersection of all callers' permissions
  2. Least-privileged caller wins
  3. doPrivileged stops the stack walk at that frame
  4. Use case: library reading its own config from sandboxed caller
  5. Danger: never pass untrusted input into a privileged block

basics

~20 s

Java checked permissions by looking at every method on the current call stack — the action was only allowed if all callers had the needed permission. doPrivileged let trusted code say "use my permissions here, ignore my untrusted caller," so a library could perform a privileged step on behalf of restricted code.

solid answer

~50 s

Under the SecurityManager, a permission check was stack-based. When code called checkPermission (directly or via a checkXxx helper), the AccessController walked the entire current call stack and required that every frame's protection domain be granted that permission; if any caller lacked it, an AccessControlException was thrown. This 'least privilege of any caller wins' rule prevented untrusted code from gaining rights by calling into trusted code. AccessController.doPrivileged was the deliberate escape hatch: when trusted library code wrapped an action in doPrivileged, the stack walk stopped at that frame, so only the library's own permissions (not its callers') were considered. A classic example: a logging library opening its own config file even when called from sandboxed code — it does the file read inside doPrivileged so the untrusted caller's lack of FilePermission doesn't block it. doPrivileged had to be used carefully: it must perform a narrow, well-controlled action, never pass attacker-controlled input through, or it becomes a privilege-escalation hole.

go deeper

for a junior

Can state that permissions were checked against the call stack and that doPrivileged let trusted code run a privileged step; not expected to know intersection semantics.

for a middle

Understands the stack walk requires all callers to have the permission and that doPrivileged stops the walk; can give the library-config example.

for a senior

Explains protection domains, the intersection rule, AccessControlContext combination, and the security pitfalls of doPrivileged (no untrusted input, narrow scope).

for a principal

Can critique the model's complexity/correctness/performance costs, relate it to confused-deputy theory, and lead a migration off AccessController/doPrivileged given JEP 411 removal.

## Why a stack walk? The SecurityManager had to answer: "is *this* operation allowed *right now*?" The danger is **confused-deputy** attacks — untrusted code tricking trusted code into doing something privileged on its behalf. Java's answer was a **stack-based access control** model: an operation is permitted only if **every method currently on the call stack** is allowed to do it. ## Protection domains Every class belongs to a **ProtectionDomain**, derived from its **CodeSource** (where it came from — a jar/URL, and signers) and the **permissions** the policy grants that source. Trusted local code might have `AllPermission`; downloaded applet code might have almost none. ## The check Sensitive JDK methods called `securityManager.checkPermission(perm)` (or a convenience `checkRead`/`checkConnect`, which built the right Permission and delegated). That delegated to `AccessController.checkPermission(perm)`, which **walked the stack** from the current frame outward. For each frame it took that frame's ProtectionDomain and asked, "does this domain imply the requested permission?" If **any** frame's domain did **not** imply it, the walk threw `AccessControlException` (a subclass of `SecurityException`). So the *effective* permission set was the **intersection** of every caller's permissions — the least-privileged caller on the stack constrained everyone. This is what stopped untrusted code from "borrowing" a trusted method's rights merely by calling it. ## The problem this creates Intersection-of-callers is safe but too strict. Consider a trusted utility — say a logging framework — that must read **its own** configuration file. If sandboxed application code (with no `FilePermission`) calls `log.info(...)`, the untrusted frame is on the stack, so the intersection excludes file reads, and the legitimate config read fails. The trusted library *should* be allowed to read *its own* file regardless of who called it. ## AccessController.doPrivileged — the escape hatch `doPrivileged` told the stack walk: **stop here**. When trusted code executed an action inside `AccessController.doPrivileged(PrivilegedAction)`, the access-control stack walk terminated at that frame and considered only **that code's own** ProtectionDomain, ignoring callers further out (the untrusted ones). So the logging library wraps its config read: ```java String cfg = AccessController.doPrivileged( (PrivilegedAction<String>) () -> readConfigFile("logging.properties")); ``` Now the read succeeds on the library's permissions even when invoked from sandboxed code. There were variants: `PrivilegedExceptionAction` (for checked exceptions), and overloads taking an `AccessControlContext` to *combine* a previously captured context (so you grant only the intersection of "my rights" and "the rights at the time the context was captured"). ## The danger of doPrivileged `doPrivileged` is a **privilege-escalation primitive**. If a library does something privileged with **attacker-controlled input** inside `doPrivileged` — e.g. opens whatever file *path the untrusted caller passed in* — it becomes a hole: the caller couldn't open that file directly, but the deputy does it for them. Rules: keep the privileged block **small**, do a **fixed, well-understood** action, **don't** pass untrusted arguments into it, and don't return capabilities (open streams, references) that leak privilege back to the caller. ## Why this whole model is being removed The stack-walk model is subtle, easy to get wrong, expensive (it instrumented hot JDK paths), and tied to the now-dead applet use case. JEP 411 deprecated the SecurityManager — and with it `AccessController`/`doPrivileged` — for removal. Modern code is migrating off these APIs; isolation moves to the process/OS/container boundary. ## Key takeaway Permission checks walked the stack and required *every* caller to have the permission (intersection rule), preventing confused-deputy escalation; `doPrivileged` stopped the walk so trusted code could act on its own authority — a powerful but dangerous escape hatch that must never launder untrusted input.

  • What is a confused-deputy attack and how did the stack-walk model defend against it?
    A confused deputy is privileged code tricked into misusing its authority for a less-privileged caller. The stack walk defends by requiring every frame (including the untrusted caller) to hold the permission, so the deputy's extra rights don't help the caller — unless the deputy explicitly uses doPrivileged.
  • Why is wrapping a file open in doPrivileged with a caller-supplied path dangerous?
    Because it launders the caller's lack of permission: the untrusted caller couldn't open the file directly, but the privileged block opens whatever path it's handed, effectively granting the caller access it should not have — a privilege escalation.

saying these in an interview costs you the question

  • Saying doPrivileged grants AllPermission (it grants only that code's own domain permissions)
  • Thinking the check used the union of callers' permissions (it was the intersection)
  • Believing doPrivileged is safe to wrap around arbitrary caller-supplied operations
  • Confusing AccessControlException with a generic RuntimeException unrelated to security

context