Skip to main content

TECH VEDA

Linux kernel & Device drivers starts on 24th Oct 2026 enrollingCorporate on-site training - Submit proposal Pick your modulesSharpen your kernel skills: deep dives, drivers, Yocto, CVEs, careers — updated daily. Read the blog →Embedded Linux fast track starts 30th sept 2026 enrollingEmbedded Linux Mastery track starts 30th sept 2026 enrollingLinux systems engineering starts 30th sept 2026 enrolling
Insights

LTP CVE Tests: How to Prove a Kernel Fix Reached Your Board

LTP CVE Tests: How to Prove a Kernel Fix Reached Your Board

Your BSP reports a kernel version that should contain a given CVE fix. That is a claim about the tag it was branched from, not a statement about the binary you flashed. The Linux Test Project maps 104 CVE numbers to reproducers that settle it by execution, and the 1 October 2026 release added seven entries. This article covers what those LTP CVE tests do, why a skipped one looks identical to a passed one in a results file, and why the runner most validation scripts still call now exits with an error instead of running anything.

An earlier piece here worked through how to tell whether a CVE fix exists for the series you ship, and ended on the point that a version string survives any amount of divergence, so it is evidence of intent rather than of content. This piece takes the next step. Suppose the fix does exist for your series and your vendor says it is in. LTP CVE tests are the part of the toolchain that makes the running kernel answer for itself.

Scope: any team that builds its own kernel for a board and has to produce evidence about it, for a customer, an auditor or its own release gate. The release is LTP 20260930, published 1 October 2026, and every file quoted below was read at that tag or at the stable kernel tag named beside it.

What LTP CVE tests actually are

LTP groups tests into plain-text files under runtest/, and one of them, runtest/cve, maps CVE numbers to the reproducer for each. The format is one line per entry, the CVE identifier followed by the test command:

cve-2016-5195 dirtyc0w
cve-2022-0847 dirtypipe
cve-2026-43499 ghostlock
cve-2026-53362 setsockopt11
cve-2026-64600 refluxfs

At tag 20260930 that file holds 104 such entries, the oldest being cve-2011-0999. The suite is cumulative, so it is a replayable record of kernel exploits, and the question it answers for each is narrow: does this kernel still do what made that CVE a CVE?

The reproducer is written to the point. testcases/cve/ghostlock.c, the entry for CVE-2026-43499, carries copyright lines for both the Linux Test Project and Nebula Security, the group that disclosed the flaw, and its header states what it targets and where the fix landed:

Test for CVE-2026-43499 (GhostLock), a stack use-after-free in the
rtmutex PI code, fixed in kernel v7.1:
3bfdc63936dd ("rtmutex: Use waiter::task instead of current in
remove_waiter()")

On a vulnerable kernel the result is a crash

The ghostlock header carries one more line, and it changes how the suite has to be run:

Beware, this test will crash the system on a vulnerable kernel.

That is the design. The reproducer builds a three-futex priority-inheritance deadlock and calls futex(2) with FUTEX_CMP_REQUEUE_PI; on an unfixed kernel the rollback from -EDEADLK leaves the waiter’s pi_blocked_on pointer dangling on its own stack. The waiter sprays that stack with non-canonical addresses through prctl(2) while the main thread calls sched_setattr(2) to force a chain walk, and the walk dereferences them.

So LTP CVE tests of this kind are not pass-or-fail checks you drop into a loop alongside a build. They need a board you can power-cycle remotely, a serial console being captured to a file, and an operator who treats a lost console as a result rather than as a flake. Several of these tests also detect the softer outcome, where the kernel complains but survives. Ghostlock asks for that explicitly:

.taint_check = TST_TAINT_W | TST_TAINT_D,

Those are bit 9 and bit 7 of the kernel taint mask, a warning issued and an oops or BUG, and the library reads it automatically at the end of any test that sets taint_check. A kernel that warns under the reproducer and survives still fails. Taint also persists until reboot, and LTP handles a pre-tainted kernel in two different ways: an already-set warning bit is dropped from the mask with a TCONF message, so that detector goes quiet, while an already-set oops bit ends the test with TBROK before it runs. Reboot between these tests.

There is one more property of the file to plan around. The last twelve of the 104 entries sit below this line:

# Tests below may cause kernel memory leak

Among them are fsconfig03, af_alg08, xfrm01 and sctphantom. Twelve deliberate leaks in sequence will exhaust a small board, so run that group last or on its own boot.

A skipped test is not a passed test

This is the failure that actually costs teams something, because it produces a clean report. LTP CVE tests declare what the kernel must provide before they can run. Ghostlock declares it in its test structure:

static struct tst_test test = {
	.runtime = 180,
	.needs_checkpoints = 1,
	.needs_kconfigs = (const char *[]) {
		"CONFIG_CHECKPOINT_RESTORE=y",
		"CONFIG_FUTEX_PI=y",
		NULL
	},
	.taint_check = TST_TAINT_W | TST_TAINT_D,
	.tags = (const struct tst_tag[]) {
		{"linux-git", "3bfdc63936dd"},
		{"CVE", "2026-43499"},
		{}
	},
};

Take the two config symbols in turn, because they behave differently. CONFIG_FUTEX_PI is a hidden symbol in init/Kconfig: it depends on FUTEX and RT_MUTEXES and defaults to y, and CONFIG_FUTEX itself defaults to y and implies RT_MUTEXES. It is normally on without anyone deciding so. Because imply is weaker than select, a configuration that switches RT_MUTEXES off takes FUTEX_PI with it, but that is unusual.

CONFIG_CHECKPOINT_RESTORE is the problem. In init/Kconfig at v6.6.157 it is a user-visible option, bool "Checkpoint/restore support", and it is default n. It has to be asked for. And it is not asked for by the reference configuration most board ports start from: the string CONFIG_CHECKPOINT_RESTORE appears zero times in arch/arm64/configs/defconfig at v6.6.157, at v6.12.111 and at v7.2.9.

So on a kernel built from the stock arm64 defconfig, the ghostlock reproducer does not run. It skips, and a skip is not a failure. A results file counting failures reports none, the gate passes, and nothing was tested. The skip is in the test output; it is not in the summary line.

Checking is two commands, and both run on the target rather than on the build host, because the question is about the kernel that booted:

raghu@techveda.org:~$ zcat /proc/config.gz | grep -E 'CHECKPOINT_RESTORE|FUTEX_PI|RT_MUTEXES'
raghu@techveda.org:~$ kirk --run-suite cve --dry-run

The first needs CONFIG_IKCONFIG_PROC in the build, which is worth having on any board you intend to audit; failing that, read the .config that produced the image. The second option is new in this release, and is covered next.

The runner your validation scripts call was retired

If a board’s validation job invokes runltp, it has been broken since May and the failure mode is quiet. The script is still present and still executable at tag 20260930. It is 240 bytes, down from 36,394 bytes at the January release, and this is its entire body after the licence header:

cat >&2 << EOF
runltp was removed from LTP use kirk instead
https://github.com/linux-test-project/kirk
https://kirk.readthedocs.io/
EOF

exit 1

Read what that does to a harness. There is no command not found, because the file exists. The explanation goes to standard error, which a job capturing only standard output discards. The exit status is 1, so a wrapper ending in || true records a successful step with zero tests executed. The C program behind it, pan/ltp-pan.c, was deleted outright in the same release, and September removed three further scripts that depended on it.

The replacement is kirk, which the LTP release notes put at version 4.2.0. It reads the same runtest/ files, so the suite names do not change:

raghu@techveda.org:~$ kirk --run-suite cve --json-report /var/log/ltp-cve.json
raghu@techveda.org:~$ kirk --run-suite cve --skip-tests 'ghostlock|fsconfig03'
raghu@techveda.org:~$ kirk --run-suite cve --dry-run

Two options are genuinely new at this version. --dry-run, documented as “Performs a dry run listing tests (no execution)”, is the one to run first, because it lists what the suite would run without asking the board to survive anything. --fault-interval is the other. Note that --fault-injection, the probability option it accompanies, is not new despite how the announcement reads; it has been present since at least version 3.2.1.

Kirk reads suites from $LTPROOT/runtest/, with LTPROOT defaulting to /opt/ltp, which is also LTP’s install prefix, so a standard make install needs no environment set. A missing directory produces an error naming it, not an empty suite.

The timeouts that misfire on a slow target

Two kirk defaults deserve attention before the first run on hardware.

LTP scales individual test timeouts by LTP_TIMEOUT_MUL, the usual remedy for a target slower than a developer machine. Less obviously, kirk sets that variable for you when you have not, deriving it from the per-test timeout:

multiplier = os.environ.get("LTP_TIMEOUT_MUL")
if multiplier:
    self._env["LTP_TIMEOUT_MUL"] = multiplier
elif timeout:
    self._env["LTP_TIMEOUT_MUL"] = str((timeout * 0.9) / 300.0)

With the default --exec-timeout of one hour that gives 10.8, which is generous and fine. But lowering --exec-timeout to stop one stuck test holding a board also shrinks every test’s own timeout: ten minutes derives 1.8, and five minutes derives a multiplier below 1, shorter than upstream intended on the slowest machine in your fleet. The spurious timeouts that follow look like kernel bugs. Setting LTP_TIMEOUT_MUL yourself skips the derivation.

The second default is --suite-timeout, also one hour. Ghostlock alone declares .runtime = 180, three minutes, and there are 104 entries. Workers default to 1, so the suite runs serially, and serial is the right choice here given the deliberate leaks. One hour for the whole CVE suite on an embedded target is optimistic. Raise --suite-timeout rather than discovering mid-run that the suite was cut short, which in a JSON report is hard to distinguish from a suite that finished.

Proving the fix survived your vendor’s fork

It would be convenient for this argument if the GhostLock fix were missing from the longterm series. It is not. remove_waiter() in kernel/locking/rtmutex.c is 56 lines and byte-identical at v6.1.189, v6.6.158, v6.12.112 and v6.18.55: it already takes a local waiter_task = waiter->task and already passes that to rt_mutex_adjust_prio_chain() rather than current. v7.2.9 differs by one annotation line, and v5.15.222 and v5.10.271 carry the same local variable in an older-shaped function. All six longterm series have the fix.

Which is the point. The upstream question is settled, so LTP CVE tests run against an upstream tag tell you nothing new. What is not settled is whether the tree you build from still carries the fix. A vendor BSP reporting 6.6.158 may have reverted a locking change for a scheduling patch of its own, forked before the backport, or carried a private rtmutex patch for a real-time requirement. None of that shows in the version string.

Asking git whether the fix commit is an ancestor of your head is the cheap check, and the earlier piece linked above covers its limits. This is the one that depends on nobody’s claim:

raghu@techveda.org:~$ kirk --run-suite cve --run-pattern ghostlock --json-report /var/log/ghostlock.json

A pass here is evidence about the running binary, which is the kind of statement worth putting in a release record.

Common wrong turns

Reading a zero-failure report as a zero-risk result. This is attractive because it is how every other test suite behaves, and because the number really is zero. The incomplete part of the model is that LTP CVE tests have three outcomes, not two, and the third is the common one on embedded configurations. Count passes, not failures.

Running the CVE suite inside the normal CI pool. It looks like just another suite, it takes a suite name like any other, and most of its entries behave. The tests that crash the kernel on purpose will take a shared runner down with them, and the leak group will exhaust a small board. These belong on a target you can power-cycle.

Lowering --exec-timeout to stop a run from hanging. Entirely reasonable as a response, and it does bound the run. It also rescales every individual test’s timeout downward, because the multiplier is derived from it, so the cure produces timeout failures that look like driver problems. Pin LTP_TIMEOUT_MUL first.

Treating runtest/cve as the full extent of LTP’s CVE coverage. The file is explicitly organised by CVE number, so it reads as an index. It lags: fanotify25 existed for a release before its line appeared. Search the .tags entries in the test sources for a CVE you care about before concluding LTP has no reproducer for it.

Key takeaways

  • runtest/cve at LTP 20260930 maps 104 CVE numbers to reproducers, seven of those entries added in this release, and the oldest dates to 2011.
  • The verdict of an LTP CVE test is about the running kernel, which is a stronger statement than any comparison of version strings, and the GhostLock fix being present in all six longterm series is what makes it useful: the open question is whether your fork still has it.
  • Reproducers declare config prerequisites and skip when they are unmet. CONFIG_CHECKPOINT_RESTORE is default n and absent from the arm64 defconfig, so the ghostlock test skips on a stock build and the report still reads clean.
  • Several of these tests crash a vulnerable kernel deliberately, and others check the taint mask for a warning, so plan for a captured console and a power-cycle rather than a shared runner.
  • runltp is a 240-byte stub that writes to standard error and exits 1. A harness that ignores standard error sees a failed step with no output, and one ending in || true sees a pass with nothing run.
  • Kirk derives LTP_TIMEOUT_MUL from --exec-timeout when you do not set it, so tightening that option silently shortens every test’s own timeout.

A checklist for running LTP CVE tests

  1. kirk --run-suite cve --dry-run has been run on the target and its output compared against runtest/cve, so you know the suite was found.
  2. The results record counts passes and skips separately from failures, and the gate reads the pass count.
  3. For each CVE you intend to claim coverage of, the config symbols in that test’s .needs_kconfigs are set in the kernel you shipped, checked against /proc/config.gz on the device.
  4. LTP_TIMEOUT_MUL is set explicitly for the slowest target in the fleet, rather than left to kirk’s derivation.
  5. --suite-timeout is larger than the summed .runtime of the suite on your slowest board, and a truncated run is distinguishable from a completed one in the report.
  6. The twelve entries below the memory-leak comment run last or on a dedicated boot, and the board is rebooted after any test that taints the kernel.
  7. Nothing in the pipeline calls runltp, and no step hides a non-zero exit behind || true.

Teams building a defensible validation process around a shipped device can work through this material as structured training in TECH VEDA’s Embedded Linux BSP Development course.

Was this worth your time?

Frequently asked questions

Will the CVE suite crash my board?
On a fixed kernel, no. Several entries are written to crash a vulnerable kernel on purpose, and the ghostlock source says so in its header, so run the suite on a target you can power-cycle with the serial console captured to a file.

Why did a CVE test report no failure when I know the kernel is old?
The most likely reason is that it skipped. LTP CVE tests declare required kernel config symbols and skip when they are absent, and a skip is not a failure. Check the test’s .needs_kconfigs against the config of the kernel that booted.

My validation job still calls runltp. What happens now?
It fails without running anything. Since the May 2026 release runltp is a stub that prints a pointer to kirk on standard error and exits 1, so a job that captures only standard output sees an empty failure. Replace it with kirk --run-suite.

Does a pass prove my product is not vulnerable?
It proves one reproducer could not trigger that defect on the kernel you ran: stronger than a version comparison, narrower than a security assessment. Vendor code with no upstream equivalent has no CVE and no LTP test.

Further reading

RB
Raghu Bharadwaj

Founder, TECH VEDA — 20+ years teaching the Linux kernel, device drivers and embedded systems.

Follow on LinkedIn

Get new posts by email

Kernel, embedded Linux and AI-era engineering — a few sharp reads a month. No spam.

We email occasionally and never share your address.