The installer printed one line and quit.
Unsupported kernel version 6.17.0-14-generic. Requires 2.6–6.8.
I’d just rebuilt my Ubuntu server — fresh install, latest LTS, nothing exotic. The kernel shipped at 6.17 because that’s what apt upgrade gives you in mid-2026. And Synology Active Backup for Business, which I’d used without drama for two years, flatly refused to even start.
Four hours later, full snapshot backups were running on a kernel Synology will not officially support for months. The fix was less about the patch than about separating the pieces the installer had bundled together.

Skip the installer, patch the parts. Vendor installers gate on supported configurations and hard-exit when they do not like what they see. The Linux agent is two components: a userspace service (the normal background program) and a kernel module (a driver that plugs into the operating system’s core). The userspace service worked on 6.17. Only the module needed changes.
What failed
The Active Backup agent ships a kernel module called synosnap that provides block-level snapshot support — it freezes the state of your disks at a filesystem level so the backup sees a consistent picture. Between kernel 6.8 and 6.17, the Linux block device API went through multiple breaking changes: renamed structs (the C-language data containers that represent things like disks and files), changed function signatures, removed fields. The module’s build system — DKMS, a framework that automatically rebuilds kernel modules when the kernel updates — hit dozens of compilation errors and bailed.
Without the module there were no snapshots, and without snapshots there were no backups.
I considered waiting for Synology. Their kernel-support lag is measured in months. The server needed backups now.
The first wrong turn
My first attempt was to edit the installer so it accepted kernel 6.17 and let the build system handle the API changes. The build produced 40+ errors. The module’s configure system — a Makefile that runs small test compilations to detect available kernel APIs — could not make sense of 6.17. Half the feature-detection tests returned wrong answers or crashed. The others “succeeded” against assumptions that would fail at runtime.
That detour took 30 minutes. The .deb had two components, the userspace service worked perfectly, and only the module needed fixing. I should have opened the archive first. Most of the four hours from installer failure to first successful backup went instead to reading kernel changelogs and understanding each API change.
I backed up and separated the work:
- Skip the broken installer — install the userspace service directly via
dpkg(Debian’s package installer), extracting it from the.debarchive Synology distributes - Fix runtime dependencies — the service needs libraries and paths the module package normally creates; reproduce those by hand
- Patch synosnap source — fix every kernel API break between 6.8 and 6.17, one source file at a time
- Build and load — compile the module, install it, configure auto-load on boot
The changes between 6.8 and 6.17
The kernel API changes were concentrated in the block device subsystem — six of the 12 patches hit the same area. Kernel maintainers tend to sweep through whole subsystems at once. Once I found the bdev_file_open_by_path change, I scanned every other block-device call in the module. Fixing one call was not enough.
the mechanism — kernel API guards, kallsyms, and making this DKMS-safe give me the detail
The full API break table (every change that required a patch):
| API Change | Kernel Version | Impact |
|---|---|---|
bdev_open_by_path → bdev_file_open_by_path | 6.9+ | Every block device open/close path |
blk_alloc_disk gained a second argument | 6.9+ | Disk allocation in tracer |
struct fd.file removed, replaced by fd_file() macro | 6.12+ | ftrace and syscall hooking |
BIO_THROTTLED → BIO_QOS_THROTTLED | 6.17 | BIO flag propagation |
bio->bi_remaining → bio->__bi_remaining | 6.17 | Snapshot handle reference counting |
submit_bio_noacct return type changed to void | 6.17 | Submit bio passthrough |
MS_RDONLY → SB_RDONLY | 6.x | Mount state checks |
bd_has_submit_bio field removed | 6.17 | Tracing transition logic |
Why #ifdef guards work here. The kernel exports its own feature-detection symbols. A compat header uses kconfig.h presence tests or explicit version guards via LINUX_VERSION_CODE / KERNEL_VERSION() macros — available from <linux/version.h>. The guard pattern means one source tree compiles cleanly across a range of kernels; the compiler dead-strips the unused branch.
The kallsyms dependency is the real maintenance cost. The module hooks system calls by resolving symbol addresses at load time from /proc/kallsyms. Those addresses are randomized per kernel build (and zeroed entirely if CONFIG_KALLSYMS_ALL is off). To make this reliable across kernel updates, the right approach is to use kallsyms_lookup_name() at module init time rather than hardcoding addresses:
#include <linux/kallsyms.h>
typedef asmlinkage long (*sys_call_fn)(const struct pt_regs *);
static sys_call_fn orig_sys_openat;
static int __init synosnap_init(void) {
orig_sys_openat = (sys_call_fn)kallsyms_lookup_name("sys_openat");
if (!orig_sys_openat) return -ENOENT;
// ...
}Note: kallsyms_lookup_name was un-exported in 5.7 and is only available again if you have a patch or use kprobe-based lookup as a workaround. For a one-kernel deployment, reading /proc/kallsyms at build time and baking the address into kernel-config.h is simpler — just be aware that any kernel update invalidates it.
To make this DKMS-persistent, add a dkms.conf that re-bakes kernel-config.h in POST_BUILD:
# /usr/src/synosnap-<version>/dkms.conf
PACKAGE_NAME="synosnap"
PACKAGE_VERSION="<version>"
BUILT_MODULE_NAME[0]="synosnap"
MAKE[0]="make -C /lib/modules/$kernelver/build M=$dkms_tree/$module/$module_version/build"
POST_BUILD="bash scripts/gen-kernel-config.sh $kernelver > kernel-config.h"
AUTOINSTALL="YES"Then dkms add, dkms build, dkms install — and the module rebuilds automatically on apt upgrade linux-image-* without manual intervention.
The result was eleven source files and twelve distinct API changes. I read a lot of kernel changelogs.
The Hardest Part: Hand-Writing kernel-config.h
The synosnap build system uses Makefile-based feature detection — it compiles tiny test programs to see which kernel APIs exist, then sets flags accordingly. On 6.17, the configure system tried 40+ Makefile test compilations. Most failed or, worse, succeeded with wrong answers rather than clean failures.
I spent 20 minutes debugging the configure system before seeing the simpler answer. It was trying to detect the APIs exposed by my specific running kernel. I was already running that kernel, so I could answer the questions directly.
So I bypassed the entire detection system and wrote kernel-config.h by hand — 40+ feature flags declaring exactly which APIs exist in 6.17.0-14-generic. Writing it took 20 minutes and worked on the first try. Reading the source and answering the questions directly was simpler than repairing feature detection:
# Get the addresses you need
sudo grep -w 'sys_call_table\|blk_mq_submit_bio\|blk_alloc_queue' /proc/kallsyms
The trickiest part is the symbol addresses. The module hooks into kernel system calls (the operating system’s internal function-call table) by looking up function addresses from /proc/kallsyms — a virtual file the kernel generates that lists every internal function and its current memory address. These addresses change on every kernel build because of address space layout randomization, which scrambles memory positions for security. Any kernel update requires regenerating this file.
It is not ideal. It works.
The block-device API migration
The biggest single patch was the block device open/close path. Kernel 6.9 replaced the bdev_handle pattern — where you got a “handle” token representing your access to a disk — with a file-based API where you open a block device the way you’d open a regular file:
#ifdef HAVE_BDEV_FILE_OPEN_BY_PATH
// Kernel 6.9+: file-based block device API
struct file *bdev_file = bdev_file_open_by_path(path, BLK_OPEN_READ, NULL, NULL);
if (IS_ERR(bdev_file))
return PTR_ERR(bdev_file);
dev->sd_base_dev = file_bdev(bdev_file);
dev->sd_base_bdev_file = bdev_file; // store for cleanup
// Cleanup: bdev_fput(bdev_file) instead of bdev_release(handle)
#elif defined HAVE_BDEV_OPEN_BY_PATH
// Kernel 6.8: handle-based API
// ...existing code...
#endif
The pattern repeated in tracer.c, bdev_state_handler.c, and ioctl_handlers.c. Every block-device open and close needed a new #ifdef branch — a compile-time check that uses the new path on 6.9 or later and the old path otherwise.
The struct fd change
This one took me a minute. Kernel 6.12 and later quietly changed struct fd — the internal data structure representing an open file — so the .file field no longer exists. You access it through the fd_file() helper macro instead, which is a one-line shorthand the kernel provides for backward compatibility:
// Old: f.file->private_data
// New: fd_file(f)->private_data
#ifdef fd_file
fc = fd_file(f)->private_data;
#else
fc = f.file->private_data;
#endif
It was a small change, but without it the module would not compile. It broke two files.
Build and deploy
After all patches, the module compiled clean:
cd /tmp/synosnap-patched
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
# synosnap.ko built successfully
sudo insmod synosnap.ko
ls /dev/synosnap-ctl # snapshot device exists
For persistence across reboots:
sudo cp synosnap.ko /lib/modules/$(uname -r)/extra/
sudo depmod -a
echo "synosnap" | sudo tee /etc/modules-load.d/synosnap.conf
This patch works on 6.17.0-14-generic specifically. A kernel update changes /proc/kallsyms addresses and may introduce new API breaks. The right long-term fix is proper DKMS integration — the Dynamic Kernel Module Support framework that automatically rebuilds modules on upgrades — with a patched configure step. For one production server, though, manual rebuilds on rare kernel updates are a reasonable trade.