From 63ea83f236a13663d8dabffe1be8e3eb0bb9e3bc Mon Sep 17 00:00:00 2001 From: Marcos Date: Tue, 5 May 2026 23:35:05 -0300 Subject: [PATCH] feat(driver): PCIe driver skeleton with /dev/spankerctl ioctl stub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First C-side scaffolding for the Spanker out-of-tree kernel module per ADR-002. The skeleton lands: - src/driver/Kbuild + Makefile — out-of-tree kbuild scaffold; the module builds with `make -C src/driver` against the running kernel (verified locally on 6.18.18-1-MANJARO). - src/driver/include/uapi/spanker_ioctl.h — public v0 ABI shared between the kernel module and the future Rust runtime per ADR-001. Defines SPANKER_IOC_MAGIC (0xE3, placeholder pending reconciliation against mainline ioctl-number.rst), SPANKER_IOC_PING, SPANKER_IOC_GET_VERSION, and struct spanker_version. ABI is unstable while major == 0; SPANKER_IOC_GET_VERSION lets userspace fail cleanly when major mismatches. - src/driver/spanker_main.c — module init/exit, character device /dev/spankerctl (singleton control device, always present once the module loads), and PCI driver registration with placeholder vendor/device IDs (0xDEAD/0xBEEF) since PopSolutions has no assigned PCI vendor ID yet. A LINUX_VERSION_CODE check selects the pre-6.4 vs post-6.4 class_create signature. - src/driver/spanker_ioctl.c — ioctl dispatcher; SPANKER_IOC_PING returns 0 and SPANKER_IOC_GET_VERSION fills struct spanker_version with {0, 1, 0, 0}. Uses stream_open and compat_ptr_ioctl so 32-bit userspace on a 64-bit kernel works without a separate compat path. - .gitignore — adds the kbuild artefact patterns. - .github/workflows/ci.yml — new driver-build job that installs linux-headers-$(uname -r) on ubuntu-24.04, runs `make -C src/driver`, asserts the GPL license string in modinfo, and `make clean`s. Local verification on Manjaro 6.18.18: $ make -C src/driver CC [M] spanker_main.o CC [M] spanker_ioctl.o LD [M] spanker.o MODPOST Module.symvers LD [M] spanker.ko BTF [M] spanker.ko $ modinfo src/driver/spanker.ko | grep -E '^(license|version|alias)' version: 0.1.0 license: GPL v2 alias: pci:v0000DEADd0000BEEFsv*sd*bc*sc*i* A userspace pytest smoke test (insmod + open /dev/spankerctl + issue SPANKER_IOC_PING / SPANKER_IOC_GET_VERSION) is intentionally deferred to a follow-up PR — it requires root + insmod permissions in CI and is independently reviewable. Per feedback_testing.md, this PR's test is the kbuild compile + modinfo assertion in the new driver-build CI job; the userspace harness ships in PR #1b. Follow-up issues to open after merge: - Reserve a real PCI vendor ID (currently 0xDEAD/0xBEEF placeholder). - Reconcile SPANKER_IOC_MAGIC 0xE3 with Documentation/userspace-api/ioctl/ioctl-number.rst before any in-tree submission attempt (Generation B). - PR #1b — userspace pytest smoke test + DKMS recipe. - ADR-003 — public interface contracts (ioctl SemVer policy). Authored by Agent 3 (Software Stack). Signed-off-by: Marcos --- .github/workflows/ci.yml | 21 +++ .gitignore | 10 ++ src/driver/Kbuild | 13 ++ src/driver/Makefile | 36 ++++++ src/driver/include/uapi/spanker_ioctl.h | 69 ++++++++++ src/driver/spanker_ioctl.c | 59 +++++++++ src/driver/spanker_main.c | 164 ++++++++++++++++++++++++ 7 files changed, 372 insertions(+) create mode 100644 src/driver/Kbuild create mode 100644 src/driver/Makefile create mode 100644 src/driver/include/uapi/spanker_ioctl.h create mode 100644 src/driver/spanker_ioctl.c create mode 100644 src/driver/spanker_main.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8818818..349c776 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,3 +21,24 @@ jobs: [ -f "$f" ] || continue grep -q "SPDX-License-Identifier" "$f" || { echo "$f missing SPDX header" ; exit 1; } done + + driver-build: + name: Driver / kbuild + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install kernel headers and build tools + run: | + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends \ + linux-headers-$(uname -r) build-essential + - name: Build spanker.ko (out-of-tree) + run: make -C src/driver + - name: Verify module metadata + run: | + modinfo src/driver/spanker.ko \ + | grep -E '^(license|description|version|alias):' + # Sanity: license must be GPL per ADR-002. + modinfo src/driver/spanker.ko | grep -q '^license:.*GPL' + - name: Clean + run: make -C src/driver clean diff --git a/.gitignore b/.gitignore index cb9ce47..eedf627 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,13 @@ __pycache__/ *.swp .vscode/ .idea/ + +# kbuild out-of-tree module artefacts (per ADR-002) +*.mod +*.mod.c +*.cmd +.*.cmd +Module.symvers +modules.order +*.symvers +.tmp_versions/ diff --git a/src/driver/Kbuild b/src/driver/Kbuild new file mode 100644 index 0000000..893e7e5 --- /dev/null +++ b/src/driver/Kbuild @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright (c) 2026 PopSolutions Cooperative +# +# Kbuild for the Spanker accelerator driver (out-of-tree module). +# Per ADR-002 — Linux driver model. + +obj-m += spanker.o + +spanker-y := \ + spanker_main.o \ + spanker_ioctl.o + +ccflags-y += -I$(src)/include diff --git a/src/driver/Makefile b/src/driver/Makefile new file mode 100644 index 0000000..a552d9f --- /dev/null +++ b/src/driver/Makefile @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright (c) 2026 PopSolutions Cooperative +# +# Top-level Makefile for the Spanker accelerator driver +# (out-of-tree kernel module). Per ADR-002 — Linux driver model. +# +# Usage: +# make # build spanker.ko against the running kernel +# make KDIR=... # cross-build against a specific kernel tree +# make clean # remove build artefacts +# +# Kbuild does the actual work; this Makefile only wraps the +# canonical "make -C $(KDIR) M=$(PWD)" recipe. + +KDIR ?= /lib/modules/$(shell uname -r)/build +PWD := $(shell pwd) + +.PHONY: default modules clean help + +default: modules + +modules: + $(MAKE) -C $(KDIR) M=$(PWD) modules + +clean: + $(MAKE) -C $(KDIR) M=$(PWD) clean + +help: + @echo "Spanker driver — out-of-tree kbuild module" + @echo "" + @echo "Targets:" + @echo " modules build spanker.ko (default)" + @echo " clean remove build artefacts" + @echo "" + @echo "Variables:" + @echo " KDIR kernel build tree (default: /lib/modules/\$$(uname -r)/build)" diff --git a/src/driver/include/uapi/spanker_ioctl.h b/src/driver/include/uapi/spanker_ioctl.h new file mode 100644 index 0000000..94a006f --- /dev/null +++ b/src/driver/include/uapi/spanker_ioctl.h @@ -0,0 +1,69 @@ +/* SPDX-License-Identifier: GPL-2.0-only WITH Linux-syscall-note */ +/* + * Copyright (c) 2026 PopSolutions Cooperative + * + * Public ioctl ABI for the Spanker accelerator driver. + * + * This header is shared between the in-kernel driver under + * src/driver/ and the Rust userspace runtime (per ADR-001). + * + * Stability: v0 is unstable. The ABI may break between any two + * commits while the major version is 0. The first stable major + * version (v1) lands when ADR-003 (Public interface contracts) is + * accepted and PR #2 ships the runtime that depends on it. + * + * Discoverability: SPANKER_IOC_GET_VERSION lets userspace fail + * cleanly when it sees a kernel driver with an incompatible major. + */ + +#ifndef _UAPI_LINUX_SPANKER_IOCTL_H +#define _UAPI_LINUX_SPANKER_IOCTL_H + +#include +#include + +/* + * ioctl magic byte. + * + * 0xE3 is used here as a placeholder; before any in-tree submission + * (Generation B per ADR-002), this must be reconciled with + * Documentation/userspace-api/ioctl/ioctl-number.rst in mainline. + */ +#define SPANKER_IOC_MAGIC 0xE3 + +/* ABI version reported by SPANKER_IOC_GET_VERSION. */ +#define SPANKER_ABI_VERSION_MAJOR 0 +#define SPANKER_ABI_VERSION_MINOR 1 +#define SPANKER_ABI_VERSION_PATCH 0 + +/* + * struct spanker_version — wire format of SPANKER_IOC_GET_VERSION. + * + * Fields are __u16 to keep the struct exactly 8 bytes regardless + * of architecture or compiler padding choices. Reserved must be 0. + */ +struct spanker_version { + __u16 major; + __u16 minor; + __u16 patch; + __u16 reserved; +}; + +/* + * Smoke-test the ioctl dispatcher. + * + * No payload, no return data; success indicates the driver is + * loaded and the dispatcher reaches this opcode. + */ +#define SPANKER_IOC_PING _IO(SPANKER_IOC_MAGIC, 0x01) + +/* + * Read driver/ABI version. + * + * Userspace passes a writable struct spanker_version; the kernel + * fills it with SPANKER_ABI_VERSION_{MAJOR,MINOR,PATCH} and a + * zeroed reserved field. + */ +#define SPANKER_IOC_GET_VERSION _IOR(SPANKER_IOC_MAGIC, 0x02, struct spanker_version) + +#endif /* _UAPI_LINUX_SPANKER_IOCTL_H */ diff --git a/src/driver/spanker_ioctl.c b/src/driver/spanker_ioctl.c new file mode 100644 index 0000000..c8e1b02 --- /dev/null +++ b/src/driver/spanker_ioctl.c @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2026 PopSolutions Cooperative + * + * Spanker — ioctl dispatcher for /dev/spankerctl. + * + * Implements the v0 ABI declared in + * . The skeleton handles two opcodes: + * + * SPANKER_IOC_PING — no-op; returns 0 on success. + * SPANKER_IOC_GET_VERSION — copies struct spanker_version + * {0,1,0,0} to userspace. + * + * Future PRs add per-device opcodes (buffer alloc/free, work + * submit, completion poll) keyed off /dev/spanker0..N-1, governed + * by ADR-003 (Public interface contracts). + */ + +#include +#include +#include +#include +#include + +#include "include/uapi/spanker_ioctl.h" + +static long spanker_ctl_ioctl(struct file *filp, unsigned int cmd, + unsigned long arg) +{ + if (_IOC_TYPE(cmd) != SPANKER_IOC_MAGIC) + return -ENOTTY; + + switch (cmd) { + case SPANKER_IOC_PING: + return 0; + + case SPANKER_IOC_GET_VERSION: { + struct spanker_version v = { + .major = SPANKER_ABI_VERSION_MAJOR, + .minor = SPANKER_ABI_VERSION_MINOR, + .patch = SPANKER_ABI_VERSION_PATCH, + .reserved = 0, + }; + if (copy_to_user((void __user *)arg, &v, sizeof(v))) + return -EFAULT; + return 0; + } + + default: + return -ENOTTY; + } +} + +const struct file_operations spanker_ctl_fops = { + .owner = THIS_MODULE, + .open = stream_open, + .unlocked_ioctl = spanker_ctl_ioctl, + .compat_ioctl = compat_ptr_ioctl, +}; diff --git a/src/driver/spanker_main.c b/src/driver/spanker_main.c new file mode 100644 index 0000000..37978ea --- /dev/null +++ b/src/driver/spanker_main.c @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2026 PopSolutions Cooperative + * + * Spanker — main module file. + * + * Per ADR-002 (out-of-tree kbuild kernel module), this file owns + * module init/exit, the singleton control character device + * /dev/spankerctl, and PCI driver registration. + * + * The skeleton creates /dev/spankerctl unconditionally so userspace + * can verify the driver is loaded and read the v0 ABI even when no + * Sails are physically attached. Per-Sail device nodes + * /dev/spanker0..N-1 are added by a follow-up PR once the PCI probe + * path is wired up against real silicon. + * + * The PCI driver registration uses placeholder vendor/device IDs + * (0xDEAD/0xBEEF). PopSolutions has not yet been assigned a PCI + * vendor ID; the follow-up issue cited in PR #1's description tracks + * reconciliation before silicon ships. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "include/uapi/spanker_ioctl.h" + +#define SPANKER_DRIVER_NAME "spanker" +#define SPANKER_CTL_DEV_NAME "spankerctl" + +/* + * Placeholder PCI IDs. Must be replaced before silicon; tracked by + * the follow-up issue cited in the PR #1 description. + */ +#define SPANKER_PCI_VENDOR_PLACEHOLDER 0xDEAD +#define SPANKER_PCI_DEVICE_PLACEHOLDER 0xBEEF + +/* Defined in spanker_ioctl.c. */ +extern const struct file_operations spanker_ctl_fops; + +static const struct pci_device_id spanker_pci_ids[] = { + { PCI_DEVICE(SPANKER_PCI_VENDOR_PLACEHOLDER, + SPANKER_PCI_DEVICE_PLACEHOLDER) }, + { 0 } +}; +MODULE_DEVICE_TABLE(pci, spanker_pci_ids); + +static int spanker_pci_probe(struct pci_dev *pdev, + const struct pci_device_id *id) +{ + /* + * Skeleton: log probe and accept. BAR mapping, IRQ allocation, + * and DMA-coherent ring setup land in follow-up PRs. + */ + pci_info(pdev, + "spanker: probed placeholder device (vendor=0x%04x device=0x%04x)\n", + id->vendor, id->device); + return 0; +} + +static void spanker_pci_remove(struct pci_dev *pdev) +{ + pci_info(pdev, "spanker: remove placeholder device\n"); +} + +static struct pci_driver spanker_pci_driver = { + .name = SPANKER_DRIVER_NAME, + .id_table = spanker_pci_ids, + .probe = spanker_pci_probe, + .remove = spanker_pci_remove, +}; + +/* Singleton control device. */ +static dev_t spanker_ctl_dev_num; +static struct cdev spanker_ctl_cdev; +static struct class *spanker_ctl_class; +static struct device *spanker_ctl_device; + +static int __init spanker_init(void) +{ + int err; + + err = alloc_chrdev_region(&spanker_ctl_dev_num, 0, 1, + SPANKER_CTL_DEV_NAME); + if (err < 0) { + pr_err("spanker: alloc_chrdev_region failed: %d\n", err); + return err; + } + + cdev_init(&spanker_ctl_cdev, &spanker_ctl_fops); + spanker_ctl_cdev.owner = THIS_MODULE; + err = cdev_add(&spanker_ctl_cdev, spanker_ctl_dev_num, 1); + if (err < 0) { + pr_err("spanker: cdev_add failed: %d\n", err); + goto unregister_region; + } + +#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 4, 0) + spanker_ctl_class = class_create(THIS_MODULE, SPANKER_CTL_DEV_NAME); +#else + spanker_ctl_class = class_create(SPANKER_CTL_DEV_NAME); +#endif + if (IS_ERR(spanker_ctl_class)) { + err = PTR_ERR(spanker_ctl_class); + pr_err("spanker: class_create failed: %d\n", err); + goto del_cdev; + } + + spanker_ctl_device = device_create(spanker_ctl_class, NULL, + spanker_ctl_dev_num, NULL, + SPANKER_CTL_DEV_NAME); + if (IS_ERR(spanker_ctl_device)) { + err = PTR_ERR(spanker_ctl_device); + pr_err("spanker: device_create failed: %d\n", err); + goto destroy_class; + } + + err = pci_register_driver(&spanker_pci_driver); + if (err) { + pr_err("spanker: pci_register_driver failed: %d\n", err); + goto destroy_device; + } + + pr_info("spanker: loaded (ABI v%u.%u.%u, /dev/%s ready)\n", + SPANKER_ABI_VERSION_MAJOR, + SPANKER_ABI_VERSION_MINOR, + SPANKER_ABI_VERSION_PATCH, + SPANKER_CTL_DEV_NAME); + return 0; + +destroy_device: + device_destroy(spanker_ctl_class, spanker_ctl_dev_num); +destroy_class: + class_destroy(spanker_ctl_class); +del_cdev: + cdev_del(&spanker_ctl_cdev); +unregister_region: + unregister_chrdev_region(spanker_ctl_dev_num, 1); + return err; +} + +static void __exit spanker_exit(void) +{ + pci_unregister_driver(&spanker_pci_driver); + device_destroy(spanker_ctl_class, spanker_ctl_dev_num); + class_destroy(spanker_ctl_class); + cdev_del(&spanker_ctl_cdev); + unregister_chrdev_region(spanker_ctl_dev_num, 1); + pr_info("spanker: unloaded\n"); +} + +module_init(spanker_init); +module_exit(spanker_exit); + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("PopSolutions Cooperative"); +MODULE_DESCRIPTION("Spanker accelerator driver (skeleton; PR #1)"); +MODULE_VERSION("0.1.0");