CIT-2640 · Network Security  //  Mandatory Access Control  //  CVE-2019-9213 Navigate: ← → or 1–5

01 — The core idea

Who decides what a process is allowed to do?

Two models answer that question. The difference is the whole point of this lecture — and it's clearest when the attacker already has root.

DAC — Discretionary Access Control

The owner of a resource sets its permissions (chmod, chown). Access is at the owner's discretion. The catch: root is exempt from the checks. This is standard Linux file permissions.

MAC — Mandatory Access Control

A system-wide policy, enforced by the kernel, decides access by the security label of the subject and object. No user — not even root — can waive it. SELinux is the implementation we'll use.

Scenario: a compromised web server tries to read the password hashes
Process: httpd  →  target: /etc/shadow
Attacker privilege
DISCRETIONARY (file permissions)
SUBJECT
httpd
uid = 48 (apache)
→
/etc/shadow
0 root root
mode 0000
DENIED
The file mode grants no access to "other", and apache is neither owner nor group.
MANDATORY (SELinux type enforcement)
DOMAIN
httpd_t
confined
→
TYPE
shadow_t
file · read
DENIED
No policy rule allow httpd_t shadow_t:file read exists. The kernel refuses regardless of uid.
Flip the switch. With DAC, the moment the attacker reaches root the file-permission check evaporates — root reads anything. With MAC, nothing changes: httpd_t has no rule permitting shadow_t:file read, so the kernel denies it even to uid 0. That is "mandatory": the policy is not the owner's to override, and root is not above it.

02 — The mechanics

Every subject and object wears a label

SELinux attaches a security context to every process and resource. A decision is just a lookup: does a rule allow this subject-type to perform this action on this object-type?

Anatomy of a security context

Click a field. This is what ls -Z and ps -Z show you.

system_u: system_r: httpd_t: s0
Four fields: user  :  role  :  type  :  level. The type is what type enforcement actually decides on.

The decision engine

Pick a subject, an object, and an action. SELinux is default-deny: if no rule explicitly allows it, it's refused.

# choose a subject / object / action and evaluate
Read the rule aloud: allow SUBJECT OBJECT_TYPE:CLASS ACTION; — if that exact line isn't in the loaded policy, the answer is "no". Try httpd_t reading shadow_t, then switch the subject to unconfined_t and watch it change.

03 — The real vulnerability

CVE-2019-9213 — mapping the forbidden zero page

A whole class of kernel exploits needs one thing first: a mapping at virtual address 0x0. Several independent layers exist to stop that. This CVE knocks one of them out.

① Hardware · SMAP

Supervisor Mode Access Prevention stops the kernel from touching user memory at all. That's why the CVE note says "on non-SMAP platforms" — older CPUs have no such guard.

② DAC · mmap_min_addr

/proc/sys/vm/mmap_min_addr (default 0x10000) blocks unprivileged mapping of low memory — you'd need CAP_SYS_RAWIO. A capability check.

③ MAC · SELinux memprotect:mmap_zero

A separate permission, bound to the process domain and decoupled from the tunable. Confined domains like httpd_t are denied the zero page even if an admin sets mmap_min_addr = 0.

# press a button to trace the stack
The bug in one line: expand_downwards() in mm/mmap.c (Linux < 4.20.14) grew a stack VMA without re-checking mmap_min_addr. So instead of calling mmap(0) — which the capability check refuses — an attacker places a VMA just above the guard and lets the stack grow down into the zero page. The DAC guard is simply never consulted. MITRE flags it as a "capability check for the wrong task."

04 — Put it together

Defense Lab: watch the attack meet each layer

Set the system's posture, then launch the exploit. The chain shows exactly where the attack is stopped — or why it isn't.

System posture

KernelCVE-2019-9213 status
SELinux modegetenforce
Process domainwho got popped
ADVANCED LAYERS
CPU SMAPhardware guard
mmap_min_addrvm tunable
—
Set a posture and launch.
# audit / dmesg output appears here
Instructor notes — the honest nuance (expand if the class is ready)

SELinux's clean win is the standard route. The memprotect:mmap_zero hook sits on the mmap() syscall path. It reliably stops a confined domain from the ordinary mmap(0) technique — and it does so even when mmap_min_addr has been zeroed (the classic "I disabled it to run WINE" footgun). That decoupling is the designed behaviour.

The CVE's stack-growth route is sneakier. expand_downwards() is not the mmap() path, so neither the capability check nor the mmap_zero hook fires on it. On a vulnerable, non-SMAP kernel the zero page gets mapped regardless of SELinux mode.

And once code runs in ring 0, it's above the reference monitor. A successful kernel-NULL-deref gives kernel-mode execution — at which point SELinux itself can be subverted. So for this bug the decisive controls are patch and SMAP. MAC's role here is to shrink the attack surface beforehand (least privilege on the service) and to own the standard route — not to magically contain ring-0 compromise. That distinction is the real defense-in-depth lesson.

05 — Takeaways

What to remember

DAC answers to the owner

File permissions are discretionary and root is exempt. Useful, but a single misconfig or a root compromise collapses it.

MAC answers to the policy

SELinux decides by label and rule, enforced in the kernel. Default-deny, and binding even on uid 0.

Confinement limits blast radius

A service pinned to httpd_t can only do what its domain allows — shrinking what a compromise can reach.

No single layer is complete

CVE-2019-9213 slipped past a capability check via stack growth. Layers — hardware, DAC, MAC, patching — cover each other's gaps.

The commands behind the demo

# current enforcement state getenforce # Enforcing | Permissive | Disabled sestatus # full status + policy name setenforce 0 # drop to permissive (log, don't block) — temporary # see the labels ps -eZ | grep httpd # subject contexts ls -Z /etc/shadow # object context -> system_u:object_r:shadow_t:s0 # investigate a denial ausearch -m avc -ts recent # find AVC denials in the audit log ausearch -m avc -ts recent | audit2allow # show the rule that would permit it # policy knobs getsebool -a | grep httpd # booleans toggle optional allows semanage fcontext ... # manage file-context mappings
Live option: if you have a terminal handy, setenforce 0, reproduce a denial, then setenforce 1 and show the AVC in ausearch. The simulator mirrors exactly those avc: denied lines.