Skip to content

CWE-243: Creation of chroot Jail Without Changing Working Directory

Overview

chroot() remaps a process's idea of the filesystem root to a new directory, but it does not change the process's current working directory. If the working directory isn't also moved inside the new root immediately afterward, a process can still reach files outside the jail through a relative path, making the jail cosmetic rather than a real containment boundary. MITRE scopes this to Unix-class C/C++ code, where chroot() is the relevant syscall.

Relationship to Other CWEs

MITRE places CWE-243 at the Variant level in Research Concepts (view-1000). Neither of the two parents listed below has a page here:

Not CWE-22 (Path Traversal). The escape here uses ../../.., so findings arrive looking like traversal, and the distinction decides the fix. CWE-22 is untrusted input reaching a path that the program builds; the fix is to validate and canonicalize that input. CWE-243 is the program's own paths resolving outside the boundary it believes it set up, with no untrusted input required - the fix is in the setup sequence, and no amount of input validation reaches it.

OWASP Classification

A01:2025 - Broken Access Control

Risk

High: A chroot jail set up without also changing into it does not actually contain anything. The process, or an attacker who has gained code execution inside it, can use a relative path such as ../../../etc/shadow to read or write files anywhere the process's underlying privileges allow. This is a well-documented, decades-old implementation mistake, not a theoretical one.

Remediation Steps

Core Principle: Creating the jail and moving into it are one operation, not two independent steps - never treat the jail as established until both have succeeded and privileges have been dropped.

Trace the Setup Path

  • Source: Any code path that calls the jail-creation syscall.
  • Sink: Every file operation the process performs afterward, which trusts that the working directory is inside the jail.
  • Missing control: No change to the working directory (and no privilege drop) between creating the jail and resuming normal operation.

Change Into the Jail Immediately After Creating It (Primary Defense)

// SECURE - pseudo-code
close_directory_handles()   // an open handle to an outside directory is a way back out
create_jail("/var/jail")
change_directory("/")       // must happen immediately after, with no file access in between
drop_privileges()           // a still-privileged process can call the jail syscall again to escape

Drop Privileges After Jail Setup

A process that keeps elevated privileges after entering the jail can often re-invoke the jail-creation syscall on a subdirectory it creates, then walk back up and out. Dropping to an unprivileged user/group immediately after the jail is established is part of the same fix, not an optional hardening step.

Prefer a Modern Isolation Mechanism

Where the platform supports it, a namespace- or container-based sandbox provides real filesystem, process, and network isolation instead of relying on a single syscall pair that has to be sequenced correctly by hand every time.

Test with Escape Attempts

  • Attempt to open a file outside the jail using a relative path (../../etc/shadow) immediately after setup, and confirm it fails with a not-found error rather than returning a handle - inside a correctly entered jail, .. at the root resolves to the root itself.
  • Confirm the process is not running with elevated privileges after setup completes.
  • Confirm the jail-creation syscall cannot be called a second time from inside the jailed process.
  • Enumerate the process's open file descriptors after setup and confirm none of them refers to a directory outside the jail.

Common Vulnerable Patterns

// VULNERABLE - pseudo-code
create_jail("/var/jail")
// working directory is still outside the jail - no change_directory call
open_file("../../../etc/shadow")  // relative path escapes the jail entirely

Why this is vulnerable: the call changes what / means and nothing else. The working directory is a separate piece of process state, and it goes on referring to the directory it referred to before - a directory outside the jail. So absolute paths are confined and relative paths are not, from the same process, at the same moment.

That split is what makes the mistake durable. A check that opens /etc/passwd from inside the jail gets the confined answer and the jail looks correct; only a relative path reveals it was never entered. Nothing errors, nothing is logged, and the call that created the jail returned success. The sandbox exists and the process is not in it.

Secure Patterns

// SECURE - pseudo-code
close_directory_handles()   // nothing open that points outside the jail
create_jail("/var/jail")
change_directory("/")
drop_privileges()

Why this works: Changing into the jail's root immediately after creating it removes the relative path back to the original filesystem - there is no longer anywhere outside the jail for a relative path to resolve to. Closing the handles first removes the other route out: a descriptor for an outside directory is a working directory the process can return to at any time, it survives the jail call by design, and no privilege check stands between the process and using it. Dropping privileges afterward closes the last escape route: a privileged process could otherwise call the jail-creation syscall again on an inner directory and walk back out.

All three steps answer the same question - is there anything left that resolves outside the new root - and a fix that does two of them leaves the jail escapable.

Common Pitfalls

  • Assuming the order of the two calls is what makes it safe: what matters is where the working directory ends up, not which call runs first. Changing into /var/jail and then creating the jail there leaves the working directory inside the new root and is a correct, widely used sequence, exactly as correct as creating the jail and then changing to /. The failure is a working directory left anywhere outside the jail - which is what happens when the process creates the jail and does not move, or moves somewhere that is not the jail. If you take the change-directory-first route, create the jail from "." rather than repeating the path, so a symlink swapped between the two calls cannot point the jail somewhere the working directory is not.
  • Leaving a directory file descriptor open across the call: the jail syscall does not close open file descriptors, and its own manual page says so. A descriptor for a directory outside the jail - inherited from a parent process, or opened before the call - is effectively a second working directory: seek to it by descriptor and the process is outside again, relative paths and all, with the jail still nominally in force. Close what the process does not need before creating the jail, and mark what it does keep close-on-exec.
  • Changing directory but not dropping privileges: a correctly jailed process that is still running with elevated privileges can call the jail-creation syscall a second time on a subdirectory and use it to climb back out - the jail alone was never a privilege boundary.
  • Treating the jail as sufficient isolation on its own: even set up correctly, this mechanism doesn't restrict process visibility, network access, or device access - it only remaps a filesystem path. A process that needs real isolation needs a namespace- or container-based sandbox, not this syscall pair alone.

Language-Specific Guidance

  • C - the concrete chroot()/chdir()/privilege-drop sequence, and container-based alternatives

Additional Resources