DriverRollback
Content-addressed restore points for a driver store
Techlosoft - Device Reliability Center
WHAT IT IS
==========
DriverRollback answers one question the rest of the Device Reliability Center
line does not: "that driver update broke the machine - how do I get the exact
bytes back?"
You take a snapshot of a driver-store directory tree before you touch it.
DriverRollback SHA-256s every file, copies each unique blob into a
content-addressed object store exactly once, and writes a JSON manifest of
path -> {hash, size, mode, modtime}. Take twenty snapshots of a tree that
barely changes and the store grows by roughly what actually changed, because
identical content is never stored twice.
When you need to go back, DriverRollback computes the minimal set of
operations that turns the tree back into the snapshot state and PRINTS IT.
That is all it does unless you explicitly pass --apply. When you do apply it,
nothing is deleted: every file it would overwrite or remove is MOVED into a
timestamped quarantine directory first, and every file it writes is re-hashed
against the manifest before the run is called a success.
Its sibling DeviceDriver seals and verifies a driver store. DriverRollback
snapshots and restores one. A driver store here is modelled as an ordinary
directory tree, which is why this runs and is testable on Linux and macOS as
happily as on Windows.
INSTALL
=======
Pre-built binaries are in dist/. There is nothing to install - copy the one for
your platform anywhere on your PATH and run it.
dist/driverrollback-linux-amd64 Linux, x86-64
dist/driverrollback-darwin-arm64 macOS, Apple Silicon
dist/driverrollback-darwin-amd64 macOS, Intel
dist/driverrollback-windows-amd64.exe Windows, x86-64
On macOS and Linux you may need to mark it executable:
chmod +x driverrollback-linux-amd64
To build from source you need Go 1.24 or newer. There are no dependencies of
any kind, no cgo and no network access required:
go build -o driverrollback .
go test ./...
COMMANDS
========
driverrollback snapshot --source
[--store ] [--name ]
[--ledger ] [--json]
driverrollback list [--store ] [--json]
driverrollback diff [--store ] [--all] [--json]
driverrollback restore --target [--apply] [--quarantine ]
[--store ] [--ledger ] [--all] [--json]
driverrollback verify [--store ] [--all] [--json]
driverrollback help | -h | --help
snapshot
Walks the source tree, hashes every regular file with SHA-256, copies each
unique blob into the object store exactly once, and writes a manifest.
A blob already in the store is not copied again, so a second snapshot of
unchanged content adds zero bytes. The manifest is written to a .part file
and renamed into place. Snapshots are immutable once written; nothing ever
rewrites or prunes an existing snapshot.
list
Every snapshot in the store, oldest first, with its timestamp, file count,
apparent size, and the bytes it actually added to the store after dedup -
plus the store-wide total of what deduplication saved.
diff
Compares two snapshots path by path: added, removed, changed, unchanged,
per-file byte deltas and the net size change. Output is sorted by path and
is stable, so it diffs cleanly against itself in version control. Unchanged
files are summarised but not listed unless you pass --all.
restore
Computes the minimal set of operations to turn the target tree back into
the snapshot state and prints the plan. THIS IS A DRY RUN. Without --apply
it does not create, write, move, rename or remove a single byte, and it
does not even create the quarantine directory. Pass --apply to perform it.
verify
Re-hashes every object the snapshot references and reports any object that
is missing, corrupt or unreadable. This is the check to run before you
trust a restore, and periodically on an archive store.
FLAGS
--source Directory tree to snapshot. A bare positional argument
to `snapshot` is also accepted.
--store Object store and manifest directory. Default: .store
Created on first snapshot along with missing parents.
--target Directory tree `restore` operates on. Restoring into a
directory that does not exist yet is legal - every file
is then a create.
--name Free-text label recorded in the manifest and shown by
list and restore. Purely descriptive.
--apply Actually perform the restore. Without it, restore is a
dry run. There is no config file, environment variable
or hidden mode that changes this.
--quarantine Where displaced files are MOVED under --apply. Default
is a timestamped folder beside the target:
/.quarantine//
It must not be inside the target; that is refused.
--ledger Append-only JSON-lines audit ledger written by every
mutating run. Default: /ledger.jsonl
--all Include the boring rows (unchanged files in diff, keep
operations in restore, healthy objects in verify).
--json Machine-readable output. Available on all five
subcommands.
Short forms -s, -S, -t, -q, -l and -n are accepted for --source, --store,
--target, --quarantine, --ledger and --name. Flags may appear before or after
positional arguments; either order works.
HOW THE STORE IS LAID OUT
=========================
/objects/7a/c1609e18a2... one file per unique SHA-256 blob,
named by its own hash, fanned out over
one 2-character directory level
/snapshots/snap-.json one manifest per snapshot
/ledger.jsonl default audit ledger
Snapshot ids are the UTC time the snapshot was taken, e.g.
snap-20260811T065709Z. Two snapshots inside the same second get -2, -3 and so
on appended; ids are never reused.
Everything in the store is plain, boring files. You can rsync it, tar it, put
it on a NAS, or hash-check it with sha256sum yourself - the object name IS the
sha256sum of its contents, which is exactly what `verify` exploits.
QUICK START
===========
# before you install anything
driverrollback snapshot --source ./driverstore --store ./.store --name pre-gpu-update
# ...the update goes badly...
# what changed?
driverrollback list --store ./.store
driverrollback diff snap-20260811T065709Z snap-20260811T065720Z --store ./.store
# is the snapshot still intact?
driverrollback verify snap-20260811T065709Z --store ./.store
# what WOULD a rollback do? (dry run - touches nothing)
driverrollback restore snap-20260811T065709Z --target ./driverstore --store ./.store
# do it
driverrollback restore snap-20260811T065709Z --target ./driverstore --store ./.store --apply
EXAMPLE OUTPUT
==============
$ driverrollback snapshot --source ./driverstore --store ./.store --name pre-gpu-update
snapshot snap-20260811T065709Z
name : pre-gpu-update
created : 2026-08-11T06:57:09Z
source : /srv/demo/driverstore
store : /srv/demo/.store
files : 9 in 3 dirs
apparent : 386.9 KiB (396209 bytes)
unique blobs: 8 (343.0 KiB)
added : 8 objects, 343.0 KiB (351209 bytes)
deduped : 43.9 KiB not copied again
ledger : /srv/demo/.store/ledger.jsonl
$ driverrollback snapshot --source ./driverstore --store ./.store --name unchanged-rerun
snapshot snap-20260811T065709Z-2
...
added : 0 objects, 0 B (0 bytes)
deduped : 386.9 KiB not copied again
$ driverrollback diff snap-20260811T065709Z snap-20260811T065720Z --store ./.store
diff snap-20260811T065709Z -> snap-20260811T065720Z
from : 2026-08-11T06:57:09Z 9 files 386.9 KiB
to : 2026-08-11T06:57:20Z 9 files 424.0 KiB
- hdaudio.inf_amd64_c3/hdaudio.sys (29.3 KiB)
~ nvlddmkm.inf_amd64_a1/nvlddmkm.inf (73 B -> 73 B, 0 B)
27987d4bd3c8 -> 95fde451ad58
~ nvlddmkm.inf_amd64_a1/nvlddmkm.sys (195.3 KiB -> 253.9 KiB, +58.6 KiB)
7ac1609e18a2 -> 5305b15e72d5
+ nvlddmkm.inf_amd64_a1/nvlddmkm_new.dll (7.8 KiB)
added : 1 files, 7.8 KiB
removed : 1 files, 29.3 KiB
changed : 2 files
unchanged : 6 files
net size : +37.1 KiB (38000 bytes)
$ driverrollback restore snap-20260811T065709Z --target ./driverstore --store ./.store
restore plan (DRY RUN)
snapshot : snap-20260811T065709Z (pre-gpu-update)
taken : 2026-08-11T06:57:09Z
target : /srv/demo/driverstore
store : /srv/demo/.store
quarantine would be: /srv/demo/driverstore.quarantine/20260811T065736Z
create hdaudio.inf_amd64_c3/hdaudio.sys (29.3 KiB)
overwrite nvlddmkm.inf_amd64_a1/nvlddmkm.inf (73 B -> 73 B)
overwrite nvlddmkm.inf_amd64_a1/nvlddmkm.sys (253.9 KiB -> 195.3 KiB)
quarantine nvlddmkm.inf_amd64_a1/nvlddmkm_new.dll (7.8 KiB, not in snapshot)
create : 1 files, 29.3 KiB to write
overwrite : 2 files, 195.4 KiB to write, 254.0 KiB to quarantine
quarantine : 1 files, 7.8 KiB
keep : 6 files
DRY RUN - nothing was written. Re-run with --apply to perform this restore.
$ driverrollback verify snap-20260811T065709Z --store ./.store
verify snap-20260811T065709Z
store : /srv/demo/.store
files : 9 referencing 8 unique objects
CORRUPT 7ac1609e18a2 stored bytes do not hash to the object name
stored bytes hash to 8b819df96b72
used by nvlddmkm.inf_amd64_a1/nvlddmkm.sys
objects ok : 7
missing : 0
corrupt : 1
unreadable : 0
verified : 147.7 KiB (151209 bytes)
result : DAMAGED - this snapshot cannot be fully restored
THE RESTORE SAFETY MODEL
========================
This is the part that matters, so it is spelled out in full.
1. DRY RUN BY DEFAULT. `restore` without --apply reads the store and the
target, prints the plan, and exits. It creates nothing, not even the
quarantine directory. There is no environment variable, config file or
undocumented flag that turns this off - only --apply on the command line.
2. NOTHING IS EVER UNLINKED. "Remove" does not mean delete. Any file the
restore would overwrite, and any file in the target that is not in the
snapshot, is MOVED into the quarantine directory first, keeping its
relative path. Deleting the quarantine afterwards is your decision, not
the tool's. If a name already exists in quarantine it is suffixed .2, .3
and so on rather than clobbered.
3. THE STORE IS CHECKED BEFORE ANYTHING MOVES. If any object the plan needs
is absent from the store, restore aborts before touching the target.
4. WRITE VIA .part AND RENAME. Every restored file is written to
.part, fsynced, re-hashed, then renamed over the destination, so a
crash mid-write cannot leave a half-written file under the real name.
5. RE-HASH AFTER WRITING. Every restored file is hashed again after the
rename and compared with the manifest hash AND size. A mismatch is a hard
error that stops the run and is recorded in the ledger. In practice this
is what catches a corrupt object store: the plan may pass step 3 (the
object file exists) and still fail here (its bytes are wrong).
6. DIRECTORIES ARE NEVER MOVED OR DELETED. If the target holds a directory
where the snapshot holds a file, that is reported as a CONFLICT and
--apply refuses the whole run until you resolve it by hand. Directories
in the target that the snapshot does not mention are left exactly where
they are.
7. THE QUARANTINE IS NEVER INSIDE THE TARGET. A --quarantine path inside
the target is refused, so the next snapshot of the tree does not sweep up
the things you just rolled back.
8. EVERY MUTATING RUN IS LOGGED. snapshot and restore --apply each append
one JSON line to the ledger, including per-file outcomes and, on failure,
the error text.
LEDGER FORMAT
=============
One JSON object per line, UTF-8, newline-terminated, appended in chronological
order. Never rewritten in place.
ts RFC 3339 UTC timestamp, nanosecond precision
tool tool name and version that wrote the line
op "snapshot" or "restore"
snapshot the snapshot id involved
store absolute store path
source absolute source path (snapshot only)
target absolute target path (restore only)
quarantine absolute quarantine root used (restore --apply only)
result "ok" or "error"
error error text when result is "error"
counts operation counts for the run
bytes bytes added to the store (snapshot) or written (restore)
files one object per file: path, action, status, hash, bytes, and
the quarantine path when the file was displaced
Actions you will see: store, dedup (snapshot); create, overwrite, remove, keep
(restore). Statuses: ok, restored, quarantined, restored+quarantined.
EXIT CODES
==========
0 Success. This includes a dry run that found work to do, and a diff
that found differences - those are answers, not failures.
1 Bad invocation (unknown command, missing required flag, wrong number
of positional arguments) or an I/O error (missing store, unknown
snapshot id, unreadable tree, hash mismatch while restoring, an
unresolved directory conflict). Explicit help exits 0.
2 `verify` completed and found the snapshot DAMAGED - at least one
object it needs is missing, corrupt or unreadable. The verification
itself succeeded; the data did not.
SCOPE / WHAT THIS DOES NOT DO
=============================
Read this before you rely on it for anything.
IMPLEMENTED, AND WORKING TODAY
- SHA-256 content-addressed object store with real deduplication across
every snapshot in the store. The same blob in two packages, or in twenty
snapshots, occupies the store once.
- Snapshot manifests recording path, hash, size, permission bits and
modification time for every regular file.
- list with per-snapshot apparent size and the bytes actually added.
- diff with added / removed / changed / unchanged, per-file and net byte
deltas, sorted and stable.
- Minimal restore plans, dry run by default, --apply to execute.
- Quarantine-instead-of-delete, .part-and-rename writes, post-write
re-hashing against the manifest, refusal on conflicts.
- verify: re-hashes every object a snapshot references and reports missing,
corrupt and unreadable objects.
- Append-only JSON-lines audit ledger for every mutating run.
- --json on all five subcommands.
- The store excludes itself from snapshots, so it can safely live inside the
tree it is snapshotting.
NOT IMPLEMENTED - AND HONESTLY ABSENT, NOT STUBBED
- IT DOES NOT TALK TO THE REAL WINDOWS DRIVER STORE API. There is no call to
SetupAPI, DIFxAPI, CfgMgr32, pnputil, DISM, the driver store service or
any Windows API whatsoever. It treats %SystemRoot%\System32\DriverStore as
what it is on disk - a directory of files - and nothing more.
- IT DOES NOT STAGE, INSTALL, RANK OR REMOVE DRIVERS IN A LIVE OS. Copying
the bytes of a driver package back into place is not the same as staging
it. Driver ranking, OEM*.inf renumbering, the INF cache, catalog trust
registration, device rebinding and reboots are all outside its remit.
- IT IS NOT A SUBSTITUTE FOR SYSTEM RESTORE, WINDOWS RE, VSS OR A BACKUP.
It captures one directory tree's file contents. It knows nothing about the
registry, boot configuration, running services, or the rest of the volume.
Do not roll back a live system's DriverStore with this and expect the
machine to boot; snapshot a staging copy.
- NO REGISTRY. No HKLM\SYSTEM\CurrentControlSet\Enum, no Class keys, no
service entries. A driver's registry footprint is not captured and not
restored.
- NO DIGITAL SIGNATURE OR CATALOG VERIFICATION. .cat files are hashed as
opaque bytes like anything else. Authenticode chains, WHQL attestation and
cross-certificates are not parsed or checked. Its sibling DeviceDriver is
the tool for sealing and verifying; this one is for snapshotting.
- NO INF PARSING. It does not read DriverVer, Class, Provider or anything
else out of an .inf. It cannot tell you "this is version 33.0.16" - only
that the bytes changed.
- NO SYMBOLIC LINKS, HARD LINKS, JUNCTIONS OR REPARSE POINTS. Non-regular
entries are counted, reported as skipped, and neither stored nor restored.
A tree that depends on links will not round-trip. Hard links become
separate files on restore (they will share one stored object, but the link
relationship is lost).
- NO EMPTY DIRECTORIES. Only files are recorded. A directory that contains
nothing in the snapshot is not recreated on restore, and an empty
directory left in the target is not removed.
- NO ALTERNATE DATA STREAMS, EXTENDED ATTRIBUTES, ACLS, OWNERSHIP OR SECURITY
DESCRIPTORS. Only the Unix permission bits and the modification time are
recorded, and on Windows the permission bits are largely decorative.
Restoring will not restore who owns a file or who may read it.
- NO COMPRESSION AND NO SUB-FILE DEDUPLICATION. Dedup is whole-file only.
Change one byte of a 200 MB driver and the whole 200 MB is stored again.
There is no rolling-hash chunking, no delta encoding and no zlib.
- NO ENCRYPTION AND NO SIGNING OF THE STORE ITSELF. Anyone who can write to
the store can replace an object; verify will catch it, but nothing
prevents it.
- NO GARBAGE COLLECTION, PRUNING OR RETENTION POLICY. Objects are never
removed, snapshots are never expired, and there is no `forget` command.
The store only grows. Deleting old snapshots safely needs a mark-and-sweep
that is deliberately not written yet - delete a manifest by hand and its
unreferenced objects simply stay.
- NO CONCURRENCY CONTROL. There is no lock file. Two DriverRollback
processes writing to one store, or a snapshot of a tree that is being
modified underneath it, are not defended against beyond a size-changed-
while-reading check.
- NO NETWORK, NO REMOTE STORES, NO CLOUD. Local filesystem paths only. No
daemon, no scheduler, no service installation - run it from Task
Scheduler, cron or a systemd timer yourself.
- NO PARTIAL OR PATH-FILTERED RESTORE, NO INCLUDE/EXCLUDE PATTERNS, AND NO
ROLLBACK OF A ROLLBACK. A restore is all-or-nothing against one snapshot.
To undo one, take a snapshot first and restore that.
- NO PROGRESS REPORTING OR PARALLELISM. Hashing is single-threaded and
silent until the run finishes. A very large tree will simply take a while.