VNX-368: Context Switching Race Condition
A temporary switch into the root context (seteuid(0), setegid(0), setreuid(-1, 0), setregid(-1, 0), setresuid(-1, 0, -1) or setresgid(-1, 0, -1), also ::-qualified) that is not an atomic, contained step, in C or C++
| Field | Value |
|---|---|
| Rule ID | VNX-368 |
| Severity | Low |
| SARIF level | note |
| Kind | sast |
| Languages | C, C++ |
| CWE | CWE-368 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:L/AP:RU/AL:S/IC:N/FC:LT/RP:RU/RL:S/AV:L/AS:N/IN:A/SC:A/BI:L/DI:H/EX:L/EC:N/P:L |
| Tags | context-switch-race privilege cwe-368 |
| Source | internal/sast/rules/vnx-368.rego |
Overview
VNX-368 maps to CWE-368: Context Switching Race Condition. A temporary switch into the root context (seteuid(0), setegid(0), setreuid(-1, 0), setregid(-1, 0), setresuid(-1, 0, -1) or setresgid(-1, 0, -1), also ::-qualified) that is not an atomic, contained step, in C or C++. Reported when the file starts threads (pthread_create, thrd_create, std::thread, std::jthread, CreateThread): the effective ID is process-wide, so every other thread runs as root for the whole switch; when the function returns or jumps out (return, goto) between the elevation and the call that switches back, leaving the process in the root context on that path; and when the function never switches back at all. Not reported: a failure exit that belongs to the elevation call itself (if (seteuid(0) != 0) return -1), an elevation that restores root after the same function lowered it, permanent drops (setuid, setgid, setresuid with three IDs), comments and string literals. The order of a permanent drop is VNX-269, and exec after a temporary drop is VNX-270.
Why This Matters
The effective user and group IDs belong to the whole process. While one thread has switched to root with seteuid(0), every other thread runs as root too, and a function that returns between the switch and the switch back leaves the process in the root context.
What Gets Flagged
int open_device(const char *dev, uid_t user)
{
seteuid(0); /* FLAGGED: returns below while still root */
fd = open(dev, O_RDWR);
if (fd < 0)
return -1;
seteuid(user);
return fd;
}
What Is Not Flagged
if (seteuid(0) != 0) /* the elevation's own failure exit */
return -1;
fd = open(dev, O_RDWR);
seteuid(user); /* restored on every path, single-threaded file */
A braced failure block after the elevation (if (seteuid(0) == -1) { perror(...); return -1; }), an elevation after fork() in the same function (the single-threaded child), permanent drops (setuid, setgid, three-ID setresuid), an elevation that restores root after the same function lowered it, comments and string literals are not reported. The order of a permanent drop is VNX-269 and exec after a temporary drop is VNX-270.
Remediation
- Do privileged work in a separate, single-threaded helper process instead of switching the effective ID of a threaded one.
- Restore the unprivileged ID on every path out of the function (a single exit, or a cleanup label).
References
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-368
// nosec vnx-368
Or record an ignore rule: vulnetix ignore add --rule vnx-368 --reason "...". See Suppressing findings.