# GhostLock Tracking — Claude Code Context

> **RETIRED 2026-08-06.**  Tracking has concluded: every row in the
> Patch status table is Fixed, the page carries a closing note and is
> frozen, and the auto-update timer, worktree, and `auto-update` branch
> have been unwired.  Do **not** resume polling sources, refreshing
> *Current kernel* cells, or re-enabling the timer.  The update-workflow
> sections below are kept as a record of how the tracker was maintained.

This repository contains a living tracking document for **GhostLock**
(**CVE-2026-43499**), a stack use-after-free in the Linux kernel's
real-time mutex (rtmutex) priority-inheritance code, reached through the
futex requeue-PI path.  `remove_waiter()` in `kernel/locking/rtmutex.c`
was written for the case where a task cleans up its *own* blocked state;
the futex requeue-PI machinery reuses it on behalf of a *different* task,
so it clears `pi_blocked_on` on the requeuer instead of the real waiter,
leaving a dangling pointer into freed kernel-stack memory — a **stack
use-after-free** the PoC turns into a near-arbitrary write and, from
there, local root and container escape.

The canonical fix is [`3bfdc63936dd`][fix] (*rtmutex: Use waiter::task
instead of current in remove_waiter()*), first released in **v7.1**.  The
bug was introduced by `8161239a8bcc` (*rtmutex: Simplify PI algorithm and
make highest prio task get lock*) in **v2.6.39** (2011), so the reachable
window is essentially **every kernel from 2.6.39 through 7.0** without the
backport — more than fifteen years.

**CVE-2026-43499** is assigned.  Discovered by **Nebula Security** (via
their "VEGA" tool) and disclosed publicly on **2026-07-07**.  PoC: the
"CyberMeowfia" project.  Google's kernelCTF awarded the submission
$92,337.

GhostLock is **architecture-independent** — rtmutex and the futex
requeue-PI path are generic kernel code, so every CPU architecture is
affected.  There is **no companion tracker**.

The rendered site is published at <https://kimmo.cloud/ghostlock/>.

[fix]: https://github.com/torvalds/linux/commit/3bfdc63936dd4773109b7b8c280c0f3b5ae7d349

## Your task

Keep `site/content/_index.md` (the canonical tracker) up to date as the
kernel fix is picked up by distro kernels.  After edits, rebuild with
`make build` and publish with `make dist`.

A scheduled background agent runs against this repo to refresh the tracker
on its own.  If you find the file has been edited since you last looked,
that's likely why — re-read before assuming stale state.

## Repo layout

```
.
├── site/                                     # Hugo project
│   ├── content/_index.md                     # the tracker — single source of truth
│   ├── hugo.toml                             # config (subpath baseURL — don't break)
│   ├── assets/css/extended/custom.css        # CSS overrides (PaperMod extension point)
│   ├── layouts/partials/post_meta.html       # overrides PaperMod: adds labels + lastmod
│   └── go.mod, go.sum                        # Hugo Modules — pulls PaperMod theme
├── scripts/                                  # auto-update agent: prompt + driver
│   ├── auto-update                           # wrapper invoked by the systemd timer
│   └── auto-update-prompt.txt                # prompt fed to headless Claude
├── systemd/                                  # user-level timer + service units
│   ├── ghostlock-tracker-update.service      # runs scripts/auto-update
│   └── ghostlock-tracker-update.timer        # Tue+Fri mornings
├── flake.nix, .envrc                         # Nix dev shell: hugo + go + git
├── Makefile                                  # `make build`, `make dist`, `make banner`
├── LICENSE                                   # CC BY 4.0
├── README.md                                 # user-facing project README
├── WEBSITE.md                                # publication plan / decisions log
└── CLAUDE.md                                 # this file
```

## The tracker file (`site/content/_index.md`) — important constraints

- It has Hugo front-matter with these required fields: `title`,
  `description`, `layout: "single"`, `date` (published), `lastmod` (last
  updated).  Keep all five; the rendering depends on them.
- The H1 has been stripped — Hugo emits the title from front-matter via
  PaperMod's single-post layout.  Don't add an H1 back.
- The TOC is generated by PaperMod's auto-TOC (`ShowToc = true` +
  `UseHugoToc = true` in `hugo.toml`).  Don't add a manual TOC.
- The "Last updated" date lives in the `lastmod` front-matter field.
- **One command per fenced code block, no inline comments.**  Each `bash`
  code fence holds a single command with nothing after it on the line, so
  the rendered copy button yields a clean, runnable command.  Put any
  clarifying note in the prose *before* the block — never as a trailing
  `# ...` comment or a second command in the same fence.

## Update workflow

1. Edit `site/content/_index.md` (or any file under `site/`).
2. Optional: `cd site && hugo server` for a local live preview at
   <http://localhost:1313/ghostlock/>.
3. `make build` — emits to `site/public/` (gitignored).
4. `make dist` — runs `make build`, then rsyncs `site/public/` to
   `haig:/ghostlock/` with `--delete`.

## What flips a verdict — the kernel backport, and only that

This bug lives entirely in the kernel, and the fix is a single commit.  A
row's verdict is one question:

- **Kernel** — is it inside the reachable window (2.6.39–7.0) *and* missing
  the [`3bfdc63936dd`][fix] backport?  A kernel `≥ 7.1` or carrying the
  backport is not exploitable.  This is what *Status* and *Fixed since* key
  on.  Because the bug dates to v2.6.39, **every** current distro kernel is
  in-window — there are no "predates the bug" rows.

There is **no reachability gate** to record as a second axis.  Unlike KVM
escapes there is no `/dev/kvm` node and no nested-virt knob; the trigger is
plain `futex(2)` (the requeue-PI operations `FUTEX_LOCK_PI`,
`FUTEX_WAIT_REQUEUE_PI`, `FUTEX_CMP_REQUEUE_PI`), reachable by **any**
unprivileged local task — including one inside an unprivileged container,
which is the container-escape path.  It does **not** require unprivileged
user namespaces, so `kernel.unprivileged_userns_clone=0` and similar
hardening do **not** block it.  There is therefore nothing to downgrade a
verdict to `:warning:` on reachability grounds: an in-window unpatched
kernel is `:x:`.

The combined *Patch status* table is the **single source** for every
row's kernel versions, dates, and status — upstream and distros alike.
Columns: `Distribution | Release | Current kernel | First fixed | Fixed
since | Status`.  The upstream kernel is the **first** "distribution" in
the table, labelled `Linux kernel`, one row per branch (`mainline`,
`7.1.x`, … `5.10.x`); its upstream-specific prose lives in the
`### Linux kernel` subsection.  Don't restate the table's columns in
prose or add a parallel per-release table.  The version cells hold
versions only (or `:grey_question:` when unverified) — the verdict lives
in *Status* as the emoji **plus a one-word verdict** and an optional
short note after an em dash (`:white_check_mark: Fixed —
RLSA-2026:38492`, `:x: Vulnerable — no cherry-pick`); longer caveats go
in the `###` prose.  Label NixOS channels in the **Release** column in
friendly form (`Unstable`, `26.05`).  Label opt-in/alternate kernel
rows by their kernel *series*, uniformly `<release> (<series> opt-in)`
— `11 (6.1 opt-in)`, `2023 (6.12 opt-in)` — and introduce the
underlying package name (`linux-6.1`, `kernel6.12`) in the `###`
prose, never in the Release cell.  Row and list ordering: releases
**descending** within a distribution; within a release the default
kernel row first, then live opt-in/alternate series rows **ascending**,
then superseded (`old`) series rows **descending** — like releases,
old series are something users are expected to move up from.  The
same rule applies to version lists in prose.  Keep the `{.distros}`
block attribute on the line **immediately after** the table (no blank
line between).

Amazon rows are **one per AL2023 kernel stream** — label the default
stream's row `2023 (default)` (it is the plain `kernel` package,
currently 6.1 — named in the `### Amazon Linux` prose, not the table)
and each opt-in stream by its series (`2023 (6.12 opt-in)` — the
`kernel6.12` package, named in the prose); keep every supported stream
(default + opt-in) as its own row, and add a row when Amazon ships a
new stream.  **AL2 has no rows**: it reached end of support on
2026-06-30 — before this tracker existed — with no ALAS for this CVE,
so it is covered collectively in the `### Amazon Linux` prose (all
three streams vulnerable, no fix expected); don't poll AL2 repodata and
don't re-add AL2 rows.  AL2023 remains supported and is polled as
usual.

Debian suites get one row for the **default** `linux` kernel and, where
one exists, a separate row per opt-in alternative kernel package (e.g.
bullseye's `linux-6.1` source package, the bookworm 6.1 kernel rebuilt
for bullseye — row `11 (6.1 opt-in)`).  A fixed opt-in row never flips
the default row's verdict: while `src:linux` is open in the security
tracker the default row stays `:x:`.  The same default-plus-variant
row pattern
applies to Proxmox (`proxmox-kernel-*` series), with two wrinkles: a
default row is labelled plain `9 (default)` — its series is visible in
*Current kernel* and named in the prose — and there is a third Release
label: PVE opt-in kernels are previews of the next default (per the
Proxmox forum announcements), so series get superseded — a former
default or an opt-in overtaken by a newer one is labelled `old`
(`9 (6.17 old)`).  Keep an `old` row (hosts still run it), but expect
no more updates for it: Proxmox discontinues updates for superseded
series after a short transition tail, and every such series is EOL on
kernel.org, so a vulnerable `old` row will likely never flip.
However, a release, stream, or kernel series that was already **dead
before the tracker existed** — its updates ended before the
disclosure — gets **no** row, anywhere in the table, if it died
*without* the fix (the AL2 treatment): its permanent `:x:` verdict is
one sentence in the relevant `###` prose.  A series that died already
*fixed* keeps its row — the upstream `7.0.x` row (fixed in 7.0.4,
EOL 2026-06-27 at 7.0.14) stays as that exception.  This is why the
6.14 series (superseded as PVE 9's default 2025-11-11; last builds on
`trixie-6.14` and `bookworm-6.14`, including PVE 8's opt-in rebuild,
dated 2026-05-15) and PVE 8's 6.11 opt-in (last built 2025-03-16)
have no rows — don't re-add them.  Don't
add upstream `Linux kernel` rows for series that appear in the table
only because PVE ships them (6.8, 6.11, 6.14, 6.17) — they are
EOL upstream without the fix, so such rows could never flip, and they
would sit misleadingly against PVE rows whose fixes arrive as
cherry-picks; the upstream-EOL fact is one sentence in the
`### Proxmox VE` prose.  The
EL `kernel-rt` variant is deliberately **not** a row — it is niche;
track it as a reader-facing caveat in the `### Rocky Linux / RHEL
family` prose only.

A per-distro `###` section is for **reader-facing** caveats that don't fit
the table (EL-family scope, a distro's own advisory state).  Keep tracking
methodology out of it — that is agent guidance and belongs in this file.

## Routine run scope — live Current kernel, sticky verdict columns

The **Current kernel** column is **live**: refresh it for **every** row
on every run — upstream point releases and distro package versions
alike — and record any movement.  A Current-kernel bump alone is a real
content change: commit it and bump `lastmod`.  The **verdict columns
are sticky**: *First fixed*, *Fixed since*, and *Status* change **only**
when a row actually flips — its kernel reaches a fixed upstream release
(see the `Linux kernel` rows), **or** the distro ships the
`3bfdc63936dd` backport / cherry-pick.  A Current-kernel bump that stays
inside the vulnerable window without the backport moves the *Current
kernel* cell and **nothing else**.

**A default-kernel-series switch is always recordable.** PVE moves its
default series during a release's lifetime (`proxmox-default-kernel`
changing which `proxmox-kernel-*` it depends on), and a distro can add
an opt-in series alongside it.  Record a switch in the prose and the
rows: the default row keeps its plain `(default)` label while its
*Current kernel* moves to the new series, the superseded series gets an
`old` row of its own (re-sort: old rows follow the live opt-ins,
descending), a new opt-in series gets a **new row**, and update the
verification log — **together**, since a log-only update
leaves the tracker self-inconsistent.  A switch can also flip a verdict
on its own — a newer series may already contain the fix, or a still-EOL
one may not — so re-derive the verdict rather than carrying the old one
across.

Each run:

- Refresh the `Linux kernel` rows' *Current kernel* from the stable
  point releases (finger_banner; verify backports via
  `~/src/linux/stable`, recipe below).  Every maintained line carries
  the fix — 7.1 / 7.0.4 / 6.18.27 / 6.12.86 / 6.6.140 / 6.1.175 and,
  since 2026-07-24, 5.15.212 / 5.10.261, per the `vulns.git` `.dyad` —
  so their *First fixed* / *Fixed since* / *Status* should never change
  again.
- For a distro row, re-pull the distro's **kernel** version and update
  *Current kernel*.  If the kernel reaches its branch's first-fixed
  release **or** a distro advisory ships the `3bfdc63936dd` backport ⇒
  flip *Status* to `:white_check_mark: Fixed`, set *First fixed* to the
  first fixed package build, and set *Fixed since*.
- Watch AlmaLinux (leading indicator) and Rocky/RHEL for the EL rows,
  and the vendor kernel changelog for the Proxmox rows.  For the
  prose-only `kernel-rt` caveat, the RHEL `kernel-rt` `fix_state` in
  the Red Hat security data API is the only signal to check.

`zcat` / `gunzip` **are** in the headless allowlist — use them for the
`Packages.gz` / repodata pulls.  Pull only kernel versions and advisory
state — the tracker records no other per-distro facts.

**Never record NixOS channel git-revisions** (the
`channels.nixos.org/<channel>/git-revision` pins) in the tracker.  They
advance on nearly every run — recording them manufactures a diff on an
otherwise no-op run.

## Conventions for status entries

A *Status* cell is the emoji plus its one-word verdict, optionally
followed by an em dash and a short note (advisory ID, `LTS`, `no
cherry-pick`) — longer caveats go in the `###` prose:

- `:white_check_mark: Fixed` — the release's kernel carries the
  `3bfdc63936dd` backport (confirmed in changelog / advisory / kernel
  pin, not merely announced): set *First fixed* to the first fixed
  build and set *Fixed since*.  (For this bug there are **no** "not
  affected — outside the window" rows; every maintained kernel is
  in-window.)
- `:x: Vulnerable` — an in-window kernel without the backport.
- `:warning: Staged` — not fully resolved: the fix is staged but not
  yet in the user-facing channel (merged / cherry-picked but not in a
  released package).  GhostLock has **no** mitigation that reduces
  exposure short of patching (see below), so `:warning:` is only ever
  the staged-fix case, never a "mitigated" one.
- `:grey_question: Unverified` — not yet verified (kernel pin or
  advisory not yet inspected).

Note: GhostLock has **no** root-vs-DoS downgrade and **no** reachability
discriminator — the single outcome is local privilege escalation /
container escape, a full compromise.  An affected unpatched row is `:x:`.

### "First fixed" and "Fixed since" columns

Both are **sticky**, set when a row flips to fixed, and stay `—` while
the row is vulnerable or unverified.  *First fixed* is the first
release or package build carrying the fix — from the `.dyad` for the
`Linux kernel` rows, from the advisory / changelog for distro rows.
*Fixed since* is the **first-observation** date the fix first held.
Only touch either if the verdict flips again or the recorded
first-fixed build turns out to have been wrong.

### Verification log

The section has a fixed shape: a short reader-facing intro paragraph
stays **visible**, and the log body is collapsed inside a
`{{< details summary="Full verification log" >}} … {{< /details >}}`
shortcode wrapper (rendered by
`site/layouts/shortcodes/details.html`).  Keep the intro and the
wrapper intact.  Inside the wrapper the
subsections are `####` headings (h4 — deliberately below the ToC's
`endLevel`, so the collapsed content has no ToC anchors pointing into
it).

Log entries are **one top-level bullet per source or topic**, opening
with a terse bold lead and the method attribution (e.g. `**Debian**
(via the security-tracker JSON, …):`), followed by **one fact per
nested sub-bullet** — never run multiple facts together into a
paragraph-bullet; the nesting is what keeps the log readable.
Sub-bullets follow the table's ordering conventions (releases
descending; within a release the default kernel first, live opt-in
series ascending, then old series descending).

When you re-verify entries, update the section rather than appending a
line per re-check.  Edit the relevant `#### Upstream` /
`#### Distributions` subsection in place — usually the one sub-bullet
whose fact changed.  Add a new `####` subsection only when a genuinely
new topic appears.  The log carries no dates of its own — the
front-matter `lastmod` is the document's only recency marker, so do
not write verification dates inline.  Method/source
attribution *without* a date is fine (e.g. `(via …/madison)`,
`(checked against ~/src/linux/stable)`).

The log records **facts**, not run outcomes.  Never write "no change",
"unchanged from prior run", "no verdict changes this run", or similar
prose anywhere in the tracker — an entry that is still accurate reports
that by staying untouched.  This applies just as much on a run that
*does* change something real: update only the lines whose facts changed
and leave every other line exactly as it was.

### Date handling — first-seen / last-changed, not "today"

Dates in the prose (`lastmod`, every "as of <date>" / "released
<date>" / "Fixed since" value) are **first-seen / last-changed**
dates, not "today" dates.  Only move a date when the fact it
qualifies actually changes.  If the entire run is a no-op (no status,
kernel version, advisory, or upstream-backport change), leave the file
alone and don't commit at all — don't bump `lastmod`, don't insert
"re-confirmed <today>" parentheticals.

## Build environment

- Hugo extended **≥ 0.146.0** (PaperMod's minimum).  Debian apt is too old.
- Go (any recent version) — needed for Hugo Modules to pull PaperMod.
- The Nix flake provides both: `nix develop` (or just `cd` in if direnv is
  set up).
- On this host `nix` is **not** available (Debian, no nixpkgs installed) —
  don't try `nix develop`.  Hugo ≥ 0.146 is at `/usr/local/bin/hugo`, so
  plain `make build` / `make dist` work directly.

## Auto-update worktree

The auto-update job works in a dedicated git worktree
at `~/src/auto-update/ghostlock`, checked out on a single long-lived branch
named `auto-update`.  The wrapper merges `origin/main` forward into that
branch on each run, then hands off to headless Claude, which commits any
tracker changes back onto `auto-update` only.  The agent must not create
per-run branches, switch branches, push, or open PRs — merges of
`auto-update` into `main` are done manually by the user.

One-time setup (from the primary checkout at `~/src/ghostlock`):

```
git worktree add -b auto-update ~/src/auto-update/ghostlock main
```

The wrapper runs `git fetch origin` and `git merge origin/main` under
`set -e`, so the repo needs a GitHub `origin` with `main` pushed or every
scheduled run aborts before doing anything.  A freshly `git init`'d tracker
must `git remote add origin https://github.com/suominen/ghostlock.git` and
`git push -u origin main` before the timer is worth enabling.

**The timer runs the wrapper from the primary checkout, not from the
worktree.**  `ExecStart` points at `<primary>/scripts/auto-update`, and the
wrapper reads its prompt from there too, so what executes is always code you
have reviewed and merged to `main`.  The agent can commit to `auto-update`,
so anything under `scripts/`, `.claude/`, `CLAUDE.md`, or `.mcp.json` on
that branch is untrusted: after merging `origin/main` forward the wrapper
refuses to run if any of them differs from `origin/main`, and it refuses
outright if it was invoked from inside the worktree at all.  A wrapper
change therefore takes effect on the next run after you merge it to `main`,
with no re-exec dance.

The service unit adds a kernel-level backstop — `ProtectSystem=strict` with
a short `ReadWritePaths` list (the worktree, the `.git` directories git has
to update, and `~/.claude/session-env`, which Claude Code writes on every
Bash call).  Without it the guards above are advisory: the agent may run
`curl`, and `curl -o` writes anywhere this user can, including over the
wrapper the timer runs.  Each repository's `.git/hooks` and `.git/config`
are handed back as `ReadOnlyPaths`, since nothing in a run needs to write
either and both are executable code.

A refusal is a stop-and-look rather than something to clear reflexively —
it means a file that should only ever arrive by merge was rewritten on the
branch.  Inspect it with `git -C ~/src/auto-update/<slug> diff origin/main
-- scripts .claude CLAUDE.md .mcp.json` before doing anything else.

To run a refresh immediately (same path the timer takes):

```
systemctl --user start ghostlock-tracker-update.service
```

It is a `oneshot`, so the command blocks until the run finishes; follow its
output with `journalctl --user -u ghostlock-tracker-update`.  Run the
trackers **one at a time**, never in parallel — they share the
`~/src/linux/*` reference clones.

The systemd units ship in `systemd/`.  They are not in a standard unit
search path, so wiring the timer means symlinking both units into
`~/.config/systemd/user/` and then enabling the timer.  Use `ln -sr` so the
links are relative:

```
ln -sr ~/src/ghostlock/systemd/ghostlock-tracker-update.service \
       ~/src/ghostlock/systemd/ghostlock-tracker-update.timer \
       ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now ghostlock-tracker-update.timer
```

The timer fires Tuesday and Friday at `05:20` — staggered from the sibling
trackers (ipv6_frag_escape `:05`, januscape `:50`, itscape `06,18:05`) so
the shared kernel clones are not fetched simultaneously.  Verify the live
set with `systemctl --user list-timers | grep tracker` — this in-doc list
has gone stale before.

## Tearing down the auto-update

To stop the scheduled refresh, unwire it in this order — the sequence
matters, because `systemctl disable` needs the unit definition to still be
resolvable when it runs.

1. **Disable the timer first**, while the unit symlinks are still in place:

   ```
   systemctl --user disable --now ghostlock-tracker-update.timer
   ```

2. **Remove the unit-definition symlinks** from the search path:

   ```
   rm ~/.config/systemd/user/ghostlock-tracker-update.timer \
      ~/.config/systemd/user/ghostlock-tracker-update.service
   ```

3. **Reload** so the running user manager drops the units:

   ```
   systemctl --user daemon-reload
   ```

If the definition symlinks were removed *before* disabling, the stale
`enable` symlink is left behind, parking the timer in a `failed` state.
Recover by deleting it directly, then reloading and clearing the failure:

```
rm ~/.config/systemd/user/timers.target.wants/ghostlock-tracker-update.timer
systemctl --user daemon-reload
systemctl --user reset-failed ghostlock-tracker-update.timer
```

Finally, remove the worktree and its branch (run from `~/src/ghostlock`):

```
git worktree remove ~/src/auto-update/ghostlock
git branch -d auto-update
```

## Local reference clones

git.kernel.org's cgit HTML pages (any URL ending in /log/, /tree/,
/commit/, etc.) and lore.kernel.org (the mailing-list archive and its
search) are Anubis-gated; WebFetch hits the no-JS challenge and the
auto-update agent cannot read them.  The agent inspects kernel history via
long-living local clones under `~/src/linux/`:

| Clone path           | Upstream                                                           |
|----------------------|--------------------------------------------------------------------|
| `~/src/linux/stable` | `https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git` |
| `~/src/linux/vulns`  | `https://git.kernel.org/pub/scm/linux/security/vulns.git`          |
| `~/src/proxmox/pve-kernel` | `https://git.proxmox.com/git/pve-kernel.git`                 |

GhostLock is a locking/rtmutex fix that flows straight into the stable
tree, so there is **no dedicated subsystem clone** here (unlike the KVM
siblings, which track `~/src/linux/kvm`).  `stable` plus `vulns` suffice.
`vulns` is the kernel CNA's CVE database.

The auto-update *wrapper* refreshes the clones with `git -C <clone> fetch`
before invoking the headless agent; the agent inspects via the
`origin/<branch>` remote-tracking refs (e.g. `origin/linux-6.12.y`).

**Is a stable branch fixed yet?**  The backport is a cherry-pick with a new
SHA that cites the upstream commit, so search by the upstream reference /
subject, not `git tag --contains`:

```
git -C ~/src/linux/stable log v<series>..origin/linux-<series>.y --grep=3bfdc63936dd --grep='waiter::task' --format='%h %s'
```

Empty output ⇒ the series is still unpatched.  Keep the range bounded to
`v<series>..` — an unbounded subject grep over the whole branch history can
match an ancient unrelated commit and read as a false "fixed".  **Check the
subject of every hit**: the grep also matches backports of the *follow-up*
hardening commit `40a25d59e85b` (*locking/rtmutex: Skip remove_waiter()
when waiter is not enqueued*, v7.1-rc7), which cites `3bfdc63936dd` in its
body.  Only a hit with the original subject (*rtmutex: Use waiter::task
instead of current in remove_waiter()*) is the CVE fix — reading the
follow-up as the fix is exactly how the seed recorded first-fixed versions
one-or-more point releases too late (6.1.177 instead of 6.1.175, etc.).
Prefer the `.dyad` (below) when it covers the branch.  Confirm the fix
landed mainline in v7.1 with:

```
git -C ~/src/linux/stable describe --contains 3bfdc63936dd
```

The git smart-HTTP protocol is not Anubis-gated, so `git fetch` /
`git ls-remote` work from any UA.

## The CVE record (`vulns.git`)

**CVE-2026-43499** is assigned.  The kernel CNA's `vulns.git` record was
published shortly after seed (2026-07-09) and is the **authoritative**
source for per-branch first-fixed versions: 6.1.175 / 6.6.140 / 6.12.86 /
6.18.27 / 7.0.4 / 7.1.  Re-read it each run in case the CNA revises it
(e.g. a 5.15.y/5.10.y backport appearing) — `vulns.git` keys each record on
the *fixing commit SHA* and is inspected via `origin/master`, not `HEAD`
(the wrapper only `git fetch`es):

```
git -C ~/src/linux/vulns show origin/master:cve/published/2026/CVE-2026-43499.dyad
```

Once it exists, the `.dyad` gives the authoritative per-branch
`<introduced>:<fixed>` versions — reconcile the table against it — and the
`.json` carries the CNA description.  A distro or MITRE CVSS could appear in
NVD later — watch the disclosure sources for a KEV/EPSS/CVSS update to add
to the Summary.  The repo/site slug stays `ghostlock` (see `WEBSITE.md`).

## Reachability — no discriminator to record

GhostLock needs no special device, capability, or namespace.  The
requeue-PI futex operations are available to every task, so any
unprivileged local user — or an unprivileged process inside a container —
can reach the bug on an unpatched kernel.  There is nothing per-host to
re-derive (no `/dev/kvm` mode, no nested-virt flag); do **not** invent a
reachability column or a per-distro exposure note along those lines.  The
only per-host fact that matters is the running kernel version, which the
table already carries.

## Local nixpkgs clone for NixOS channel verification

*Seeding-and-adoption method — see "Routine run scope" above.  Do not
record the resolved channel revisions in the tracker.*

NixOS rows are verified from a local nixpkgs clone at `~/src/nixos/nixpkgs`,
not the (JS-rendered, lagging) security-tracker page.  Each channel has a
git-revision pointer at `https://channels.nixos.org/<channel>/git-revision`;
at that commit, `pkgs/os-specific/linux/kernel/kernels-org.json` carries
the version for every supported mainline.org kernel series.  The **default**
`linuxPackages` is set by `packageAliases.linux_default` in
`pkgs/top-level/linux-kernels.nix`.  Read that alias rather than assuming
the default is the newest or oldest LTS — it has moved before.

Tracked channels: `nixos-26.05` (default), `nixos-unstable`.

```
rev=$(curl -fsSL https://channels.nixos.org/<channel>/git-revision)
git -C ~/src/nixos/nixpkgs show "${rev}:pkgs/os-specific/linux/kernel/kernels-org.json"
```

`channels.nixos.org/<channel>/git-revision` returns a 302 — always pass
`-L` to curl.  The wrapper refreshes the clone on every run.

## Proxmox kernel version source

*Seeding-and-adoption method — see "Routine run scope" above.  The
`Packages.gz` index is gzipped; `zcat` is in the headless allowlist.*

Proxmox VE is a common container/VM host, so the container-escape angle
makes it worth a row.  It ships its **own** Ubuntu-derived kernel
(`proxmox-kernel-*`) with a Debian userland, so the Debian madison feed
does not cover it.  Pull the kernel version from the `pve-no-subscription`
`Packages` index.  VE 9 is trixie-based, VE 8 bookworm-based:

```
url=http://download.proxmox.com/debian/pve/dists/<trixie|bookworm>/pve-no-subscription/binary-amd64/Packages.gz
curl -fsSL "$url" | zcat | grep -A3 '^Package: proxmox-default-kernel'
```

The default kernel *series* is whatever the highest-versioned
`proxmox-default-kernel` meta-package depends on — check it each time, it
moves.

**Two sources — only one is authoritative for the version.** The
*Current kernel* column is the `proxmox-kernel-<series>` build published
in `pve-no-subscription` (read it from the same Packages.gz — the
per-series package entries, not just the `proxmox-default-kernel` meta;
take the highest `rel`).  That is the build a host actually installs.
The pve-kernel **git changelog leads apt** — a build is committed there
before it reaches any apt channel (git → pvetest → pve-no-subscription →
enterprise) — so **never copy a changelog version into *Current
kernel***; reading the version from git on one run and apt on the next
is exactly what makes the column bounce forth-and-back.  Use the git
changelog only to confirm a cherry-pick.  If it shows the fix cherry-pick
in a build `pve-no-subscription` has not yet published, the fix is
*staged*, not shipped: mark `:warning: Staged`, keep *Current kernel* at
the published version, and flip to `:white_check_mark: Fixed` only when
the fixed build appears in `pve-no-subscription`.

Whether a Proxmox kernel carries the fix tracks its Ubuntu series,
not Debian's.  **A named cherry-pick is not the only fix path.**  For
a series Ubuntu still maintains, the fix usually arrives silently
inside an `update sources to Ubuntu-<base>` rebase, with **no**
CVE-named changelog line — that is exactly how PVE 8's default 6.8
was fixed (`6.8.12-39` rebased onto noble's `6.8.0-136.136`, the
USN-8488-1 build), and grepping the changelog for the CVE alone read
it as "no backport" for a month.  On every run, for each live series,
take the newest `update sources to Ubuntu-*` base version from the
changelog and compare it against Ubuntu's fixed version for that
series in the Ubuntu CVE tracker
(`https://ubuntu.com/security/cves/CVE-2026-43499.json` — the
`packages[].statuses[]` entries; `released` + version).  Base ≥
Ubuntu's fixed version ⇒ the PVE build carries the fix.  Kernel.org
EOL for the series is irrelevant here — 6.8.y is long EOL upstream
while noble's 6.8 keeps getting fixes.  Named cherry-picks remain the
signal only for series Ubuntu no longer fixes (superseded/`old`
series, and opt-ins whose Ubuntu HWE source is EOL).  To confirm a
cherry-pick, read the packaging changelog in Proxmox's kernel git,
where Proxmox lists every security cherry-pick by name/CVE.  The
cgit HTML may be gated, so read it from the shared local clone at
`~/src/proxmox/pve-kernel` via its `origin/...` refs — the wrapper
refreshes it each run, alongside the other reference clones:

```
git -C ~/src/proxmox/pve-kernel show origin/master:debian/changelog
```

Branches are named `<debian-suite>-<series>` — `bookworm-6.8`,
`trixie-6.17` — except the newest series on the current suite, which lives
on `master`.  **Don't assume a mapping.** PVE switches its default kernel
series during a release's lifetime and adds opt-in series alongside it, so
resolve the series first (`proxmox-default-kernel` for the default, the
`proxmox-kernel-*` metapackages for opt-in), then pick the branch that
matches:

```
git -C ~/src/proxmox/pve-kernel branch -r
```

Confirm the branch is the one you meant by checking that its changelog head
names that series, then read `debian/changelog` and `patches/` from it.  A
series that PVE has moved off is still a live branch here, so reading a
stale one reports "no cherry-pick" from a kernel nobody runs.  For an
`old` row's series, refresh *Current kernel* from the same Packages.gz
pull as the rest, but only re-read its changelog branch if that package
version actually moved — Proxmox has stopped updating superseded
series, so on a routine run there is nothing new to find there.

Do not clone it per run: it is a shared reference like
`~/src/linux/stable`, and a clone made inside the worktree leaves untracked
debris that stalls every later run.

The enterprise repository (`enterprise.proxmox.com`) is HTTP-auth-gated
(401 without subscription credentials), so it cannot be polled headlessly.
Track `pve-no-subscription` only: packages flow pvetest →
pve-no-subscription → pve-enterprise, so no-subscription is strictly the
leading indicator and enterprise receives the same kernels later.

## Rocky / Amazon kernel version source (RPM repodata)

*Seeding-and-adoption method — see "Routine run scope" above.  These
indexes are gzipped; `zcat` is in the headless allowlist.*

**For the EL rows, the Red Hat security data API is the authoritative
leading signal** — RHEL is upstream of Rocky and AlmaLinux, so neither can
be fixed before RHEL is.  Read the per-CVE record's `package_state`
(per-product `fix_state`: Affected / Not affected / Will not fix / Out of
support scope) and `affected_release` (populated with the RHSA advisory ID
and the fixed kernel NVR once a fix ships):

```
curl -fsSL 'https://access.redhat.com/hydra/rest/securitydata/cve/CVE-2026-43499.json'
```

(If hydra ever 404s — it can lag a new record by days — fall back to
the CSAF/VEX record Red Hat publishes first,
`https://security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-43499.json`:
`product_status` carries the per-product verdicts, `flags` the
justification, and a shipped RHSA appears as a `vendor_fix`
remediation.  Never WebFetch the access.redhat.com CVE page — it is
JS-rendered and returns only the navigation shell headlessly, a false
"no record".)  While `fix_state` is **Affected** and
`affected_release` is empty, no EL fix has shipped → the Rocky rows stay
`:x:`.  When `affected_release` gains a RHEL `kernel` entry, that NVR is the
target; Rocky rebuilds it as an RLSA, with AlmaLinux the fastest rebuild —
cross-check OSV (`https://api.osv.dev/v1/vulns/CVE-2026-43499` gains an
`AlmaLinux` ecosystem entry when an ALSA ships).

The RPM repodata then confirms the Rocky ship and gives the current NVR
(gzipped; `zcat` is in the allowlist).  Pick the numerically-highest `rel`
(`sort -V`):

- **Rocky** BaseOS: `https://dl.rockylinux.org/pub/rocky/<8|9|10>/BaseOS/x86_64/os`.
  **All** of Rocky 8 (4.18), 9 (5.14), and 10 (6.12) are in-window for
  GhostLock (the bug predates them all).  A row flips only when the BaseOS
  kernel NVR reaches the RHEL fixed build from the Red Hat record above.
- **EL `kernel-rt`** is tracked in prose only (a caveat in the
  `### Rocky Linux / RHEL family` section — no table rows): read the
  separate `kernel-rt` `package_state` in the same Red Hat CVE record.
  While it reads **Affected** for RHEL 9 GA the caveat stands; there is
  no need to poll Rocky's RT (EL8) / NFV (EL9) repodata for it.
- **Amazon Linux**: the machine-readable ALAS signal is the repodata
  **`updateinfo.xml.gz`** (maps CVE → ALAS → fixed kernel NVR) — the per-CVE
  ALAS HTML pages (`alas.aws.amazon.com/cve/html/…`) are JS-rendered and
  return no data headlessly, so a "cross-check ALAS" that reads them will
  falsely see "no advisory" (this is exactly how the AL2023 row was seeded
  wrong).  Resolve the mirror, then fetch `<base>repodata/updateinfo.xml.gz`
  (plain filename, not hashed) and grep for the CVE; the `<update>` block
  names the ALAS id, date, and fixed `kernel*` package version.  Check
  **all** kernel streams, not just the default: AL2023 ships `kernel` (6.1),
  opt-in `kernel6.12`, and `kernel6.18` — a narrow-window bug can leave the
  default not-affected while an opt-in stream is affected (and separately
  fixed).  Read current versions from `primary.xml.gz`.  Mirror:
  `…/al2023/core/mirrors/latest/x86_64/mirror.list`.  (AL2 is EOL and
  untracked — no AL2 repodata pulls.)

## Debian kernel version source

**The security tracker, not a version comparison, is authoritative for
Debian status.**  Two traps make a naive madison base-version check wrong
(both bit the seed): the base suite lags the `-security` upload that ships
the fix, and Debian often **backports** the fix to a version *below* the
upstream first-fixed release — e.g. `bookworm-security 6.1.176-1` carries
GhostLock although upstream 6.1 was not fixed until 6.1.177.

Read status and fixed version straight from the security tracker.  WebFetch
the human-readable per-CVE page, or `curl` the JSON and grep the
`CVE-2026-43499` block for each release's `status` (resolved/open) and the
version under `repositories` (a `<suite>-security` entry means the fix
shipped as a security update):

```
curl -fsSL 'https://security-tracker.debian.org/tracker/data/json'
```

Use the dak madison API only for the base-suite version and the sid/testing
lineage (unstable=sid, testing=forky, stable=trixie, oldstable=bookworm,
oldoldstable=bullseye):

```
curl -fsSL 'https://api.ftp-master.debian.org/madison?package=linux&s=sid,forky,trixie,bookworm,bullseye&text=on'
```

For a *Fixed since* date, use the `first_seen` of the fixed version in
snapshot.debian.org:

```
curl -fsSL 'https://snapshot.debian.org/mr/package/linux/6.12.95-1/srcfiles?fileinfo=1'
```

## Key sources to monitor

| Source | URL |
|---|---|
| Disclosure writeup (Nebula Security) | <https://nebusec.ai/research/ionstack-part-2/> |
| Kernel fix | <https://github.com/torvalds/linux/commit/3bfdc63936dd4773109b7b8c280c0f3b5ae7d349> |
| Introducing commit | <https://github.com/torvalds/linux/commit/8161239a8bcce9ad6b537c04a1fa3b5c68bae693> |
| CVE record | <https://www.cve.org/CVERecord?id=CVE-2026-43499> |
| stable point release banner | <https://www.kernel.org/finger_banner> |
| Debian security tracker | <https://security-tracker.debian.org/tracker/CVE-2026-43499> |
| Debian package madison (dak-backed) | <https://api.ftp-master.debian.org/madison?package=linux&s=sid,forky,trixie,bookworm,bullseye&text=on> |
| AlmaLinux errata | <https://errata.almalinux.org/> |
| Amazon Linux ALAS | <https://alas.aws.amazon.com/> |

For machine-readable data, prefer API/feed endpoints over HTML pages —
several distro sites are JS-rendered SPAs that don't render via WebFetch.

## Platform-specific notes

- **Architecture-independent.**  rtmutex and futex requeue-PI are generic
  kernel code; x86, arm64, and every other port are affected alike.  There
  is no arch column and no "not affected — different arch" caveat.
- **No mitigation short of patching.**  The trigger is ordinary `futex(2)`;
  you cannot disable it, and it needs neither elevated privilege nor
  unprivileged user namespaces.  Do not present any sysctl or module knob
  as a mitigation — the only fix is the `3bfdc63936dd` backport.
- **Container hosts.**  The bug is reachable from inside an unprivileged
  container, so the container-escape vector puts any multi-tenant container
  host in scope until its host kernel is patched.
- **7.0.y got the fix before EOL.**  Unlike some backports, `7.0.4` carries
  it; `7.0.14` is EOL but still fixed.  5.15.y and 5.10.y are in-window and
  had **no** backport at seed — watch them.

## Known harmless warnings during build

PaperMod's templates still call `.Language.LanguageDirection` and
`.Language.LanguageCode`, which Hugo deprecated in 0.158.0.  The build emits
`WARN deprecated:` lines for both.  Upstream theme issue — don't try to fix
it in this repo.

## License

The tracker content is licensed under **CC BY 4.0** (see `LICENSE` at the
repo root).  Copyright © 2026 Kimmo Suominen.  The site footer credits both
the author and the licence.
