Skip to content

Bug: dnf5 transactions fire the snapshot hook twice (libdnf5 actions plugin and RPM plugin both run) #16

@rocketman-code

Description

@rocketman-code

Current Behavior

On a dnf5 transaction with a system that has both the libdnf5 actions plugin
and the RPM plugin installed (the default after dnf install atomic-rollback),
the snapshot hook fires twice and produces two messages. Observed during a
dnf remove transaction:

Snapshot 'root.pre-update' already exists.
Snapshot 'root.pre-update' already exists.

Both messages come from the same Command::Snapshot code path in
src/main.rs:180. Each is emitted by a separate invocation of
/usr/bin/atomic-rollback snapshot: one from the libdnf5 actions plugin at
pre_transaction, and one from the RPM plugin's tsm_pre hook triggered when
libdnf5 invokes librpm later in the same transaction. On a transaction where
no prior snapshot exists, the first invocation would emit "created" and the
second "already exists", still producing two messages for one logical
transaction.

Expected Behavior

A single dnf5 transaction should produce at most one snapshot-related message
from the atomic-rollback integration, regardless of how many plugin layers the
tool hooks into. The user should not see the tool reporting snapshot actions
twice for the same transaction.

Context

The libdnf5 actions plugin and the RPM plugin target pre-transaction hooks at
different layers of the stack. libdnf5 calls into librpm to execute the actual
transaction, so when a dnf5 transaction runs, both layers' pre-transaction
hooks fire and each invokes /usr/bin/atomic-rollback snapshot. This is not
visible on pure-rpm or non-dnf5 transactions (only the RPM plugin fires) or
on dnf5 transactions with only one of the two plugins installed. It is only
observable when both plugins are registered, which is the default state after
installing the package.

There is no correctness impact because the snapshot command is idempotent:
the first invocation creates the snapshot (or no-ops if it already exists),
and the second sees it already exists and reports that. But the duplicate
output is confusing. It suggests something is running twice, which it is, and
it is unclear to the user whether one of the plugins failed, whether the
snapshot was taken correctly, or whether the tool is in a bad state.

Discovered on atomic-rollback-0.3.7-1.fc43 during a dnf5 package removal.

Technical Details

Reproduction

With atomic-rollback 0.3.7 installed via dnf (which installs both plugins),
run any dnf5 transaction that modifies packages:

$ sudo dnf install <any-package>

Observe two snapshot-related messages from the atomic-rollback hook for the
single transaction.

Relevant Code

Both plugins invoke /usr/bin/atomic-rollback snapshot at pre-transaction
time.

plugins/atomic-rollback.actions installed at
/etc/dnf/libdnf5-plugins/actions.d/atomic-rollback.actions:

pre_transaction:::raise_error=1:atomic-rollback snapshot

plugins/atomic_rollback.c installed at
/usr/lib64/rpm-plugins/atomic_rollback.so:

static rpmRC atomic_rollback_tsm_pre(rpmPlugin plugin, rpmts ts)
{
    if (rpmtsFlags(ts) & (RPMTRANS_FLAG_TEST|RPMTRANS_FLAG_BUILD_PROBS))
        return RPMRC_OK;

    if (access(BINARY, X_OK) != 0)
        return RPMRC_OK;

    int rc = system(BINARY " snapshot");
    if (rc != 0) {
        rpmlog(RPMLOG_ERR, "atomic-rollback: snapshot failed, aborting transaction\n");
        return RPMRC_FAIL;
    }
    return RPMRC_OK;
}

Both call the same Command::Snapshot entrypoint in src/main.rs:175-184,
which emits the user-visible message:

Command::Snapshot(sub) => match sub {
    SnapshotCommand::Create { name } => {
        match snapshot::snapshot(name.as_deref()) {
            Ok(snapshot::SnapshotResult::Created(name)) =>
                eprintln!("Snapshot '{name}' created."),
            Ok(snapshot::SnapshotResult::Existed(name)) =>
                eprintln!("Snapshot '{name}' already exists."),
            ...
        }
    }

Root Cause

Neither plugin is aware of the other. Each plugin's hook fires independently
at its respective layer (libdnf5 pre_transaction, librpm tsm_pre) and
each unconditionally invokes atomic-rollback snapshot. When libdnf5 drives
a transaction, it calls through to librpm, so both layers fire for the same
user-initiated transaction. The tool has no mechanism to detect that a
sibling plugin has already performed the snapshot for this transaction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions