I started playing around with the race condition class of vulnerabilities,
and ran tuktam, (our TOCTOU SAST tool), over a few open-source projects to see what
would come up. In LightFTP 2.3.1 it flagged worker_thread_cleanup,
the function involved in the fix for
CVE-2024-11144.
I followed the static finding with source review and ThreadSanitizer testing,
and that work led to
CVE-2026-67607.
d28c5e0, whose source identifies itself as version 2.4,
is a distinct defect in a changed synchronization scheme and is tracked
separately as
CVE-2026-70637,
classified as CWE-820 (missing synchronization) rather than CWE-367; the
dynamically confirmed 2.4 defect is a data race, not a check/use ordering bug.
There is no tagged 2.4 release, so that record is pinned to the commit rather
than to a published version. ThreadSanitizer confirms unsynchronized
operations; it does not by itself prove a production-build crash or exploit.tuktam looks for time-of-check/time-of-use patterns. Its C and C++ analysis
is heuristic: it recognizes source-level patterns rather than proving full
program semantics. One rule family, TOCTOU-STATE, looks for an
unlocked check of shared state followed by an operation on a related handle or
identifier. I pointed it at LightFTP 2.3.1 and it landed on the cleanup function
associated with the earlier CVE:
Here, HIGH is tuktam's confidence tier for the detected pattern.
It is not a CVSS score or a claim about vulnerability severity. The relevant
shape in worker_thread_cleanup():
LightFTP 2.3.1 was released as a security update for CVE-2024-11144. The
patch added a re-check and reordered cleanup, but the re-check and
pthread_join() remained outside context->MTLock.
Worker-side code updates related state under a different scheme, and transfer
workers detach themselves. The re-check narrows one timing window but does not
establish a happens-before relationship for the shared lifecycle state. It also
leaves cleanup acting on a thread identifier whose lifetime can end or whose
value can change through reuse of the connection context. That is the basis for
treating 2.3.1 as an incomplete fix rather than a complete synchronization
repair.
ThreadSanitizer (TSan) is a runtime data-race detector available with GCC and
Clang. A build compiled with -fsanitize=thread instruments memory
accesses and synchronization operations, and reports conflicting accesses when
at least one is a write and it cannot establish an ordering between them. A TSan
report is strong evidence of missing synchronization in the executed path. It
does not, by itself, establish the exact security impact, a reliable remote
crash, or exploitability on an optimized production build.
I built LightFTP 2.3.1 under TSan and used an anonymous FTP account to issue
LIST followed by ABOR, entering cleanup while a
transfer worker was active. Anonymous access still involves an FTP login; the
demonstrated path does not require privileged credentials when anonymous access
is enabled.
The executed path produced a race report between cleanup reading the shared worker state and the transfer worker writing it:
This harness confirms unsynchronized access in the worker-cleanup state. In my normal release-build testing, the daemon kept running without a crash.
I reported the 2.3.1 finding as an incomplete fix for CVE-2024-11144. It was assigned CVE-2026-67607 by VulnCheck, classified as CWE-367, with LightFTP through version 2.3.1 listed as affected. The dynamic testing directly confirms the underlying race.
A public project
issue referencing the incomplete fix was closed with the label
invalid. That issue concerns the 2.3.1 release. I then examined the
development branch separately rather than treating it as part of the assigned
CVE.
The unsynchronized cleanup problem in the development branch was already
described publicly in
LightFTP issue #75,
opened by grant-yim on 22 June 2026, before my own testing. That
report covers the same worker-cleanup races against transfer workers, including
shared data_socket, file_fd, and abort state, with
ThreadSanitizer output and the same close()-versus-worker fd-lifetime race. The
2.4 results below should therefore be read as an independent reproduction and
additional coverage, not as the first public disclosure of the
development-branch issue.
The following results concern master commit d28c5e0, whose source
defines FTP_VERSION "2.4". This was a development snapshot, not a
tagged 2.4 release, and the master branch may have changed since. First, the
static scan:
That result means the specific source pattern recognized by tuktam was no
longer present: the worker thread ID was copied to a local variable before the
later pthread_cancel(), and pthread_join() had been
removed. It does not mean the lifecycle was synchronized or race-free. This is
the boundary of pattern-based static analysis. I then built that snapshot under
TSan:

I drove it with the same anonymous LIST plus ABOR
client and reviewed the resulting reports:

In the tested snapshot, an atomic compare-and-swap on
context->busy serialized worker startup, but it did not
synchronize cleanup against a worker that was already running. The control
thread could close and clear data-connection state while a transfer worker still
performed I/O. One report involved concurrent close() and
send() associated with the same descriptor lifetime:
The size and wording of this report come from TSan's syscall interceptors; it
should not be described as a plain eight-byte C read of
context->data_socket. The security-relevant observation is a
descriptor-lifetime race: cleanup closes an fd while the worker may still use a
copied value. Since descriptor numbers can be reused, wrong-resource access is a
possible consequence, but I did not demonstrate such reuse or cross-talk.
The number of reports varied with scheduling and concurrency. I extracted the distinct source locations from one captured log:
That log contained at least 16 distinct reported locations across cleanup, transfer, socket creation, command handling, and teardown. These are not 16 separate vulnerabilities. They are multiple observations that should be grouped under a smaller set of root causes: missing worker-lifecycle synchronization, unclear descriptor ownership, and teardown of shared context while a worker remains active.
In the lab, TSan reliably reported unsynchronized accesses in LightFTP
2.3.1 and in development commit d28c5e0. The 2.3.1 reports confirm a
race in the worker-cleanup state associated with CVE-2026-67607. In the development snapshot, TSan also
reported concurrent cleanup and worker operations involving shared state and
descriptor lifetimes; that distinct defect is tracked as
CVE-2026-70637,
pinned to commit d28c5e0 since no 2.4 release is tagged. The demonstrated result is therefore a remotely reachable
concurrency defect with undefined behavior and plausible availability impact. A
normal-build crash, descriptor-reuse exploit, data disclosure, or code execution
was not demonstrated by these tests.
Ask Dzitka LLM.
The same run flagged a filesystem check-then-use in libwebsockets. Its
recursive-delete callback lws_dir_rm_rf_cb in
lib/misc/dir.c calls readlink() to decide whether a
directory entry is a symlink, then acts on the same path by name, recursing with
lws_dir() or removing it with
rmdir()/unlink(). That is the classic recursive-delete
symlink shape.
This one is not a new issue. The libwebsockets source already documents the pattern: the code checks for symlinks precisely to avoid recursing into them, and a comment at that spot notes the developers "Hide this from Coverity since it flags any use of readlink() even if safe." Static analyzers already flag this shape, the maintainers have evaluated it, and they accept it in that context. tuktam reported the same thing Coverity does.
That is the recurring point: a static flag is a hypothesis, not a verdict. In LightFTP the hypothesis held up under ThreadSanitizer and became a CVE. In libwebsockets, it was a known, documented and accepted pattern.