## https://sploitus.com/exploit?id=FE8307BF-7507-50DB-80F6-7328AB78C553
# tuktam
[](LICENSE-MIT)




[](https://github.com/zeroscience/tuktam/actions/workflows/ci.yml)
A focused, cross-language detector for **time-of-check / time-of-use** races
(CWE-367, CWE-362) - the bug class behind a large share of local privilege
escalations and remote DoS. It **finds** the pattern in source and then
**generates a runnable PoC that verifies the race is real**.
---
## The idea
Every TOCTOU reduces to one OS-agnostic invariant:
> A program **checks** a resource **by name / by shared state**, then **uses**
> it - instead of operating on a stable **handle** it already holds under a
> lock. An attacker mutates what the name/state refers to **in the gap**, so the
> use lands on something else (a symlinked secret, a recycled thread id).
Two concrete shapes fall out of that one invariant, and `tuktam` detects both:
| Family | Check | Use | Attacker swaps | Impact |
|---|---|---|---|---|
| **Filesystem** | `access`/`stat`/`exists` on a path | `open`/`chmod`/`unlink` on the same path | a symlink / junction / rename | privilege escalation, arbitrary read/write |
| **Thread / shared state** | `if (ctx->flag)` unlocked | `pthread_join(ctx->tid)` | the shared struct via a concurrent thread | crash / DoS, use-after-free |
| **App check-then-act** | `store.find(token)` | `store.create/consume(...)` | the record/token via a concurrent request | token reuse, duplicate accounts, double-spend |
Every engine emits the **same `Finding`** (`tuktam/model.py`). That shared schema
is the "universal" part - every engine speaks it, so one reporter and one PoC
generator serve them all.
---
## Feature summary
- **Languages:** Python (AST, precise), C/C++, Go, Java, C#/.NET, Rust,
TypeScript/JS, Ruby, PHP (heuristic engines).
- **3 rule families:** filesystem TOCTOU (`TOCTOU-FS`), unsynchronized
thread/state TOCTOU (`TOCTOU-STATE`, C/C++), and application non-atomic
check-then-act (`TOCTOU-APP`, Go/TS/JS/Java/C#/Ruby/PHP).
- **Taint-lite:** follows plain `a = b` / `a := b` aliases across the gap.
- **Confidence tiers:** `HIGH` / `MEDIUM` / `LOW`, based on precision and
aggravators (e.g. a `sleep()` widening a race window).
- **Numbered findings** + an interactive **`Y` / `N`** PoC prompt.
- **PoC generator:** emits a standalone script that verifies each race with a
*deterministic proof* and an *empirical win-rate*. Faithful symlink mode plus a
privilege-free fallback for Windows.
- **CI-ready output:** `text` / `json` / `sarif` (GitHub code scanning), plus a
`--fail-on-findings` gate.
- **No dependencies, no privileges, no target execution** for scanning - pure
Python stdlib, runs anywhere including Windows.
---
## Install & requirements
- **Python 3.8+** (stdlib only; `ast.get_source_segment` needs >= 3.8).
- No third-party runtime dependencies.
Install the `tuktam` command (isolated, recommended):
```bash
pipx install tuktam
```
Or with pip / from a clone:
```bash
pip install tuktam # from PyPI
pip install -e . # editable, from a checkout
```
Then run it as `tuktam `. No install needed either - the package also
runs in place:
```bash
python -m tuktam
```
Development (tests):
```bash
pip install -e ".[dev]"
pytest
```
---
## Usage
```
python -m tuktam TARGET [options]
positional:
TARGET file or directory to scan
options:
-f, --format {text,json,sarif} report format (default: text)
-o, --output FILE write the report to FILE
--fail-on-findings exit 1 if any finding (CI gate)
--poc N non-interactively generate a PoC for finding #N
--poc-dir DIR output dir for PoCs (default: poc/)
--prompt force the interactive PoC prompt (even when piped)
--no-prompt never show the interactive PoC prompt
--no-banner suppress the startup banner
--version show version and exit
```
**Samples**
```bash
python -m tuktam ./src # scan a tree
python -m tuktam app.py # scan one file
python -m tuktam ./src -f sarif -o tuktam.sarif # SARIF for GitHub code scanning
python -m tuktam ./src --fail-on-findings # CI gate
python -m tuktam app.py --poc 3 # PoC for finding #3, no prompt
python -m tuktam app.py --poc 3 --poc-dir out/ # ...into out/
```
After a text-mode scan on a terminal, findings are numbered and you're asked:
```
Create a PoC script for a detected vuln? [N / Y, e.g. Y3]: Y3
[+] PoC #3 written: poc/poc_003_python_access_chmod.py
run it: python poc/poc_003_python_access_chmod.py
```
`Y3` -> PoC for finding #3 - `N` (or empty) -> skip - bad input -> re-asks with the
valid range. Accepts `Y3`, `y 3`, `y3` (case-insensitive).
---
## Output formats
**text** (default)
```
tests/fixtures/vuln.py
#1 [HIGH ] TOCTOU-FS check exists()@6 -> use open()@7 on `path` (python)
'exists' checks the path, then 'open' re-opens it by name - attacker
can swap the target in between.
fix: Open the path ONCE to a handle/fd, then operate on that fd
(os.open(..., O_NOFOLLOW|O_EXCL) + os.fstat). Never re-resolve the name.
```
**json** - the raw `Finding` list (`file`, `check_line`, `use_line`, `path_expr`,
`check_call`, `use_call`, `language`, `confidence`, `rule_id`, `message`,
`suggestion`, `index`). Ideal for piping into other tooling.
**sarif** - SARIF 2.1.0 with a tool driver (name/version/organization) and both
rule descriptors, so GitHub code scanning renders findings inline on PRs.
---
## Desktop GUI
A stdlib **Tkinter** GUI ships alongside the CLI - same engine, still no extra
dependencies. Launch it with:
```bash
tuktam-gui # installed
python -m tuktam.gui # from a checkout
```
Pick a file or folder, hit Scan, and browse findings grouped by file and colored
by severity. Selecting a finding shows its message and fix plus the offending
source with the check and use lines highlighted; one click generates a PoC for
it, and the whole run exports to JSON or SARIF from the same window.

*Findings grouped by file and colored by severity; the selected finding shows its
message, fix, and source with the check (amber) and use (red) lines highlighted.*
### Windows .exe
Bundle a standalone, no-Python-required executable with PyInstaller:
```bash
pip install pyinstaller
pyinstaller --onefile --windowed --name tuktam-gui packaging/tuktam_gui.py
# -> dist/tuktam-gui.exe
```
## How detection works
Scanning is **static**: the code is parsed/scanned, never executed. Files are
dispatched by extension in `tuktam/static/engine.py`:
- `.py` -> `rules_python.scan_python` (AST)
- `.c/.cpp/.h/...` -> `languages` heuristic **+** `rules_state.scan_state`
- `.go/.java/.cs/.rs` -> the `languages` heuristic engine
### Family 1 - filesystem TOCTOU (`TOCTOU-FS`)
The invariant: within one function scope, a **check-by-name** call is followed by
a **use-by-name** call on the *same* path expression, with no atomic handle in
between.
- **Python** (`rules_python.py`) uses the real **AST** - true scopes, true
argument nodes - so it is precise. Direct hits are `HIGH`.
- **C, Go, Java, C#, Rust** share one engine (`heuristic.py`) driven by
per-language pattern tables (`languages.py`). It uses regex for the check/use
calls and **brace-depth scoping** so a check only pairs with a use in the same
(or a nested) block. Method-style checks (`f.exists()`, `p.is_file()`) capture
the *receiver* as the path; call-style checks capture the argument.
Check/use vocab per language (excerpt):
| Lang | check | use | safe form |
|---|---|---|---|
| C | `access` `stat` `lstat` | `open` `fopen` `unlink` `chmod` | `openat(...,O_NOFOLLOW)` + `fstat(fd)` |
| Python | `os.path.exists` `os.access` | `open` `os.chmod` `shutil.copy` | `os.open(...,O_EXCL)` + `os.fstat` |
| Go | `os.Stat` `os.Lstat` | `os.Open` `os.Remove` `os.WriteFile` | `os.OpenFile` handle |
| Java | `f.exists()` `Files.exists` | `new FileInputStream` `Files.delete` | `NOFOLLOW_LINKS` channel |
| C# | `File.Exists` | `File.OpenRead` `File.Delete` | `FileMode.CreateNew` |
| Rust | `fs::metadata` `p.is_file()` | `File::open` `fs::remove_file` | `OpenOptions` + `O_NOFOLLOW` |
### Family 2 - thread/state TOCTOU (`TOCTOU-STATE`)
Not every TOCTOU touches the filesystem. `rules_state.py` (C/C++) detects the
**unsynchronized shared-state race**: a thread reads a shared field in a guard,
then acts on a related handle without holding the lock other code uses for it.
Per function (brace-scoped) it looks for:
1. **Sink** - `pthread_join` / `pthread_cancel` / `pthread_detach` / `pthread_kill`
on an `obj->field` (a thread handle).
2. **Guard** - an earlier `if/while (... obj->field ...)` on the **same** base
object.
3. **Unsynchronized** - the function takes **no** mutex
(`pthread_mutex_lock`, `EnterCriticalSection`, ...). If it does, the function is
skipped (keeps false positives low).
4. **Aggravator** - a `sleep()`/`usleep()`/`Sleep()` between guard and sink -> the
window is huge and attacker-triggerable -> `HIGH`.
This is the exact shape of **CVE-2024-11144** (see the case study below).
### Family 3 - app check-then-act (`TOCTOU-APP`)
The dominant *modern* race class (most 2026 race-condition CVEs). Not a symlink,
not a thread - application **logic**: a shared record/token is checked, then
mutated, with no transaction or lock. Two concurrent requests both pass the check
before either acts, so a one-time action happens twice (invite-token reuse,
duplicate accounts from a single-use token, double-spend).
`rules_app.py` runs over Go, TypeScript/JS, Java, C#, Ruby, and PHP (brace-scoped
per function) and flags:
1. **CHECK** - a lookup/verify call (`find*`, `first`, `exists`, `verify`, ...).
2. **ACT** - a later mutation (`create`, `insert`, `update`, `delete`,
`consume`, `revoke`, ...).
3. **Correlated key** - the two calls share a meaningful identifier
(`token`, `email`, `invite`, ...), so unrelated calls aren't paired.
4. **No atomicity guard** in scope - if the function uses a transaction, row lock
(`SELECT ... FOR UPDATE`), `ON CONFLICT`, `synchronized`, mutex, or CAS, it's
skipped.
```
tests/fixtures/app_invite.ts
#1 [MEDIUM] TOCTOU-APP check find()@3 -> use update()@8 on `token` (ts)
Non-atomic check-then-act on shared state: 'find(...)' then 'update(...)'
share key 'token' with no transaction/lock in scope - two concurrent
requests can both pass the check before either acts (CWE-367).
```
This family is heuristic and cross-language -> `MEDIUM` confidence. The keyed
correlation and guard suppression are what keep it precise: in the fixtures, the
transaction-wrapped `*Safe` variants produce **zero** findings.
### Taint-lite
Both engines follow **plain copies** (`a = b`, `a := b`, `let a = b`) across the
gap, so a check on one name pairs with a use of its alias. Alias-linked findings
are reported at **lower** confidence to signal the indirection. Path-building
(`a = join(dir, name)`) and reassignment ordering are intentionally *not* modeled
yet - that's full dataflow, and it's future work.
### Confidence tiers
| Tier | Meaning |
|---|---|
| **HIGH** | Python AST direct hit, **or** a state race with a `sleep()` window |
| **MEDIUM** | Heuristic direct hit, Python alias-linked hit, or an unslept state race |
| **LOW** | Heuristic alias-linked hit (indirection increases FP odds) |
---
## How PoC verification works
`tuktam/poc.py` turns a `Finding` into a **standalone, runnable Python script**.
It reconstructs the detected `check -> use` window and swaps what the target
resolves to *inside* that window, then reports two independent results:
1. **Deterministic proof** - the attacker is fired exactly once inside the gap.
If the use lands on the SECRET target, the script prints `RACE WON` and the
pattern is proven exploitable *in the model*.
2. **Empirical win-rate** - a free-running attacker thread races the victim over
thousands of trials; the script reports how often it wins. This shows the race
is winnable **without** an artificially widened window.
For **filesystem** findings the "use" is mapped to the right attack - **read**
(leak a secret), **write** (overwrite a protected file through a symlink), or
**chmod** (make a secret world-writable):
- **symlink mode** (default, faithful): follows a symlink to a protected target -
real privilege escalation. Needs symlink privilege (Linux/macOS; Windows with
Developer Mode/admin).
- **`--content-swap`** (privilege-free fallback): swaps file *content* instead of
a link; auto-engages when symlinks aren't permitted. Enough to demonstrate
**read** races on locked-down Windows.
- **`--wide`**: widens the window with a tiny sleep - a deterministic teaching demo.
For **state** findings the PoC models the thread lifecycle: a shared `{valid, tid}`
struct, a victim that checks `valid` then joins `tid` without a lock, and a
concurrent thread that recycles the context in the gap -> the join hits a stale id
-> modeled `SIGSEGV` (DoS).
For **app** findings the PoC models the shared store: two concurrent requests
both pass the check for a single-use key before either acts -> the one-time action
runs twice (`RACE WON`). `--wide` drives the empirical rate to 100%.
### "Is it 100% when the race is won?"
Short answer: **the deterministic proof is 100% conclusive about the *logic*, not
about your *production binary*.**
- When the deterministic proof prints `RACE WON`, it has *proven* that the
check->use gap allows the use to land on an attacker-controlled target. There is
**no false positive on the pattern** - the window is real and the logic is
exploitable. That's the 100% claim, and it's a claim about the reconstructed
model of the detected pattern.
- It does **not** prove that *your specific deployed program* is exploitable
end-to-end. Real exploitation still depends on **reachability** (can an attacker
reach that code path?), **timing** (how wide is the real window? - see the
empirical rate), and **privileges** (symlink rights, filesystem layout).
- The **empirical win-rate** is your realism gauge: filesystem races typically win
a single-digit-to-low-double-digit percentage of narrow-window trials, which is
plenty for a patient attacker retrying in a loop.
So: `RACE WON` (deterministic) = "this is a genuine TOCTOU, not a false alarm."
Empirical % = "here's how winnable it is in practice." Turning that into a live
exploit against a real target is the last mile you drive by adapting the PoC's
clearly-marked `victim()` section. The generator is `tuktam/poc.py`; the CLI glue
(numbering + `Y` prompt + `--poc N`) is `tuktam/cli.py`.
---
## Real-world case study: LightFTP CVE-2024-11144
**Target:** [hfiref0x/LightFTP](https://github.com/hfiref0x/LightFTP) v2.3 -
**Advisory:** Black Duck CyRC, CVE-2024-11144 (CVSS 7.5, remote DoS) -
**Bug:** unsynchronized thread-lifecycle TOCTOU in `worker_thread_cleanup`.
**Step 1 - the filesystem engine alone finds nothing** (this is *not* a
filesystem race):
```
$ python -m tuktam LightFTP/Source
No TOCTOU patterns found.
```
**Step 2 - with the `TOCTOU-STATE` family, the scanner lands on the exact line
the advisory names (`ftpserv.c:240`):**
```
LightFTP/Source/ftpserv.c
#1 [HIGH] TOCTOU-STATE check context->WorkerThreadValid()@232 -> use pthread_join()@240 on `context->WorkerThreadId` (c)
Unsynchronized thread-state race: 'context->WorkerThreadValid' is checked,
then 'pthread_join(context->WorkerThreadId)' runs with no mutex held and a
sleep() widens the window - a concurrent thread can invalidate the handle
in the gap (CWE-362/367).
#2 [HIGH] TOCTOU-STATE check context->WorkerThreadValid()@232 -> use pthread_cancel()@244 on `context->WorkerThreadId` (c)
```
The vulnerable code:
```c
void worker_thread_cleanup(PFTPCONTEXT context) {
if ( context->WorkerThreadValid == 0 ) { // TIME OF CHECK (line 232)
context->WorkerThreadAbort = 1;
sleep(2); // 2-second window, attacker-triggerable
err = pthread_join(context->WorkerThreadId, &retv); // TIME OF USE (line 240)
...
}
}
```
Note that `worker_thread_start()` right below *does* take `pthread_mutex_lock(&context->MTLock)`
for the same state - but `worker_thread_cleanup()` doesn't. That asymmetry is the
bug: an EPRT-command anomaly triggers cleanup while a fresh connection reuses the
context, so `WorkerThreadId` changes during the `sleep(2)` and `pthread_join`
crashes the server.
**Step 3 - the patch analysis.** On the *fixed*
v2.3.1 the scanner **still flags the pattern** (now at lines 253/257):
```
LightFTP/Source/ftpserv.c
#1 [HIGH] TOCTOU-STATE check context->WorkerThreadValid()@232 -> use pthread_join()@253 ...
```
That's correct: the official fix **narrows** the window (re-check
`WorkerThreadValid == -1` before join, close sockets earlier, split the sleep) but
**never adds a mutex** to `worker_thread_cleanup`. The unsynchronized access
remains - the fix reduces the odds rather than eliminating the race. `tuktam`'s
suggestion is exactly the durable fix: hold `context->MTLock` across *both* the
check and the join.
**Step 4 - the generated PoC verifies the DoS model:**
```
$ python -m tuktam LightFTP/Source/ftpserv.c --poc 1
$ python poc/poc_001_c_context_WorkerThreadValid_pthread_join.py
TOCTOU state PoC -- context->WorkerThreadValid -> pthread_join
================================================================
[*] Deterministic proof:
[+] RACE WON (DoS): pthread_join on invalid tid 62715 -> SIGSEGV
[+] check 'context->WorkerThreadValid' then 'pthread_join' is UNSYNCHRONIZED and exploitable.
[*] Empirical race: modeled cleanup calls hit an invalid join.
```
The PoC is a faithful *model*; a live exploit drives the real server (open a
session, trigger the EPRT anomaly while racing a fresh connection). The scanner
found it, explained it, out-analyzed the patch, and produced a verification - the
whole `find -> explain -> verify` loop on a real CVE.
---
## Module reference
```
tuktam/
โโโ __init__.py version/author/vendor constants + BANNER
โโโ __main__.py enables `python -m tuktam`
โโโ cli.py arg parsing, finding numbering, Y/N PoC prompt, --poc
โโโ model.py Finding dataclass - the one schema every engine emits
โโโ report.py text / json / sarif renderers
โโโ poc.py PoC generator (filesystem + state + app templates)
โโโ static/
โ โโโ engine.py walks a path, dispatches files by extension
โ โโโ rules_python.py Python AST engine (TOCTOU-FS) + taint-lite -> precise
โ โโโ heuristic.py generic regex + brace-scope engine + taint-lite
โ โโโ languages.py per-language pattern tables (C/Go/Java/C#/Rust)
โ โโโ rules_c.py back-compat shim over the heuristic engine
โ โโโ rules_state.py thread/shared-state engine (TOCTOU-STATE, C/C++)
โ โโโ rules_app.py app check-then-act engine (TOCTOU-APP, Go/TS/JS/Java/C#/...)
tests/fixtures/ vulnerable + safe samples across the supported languages
```
---
## Limitations
- Non-Python engines are line/brace **heuristics**, not full parsers -> occasional
false positives/negatives. Python (AST) is precise.
- **Taint-lite** follows only plain `a = b` copies - not path-building or
reassignment ordering. Full dataflow is future work.
- `unlink`-style deletes in the filesystem PoC are demonstrated via destructive
overwrite-through-symlink; a true directory-swap `unlink` race needs a dedicated
harness (noted in the generated PoC).
- Static analysis reports *potential* races. The PoC proves the *pattern* is
exploitable; end-to-end exploitation of a specific binary still depends on
reachability, timing, and privileges.
---
## License
MIT ยฉ Zero Science Lab
---
*tuktam v3.42 (build 1700, "Kumanovo").*