Safeguard
Vulnerability Management•

zvariant Is Unmaintained: Why cargo deny Fails and How to Move to zbus 6

RustSec marked zvariant, zvariant_derive and zbus_names unmaintained after zbus 6.0 merged them, and cargo-deny fails on that by default. How far it reaches (523 of 637 zbus dependents still on zbus 5), what each tool does, the three situations you can be in, the zbus 6 migration essentials, and a Cargo.lock checker.

Safeguard Security Team
Security Research
13 min read

Since 17 October 2026, cargo deny check has been failing for Rust projects that talk to D-Bus and run cargo-deny with its default settings. RustSec marked three crates as unmaintained after zbus 6.0 merged them into zbus itself: zvariant (RUSTSEC-2026-0370), zbus_names (RUSTSEC-2026-0371) and zvariant_derive (RUSTSEC-2026-0372). cargo-deny treats any unmaintained advisory as an error by default, and every zbus release before 6.0 depends on zvariant. Nothing here is a vulnerability.

This post is for Rust teams whose CI just went red on these advisories, and for anyone deciding how to handle "unmaintained" findings in general. It covers what changed, how far it reaches, what cargo-deny and cargo audit each do with it, the three situations you can be in, and a script that reads a Cargo.lock and tells you which one applies.

The short answer

zvariant, zvariant_derive and zbus_names now live inside zbus 6.0 (released 16 October 2026) as zbus::wire, the zbus derives and zbus::names, and the standalone crates will get no further releases. RustSec's informational advisories for them were merged on 17 October. Any project with zbus 1.x to 5.x anywhere in its dependency tree pulls in zvariant, so cargo-deny with default settings fails. If zbus comes in through another crate, add an ignore entry with a reason, or set unmaintained = "workspace", and wait for that crate to move to zbus 6. If you use zbus 5 yourself, plan the upgrade: it's a major release that removes the blocking API. If you depend on zvariant or zbus_names directly, switch to zbus 6 with default features off.

What changed

The zbus maintainers planned the merge in a design document dated 25 August 2026 and carried it out with the 6.0 release:

WasIn zbus 6.0
the zvariant cratecommon types at the zbus root, encoding and decoding in zbus::wire
the zvariant_derive cratethe derives, re-exported from zbus
the zbus_names cratethe zbus::names module
zvariant's GVariant supportthe separate zgvariant crate

On 16 October the old crates got final releases (zvariant 5.15.1, zvariant_derive 5.15.1, zbus_names 4.3.5) that mark them deprecated, with a README banner saying they receive no further releases. The maintainers then filed the three RustSec advisories themselves, and RustSec merged them on 17 October at 15:05 UTC. That's when CI runs that fetch the advisory database started to see them.

The advisories are informational, category "unmaintained", and list every version with no patched release, because there won't be one. The recommended replacement is zbus 6.

How far it reaches

Every zbus before 6.0 depends on zvariant. We checked one release from each major line on crates.io: 1.9.3, 2.3.2, 3.15.2, 4.4.0 and 5.19.0 all list it, and 2.x onward also list zbus_names. zbus 6.0.0 lists neither.

On 19 October, crates.io counted 637 crates that depend on zbus. Their latest releases split like this:

zbus requirement in the dependent's latest releaseCrates
5.x523
1.x to 4.x108
6.x6

So almost every crate that uses zbus, including widely used keyring, desktop notification, accessibility, desktop portal and Tauri plugin crates, still brings zvariant into your tree. Each of those needs its own release on zbus 6 before the finding goes away for its users, and zbus 6 is not a drop-in upgrade.

What each tool does with an unmaintained advisory

Tool and settingResult
cargo-deny, default (unmaintained = "all")Error for any crate in the tree. Fails cargo deny check
cargo-deny, unmaintained = "workspace"Error only if the crate is a direct dependency of a workspace member
cargo-deny, unmaintained = "none"Ignored
cargo audit, defaultWarning; exit status 0
cargo audit, --deny unmaintained or --deny warningsError

cargo-deny has failed on unmaintained advisories by default since 0.16.0 (August 2024), and 0.18.2 (March 2025) added a setting to limit that to direct dependencies. Our guide to cargo-deny's yanked and unmaintained settings covers those options in more detail. That's why this one shows up as a red build for cargo-deny users and a yellow line for most cargo audit users.

Which of three situations you're in

1. zbus comes in through another crate. You can't fix this in your own code. The finding clears when that crate releases a version on zbus 6 and you update to it. Until then, record the decision. In deny.toml:

[advisories]
ignore = [
  { id = "RUSTSEC-2026-0370", reason = "zvariant comes with zbus 5 via a dependency; informational, not a vulnerability" },
  { id = "RUSTSEC-2026-0371", reason = "zbus_names comes with zbus 5 via a dependency; informational, not a vulnerability" },
  { id = "RUSTSEC-2026-0372", reason = "zvariant_derive comes with zvariant; informational, not a vulnerability" },
]

Or set unmaintained = "workspace" so only crates you depend on directly can fail the build. That's a broader change, so make it on purpose, not just to get past this week.

2. You use zbus 5 yourself. Upgrading to zbus 6 removes all three findings, but it's a real migration. The upgrade chapter in the zbus book is the full list of breaking changes. The ones most teams will hit:

  • The blocking API is gone. zbus::blocking and the *ProxyBlocking types have no equivalent. Wrap the async API in zbus::block_on, ideally once around the whole program.
  • Value is now Variant, and encoding and decoding moved to zbus::wire. zbus::zvariant stays as a deprecated alias module until 7.0, so most code still compiles, with warnings.
  • Features changed. The D-Bus API sits behind a comms feature, async-io is replaced by a default-rt runtime, and several transports became opt-in.
  • The minimum Rust version is 1.89.

Here's the blocking change, from the upgrade chapter:

// zbus 5
let connection = zbus::blocking::Connection::session()?;
let proxy = FooProxyBlocking::new(&connection)?;
let answer = proxy.bar()?;

// zbus 6
let answer = zbus::block_on(async {
    let connection = zbus::Connection::session().await?;
    let proxy = FooProxy::new(&connection).await?;
    proxy.bar().await
})?;

3. You depend on zvariant or zbus_names directly. Depend on zbus 6 with default-features = false instead, keeping the same feature names except the removed gvariant, ostree-tests and enumflags2. Rename zvariant:: to zbus:: for common types and derives, zbus::wire:: for encoding and decoding, and zbus_names:: to zbus::names::. GVariant users move to zgvariant.

One trap applies to anyone who upgrades while a dependency is still on zbus 5. zvariant's Value and Signature types are unrelated to zbus 6's, so passing values between the two produces errors like "expected zbus::Variant, found zvariant::Value". cargo tree -i zvariant shows which crate still holds the old one.

Find out which situation applies

This script reads a Cargo.lock, finds each of the three crates, prints the shortest path from your workspace to it, says who has to act, and writes the matching deny.toml entries. It needs Python 3.11 or later for tomllib and nothing else:

#!/usr/bin/env python3
"""Explain the zvariant, zvariant_derive and zbus_names "unmaintained" findings
(RUSTSEC-2026-0370, -0372, -0371) in a Cargo.lock: who pulls each crate in,
whether you can drop it yourself, and what to write in deny.toml meanwhile.

Usage: python3 zvariant_check.py path/to/Cargo.lock"""
import collections
import sys
import tomllib

ADVISORIES = {"zvariant": "RUSTSEC-2026-0370", "zbus_names": "RUSTSEC-2026-0371",
              "zvariant_derive": "RUSTSEC-2026-0372"}


def load(path):
    with open(path, "rb") as f:
        pkgs = tomllib.load(f).get("package", [])
    by_name = collections.defaultdict(list)
    for p in pkgs:
        by_name[p["name"]].append(p)

    def key(p):
        return f'{p["name"]} {p["version"]}'

    graph = {}
    for p in pkgs:
        deps = []
        for d in p.get("dependencies", []):
            name, *rest = d.split()
            cands = by_name[name]
            match = [c for c in cands if not rest or c["version"] == rest[0]]
            deps += [key(c) for c in match[:1]]
        graph[key(p)] = deps
    members = [key(p) for p in pkgs if "source" not in p]
    return by_name, graph, members, key


def path_to(graph, members, target):
    """Shortest dependency path from a workspace member to target."""
    queue = collections.deque([[m] for m in members])
    seen = set(members)
    while queue:
        path = queue.popleft()
        if path[-1] == target:
            return path
        for nxt in graph.get(path[-1], []):
            if nxt not in seen:
                seen.add(nxt)
                queue.append(path + [nxt])
    return None


def main(lock):
    by_name, graph, members, key = load(lock)
    zbus = sorted({p["version"] for p in by_name["zbus"]})
    print(f"zbus versions in the lockfile: {', '.join(zbus) or 'none'}")
    ignores = []
    for crate, rid in ADVISORIES.items():
        for p in by_name.get(crate, []):
            path = path_to(graph, members, key(p))
            if not path:
                continue
            parent = path[-2].split()[0]
            direct = len(path) == 2
            print(f"{rid}: {key(p)}")
            print("  via " + " -> ".join(path))
            if direct:
                print("  direct dependency: replace it with zbus 6 (default-features = false) and "
                      + ("zbus::wire / the zbus root" if crate != "zbus_names" else "zbus::names"))
            elif parent in ADVISORIES:
                print(f"  comes with {parent}: goes away with it")
            else:
                owner = path[-3] if parent == "zbus" else path[-2]
                if owner in members:
                    print(f"  your crate {owner.split()[0]} uses {path[-2]}: upgrading it to zbus 6 removes this")
                else:
                    print(f"  {owner.split()[0]} holds it: needs a release of {owner.split()[0]} on zbus 6")
            ignores.append((rid, crate, parent, direct))
    if any(v.startswith("6.") for v in zbus) and by_name.get("zvariant"):
        print("warning: zbus 6 and zvariant are both in the tree; their Variant/Value and Signature "
              "types are unrelated, so values can't pass between the two crates' APIs")
    if ignores:
        print("\n# deny.toml, until the upgrade lands (cargo-deny fails on unmaintained by default)")
        print("[advisories]\nignore = [")
        done = set()
        for rid, crate, parent, direct in sorted(ignores, key=lambda r: (r[0], not r[3])):
            if rid in done:
                continue
            done.add(rid)
            why = ("direct dependency, migration to zbus 6 planned" if direct
                   else f"{crate} comes with {parent}; informational, not a vulnerability")
            print(f'  {{ id = "{rid}", reason = "{why}" }},')
        print("]")
    return 1 if any(d for *_, d in ignores) else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "Cargo.lock"))

Run against three example lockfiles: an app that gets zbus 5 through a notification crate, a tool that moved to zbus 6 but kept a direct zvariant dependency, and an app that's fully migrated:

$ python3 zvariant_check.py tray-app/Cargo.lock
zbus versions in the lockfile: 5.19.0
RUSTSEC-2026-0370: zvariant 5.15.1
  via tray-app 0.3.0 -> example-notify 1.2.0 -> zbus 5.19.0 -> zvariant 5.15.1
  example-notify holds it: needs a release of example-notify on zbus 6
RUSTSEC-2026-0371: zbus_names 4.3.5
  via tray-app 0.3.0 -> example-notify 1.2.0 -> zbus 5.19.0 -> zbus_names 4.3.5
  example-notify holds it: needs a release of example-notify on zbus 6
RUSTSEC-2026-0372: zvariant_derive 5.15.1
  via tray-app 0.3.0 -> example-notify 1.2.0 -> zbus 5.19.0 -> zvariant 5.15.1 -> zvariant_derive 5.15.1
  comes with zvariant: goes away with it

# deny.toml, until the upgrade lands (cargo-deny fails on unmaintained by default)
[advisories]
ignore = [
  { id = "RUSTSEC-2026-0370", reason = "zvariant comes with zbus; informational, not a vulnerability" },
  { id = "RUSTSEC-2026-0371", reason = "zbus_names comes with zbus; informational, not a vulnerability" },
  { id = "RUSTSEC-2026-0372", reason = "zvariant_derive comes with zvariant; informational, not a vulnerability" },
]
exit=0
$ python3 zvariant_check.py dbus-tool/Cargo.lock
zbus versions in the lockfile: 6.0.0
RUSTSEC-2026-0370: zvariant 5.15.1
  via dbus-tool 1.0.0 -> zvariant 5.15.1
  direct dependency: replace it with zbus 6 (default-features = false) and zbus::wire / the zbus root
RUSTSEC-2026-0372: zvariant_derive 5.15.1
  via dbus-tool 1.0.0 -> zvariant 5.15.1 -> zvariant_derive 5.15.1
  comes with zvariant: goes away with it
warning: zbus 6 and zvariant are both in the tree; their Variant/Value and Signature types are unrelated, so values can't pass between the two crates' APIs

# deny.toml, until the upgrade lands (cargo-deny fails on unmaintained by default)
[advisories]
ignore = [
  { id = "RUSTSEC-2026-0370", reason = "direct dependency, migration to zbus 6 planned" },
  { id = "RUSTSEC-2026-0372", reason = "zvariant_derive comes with zvariant; informational, not a vulnerability" },
]
exit=1
$ python3 zvariant_check.py migrated/Cargo.lock
zbus versions in the lockfile: 6.0.0
exit=0

It exits with status 1 only when you have a direct dependency on one of the three, since that's the case you can fix today.

An ignore entry is a policy exception, and it should be easy to find again later. Here's Safeguard's interactive walkthrough of handling policy exceptions:

What to do

  1. Run cargo tree -i zvariant, or the script above, to see where zvariant comes from.
  2. If it comes through a dependency, add ignore entries with reasons, and note which crate you're waiting on. Check for a zbus 6 release of it when you next update dependencies.
  3. If you use zbus 5 directly, schedule the 6.0 migration with the upgrade chapter open. Start with the blocking API and Value to Variant. Don't ship zbus 6 while a dependency that exchanges values with your code is still on zbus 5.
  4. If you depend on zvariant or zbus_names directly, switch to zbus 6 with default features off. It's mostly a rename.
  5. Review your unmaintained policy once, while you're here. Decide whether a merged-and-replaced crate should fail your build the same way an abandoned one does, and write the answer in deny.toml. Our guide on when it's OK to ignore an advisory can help you decide.

The fair view

The maintainers did this carefully. They planned the merge in the open, released the old crates once more with clear deprecation notices, wrote a detailed upgrade chapter, kept an alias module for a whole major version, and filed the advisories themselves so nobody would find out by accident. An advisory is the one channel that reaches every downstream build, and that's the point of it.

The cost is that a build failure lands on hundreds of projects that can't act on it, because zvariant reaches them through a crate they don't control. That's cargo-deny's default doing what it says: it can't tell a crate that was merged and replaced from one that was abandoned. Ignoring with a reason is the right response, not a workaround. Just don't let it become permanent. A crate that "receives no further releases" won't get a fix if a bug turns up later, so the ignore entries should go once your dependencies are on zbus 6.

Check yourself

Your project doesn't use zbus directly, but cargo deny check fails on RUSTSEC-2026-0370. Why?

A dependency uses zbus 1.x to 5.x, and every one of those depends on zvariant, which RustSec marked unmaintained on 17 October. cargo-deny fails on unmaintained advisories for any crate in the tree by default.

Is zvariant vulnerable?

No. The advisories are informational, category "unmaintained". zvariant's code was merged into zbus 6.0, and the standalone crate will get no further releases. The risk is that a future bug in it won't be fixed there.

You switched your app to zbus 6, and now a call into a dependency fails with "expected zbus::Variant, found zvariant::Value". What happened?

The dependency still uses zbus 5, so it brings its own zvariant, whose types are unrelated to zbus 6's. Find it with cargo tree -i zvariant, then wait for its zbus 6 release, or keep its types on its side of the boundary.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.