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 I had full snapshot backups running on a kernel Synology won’t officially support for months. Here’s what broke, how I fixed it, and why the real lesson isn’t about kernel patches.

The takeaway before I go deep: skip the installer, patch the parts. Vendor installers gate on supported configurations and hard-exit when they don’t like what they see. But a Linux agent is just two components — a userspace service (the normal program that runs in the background) and a kernel module (a driver that plugs directly into the operating system’s core). The userspace service worked fine on 6.17. Only the module needed surgery.
What Died and Why
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.
No module, no snapshots. No snapshots, no backups.
I debated waiting for Synology to ship an update. Their track record on kernel support lag is months. The server needed backups now.
The Strategy, and What I Tried First
My first attempt was dumb. I figured I’d just edit the installer script to accept kernel 6.17 and let the build system figure out the API changes. The build exploded with 40+ errors. The module’s configure system — a Makefile that runs tiny test compilations to detect which kernel APIs are available — couldn’t make sense of 6.17 at all. Half the feature-detection tests returned wrong answers or crashed, and the ones that “succeeded” were compiling against wrong assumptions that would fail at runtime.
So I backed up and took the thing apart:
- 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
What Broke 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. When you find one break in a subsystem, expect more.
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.
Eleven source files. 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, most of those test compilations failed or, worse, succeeded with wrong answers.
I spent 20 minutes trying to debug the configure system before I realized I was being an idiot. The configure step was trying to detect which APIs my specific running kernel exposed. I was already running that kernel. I could just 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:
# 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.
Not ideal. But 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
This pattern repeated across three files: tracer.c, bdev_state_handler.c, and ioctl_handlers.c. Every place the module opens or closes a block device needed a new #ifdef branch — a compile-time check that says “if the kernel is 6.9 or later, use this code path; otherwise use the old one.”
The struct fd Surprise
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
Small change, but without it the module won’t compile at all. 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
What I’d Do Differently
1. Skip the installer, install the parts. When vendor installers gate on unsupported configurations, crack open the archive. The userspace service worked perfectly — the .deb package had two components, and only one needed fixing. I should’ve checked that before burning 30 minutes trying to trick the installer script.
2. Manual kernel-config.h beats broken feature detection. The module’s configure system tried 40+ Makefile test compilations, most of which failed on 6.17 in ways that produced wrong answers rather than clean failures. Writing the config header by hand took 20 minutes and worked first try. Sometimes the simplest approach is to read the source and answer the questions yourself.
3. Kernel API changes cluster by subsystem. The block device subsystem accounted for 6 of 12 patches. Once I found the bdev_file_open_by_path change, I knew to scan every other block device call in the module. Don’t fix one and assume you’re done — the kernel maintainers tend to sweep through whole subsystems at once.
4. Plan for kernel updates. This patch works on 6.17.0-14-generic specifically. A kernel update will change /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 auto-rebuilds modules on kernel upgrades — with a patched configure step. For one production server, though, manual rebuilds on the rare kernel update are a reasonable trade.
The backup agent is now running with full snapshot support. Total time from “installer failed” to “first successful backup”: about four hours. Most of that was reading kernel changelogs to understand what each API change actually did — and those 30 wasted minutes trying to fool the installer I should have just skipped.