Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions checks/symbol_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
"""Cross-version KPM symbol compatibility gate.

Usage: symbol_compat.py [extra.kpm dirs...]

Fails when a symbol referenced by any known .kpm (the freshly built demos in
kpms/*/ plus previous-generation .kpm files passed as extra paths) could not
resolve on either backend.

The kpimg export set is read from kernel/kpimg.elf (build it first). Provider
names come from each KPM's own .kpm.export section, which is what the loader
actually consults -- not from its global symbol table, so an ordinary global
helper cannot make an unresolved import pass.

Exit status:
0 every referenced symbol resolves on both backends
1 at least one symbol is unresolvable
2 the gate could not run (no kpimg.elf, or no .kpm inputs found)
"""
import re
import shutil
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
NM = 'aarch64-linux-gnu-nm'
SECTION = '.kpm.export'


def require_tool(name):
if shutil.which(name) is None:
sys.exit(f'required tool not found in PATH: {name}')


def kpimg_exports():
"""Names the kpimg runtime publishes to KPMs (kernel/kpimg.elf)."""
require_tool(NM)
require_tool('readelf')
elf = ROOT / 'kernel' / 'kpimg.elf'
if not elf.exists():
sys.exit('kernel/kpimg.elf missing; build it first')
out = subprocess.run([NM, str(elf)], capture_output=True, text=True).stdout
names = set()
for line in out.splitlines():
parts = line.split()
if len(parts) == 3 and parts[2].startswith('__kp_symbol_'):
names.add(parts[2][len('__kp_symbol_'):])
if not names:
sys.exit('no __kp_symbol_* entries in kernel/kpimg.elf; is it built?')
return names


def lkm_exports():
"""Names the LKM backend publishes (lkm/kpm/symbols.c)."""
src = ROOT / 'lkm' / 'kpm' / 'symbols.c'
if not src.exists():
sys.exit('lkm/kpm/symbols.c missing')
text = src.read_text()
lits = re.findall(r'\{\s*"([^"]+)"\s*,', text)
kfuncs = ['kf_' + m for m in re.findall(r'KP_KPM_KFUNC_ENTRY\((\w+)\)', text)]
names = set(lits) | set(kfuncs)
names.discard('kf_name') # macro parameter artifact, not a real symbol
return names


def export_names(kpm):
"""Return only identifiers emitted by KPM_EXPORT in this object file.

KPM_EXPORT(foo) creates a local section object named
__kpm_export_foo. Ordinary GLOBAL functions are not exports, and a
function explicitly exported by the macro may itself be static.
"""
out = subprocess.run(['readelf', '-sW', str(kpm)], capture_output=True, text=True)
if out.returncode:
raise RuntimeError(f'readelf failed for {kpm}: {out.stderr.strip()}')
prefix = '__kpm_export_'
return {
fields[7][len(prefix):]
for line in out.stdout.splitlines()
if len(fields := line.split()) >= 8
and fields[0].rstrip(':').isdigit()
and fields[6] != 'UND'
and fields[7].startswith(prefix)
}


def elf_syms(path, want):
out = subprocess.run(['readelf', '-sW', str(path)], capture_output=True, text=True).stdout
syms = set()
for line in out.splitlines():
c = line.split()
if len(c) < 8 or not c[0].rstrip(':').isdigit():
continue
bind, ndx, name = c[4], c[6], c[7]
if want == 'und' and ndx == 'UND' and name not in ('', 'UND'):
syms.add(name)
elif want == 'global' and bind == 'GLOBAL' and ndx != 'UND':
syms.add(name)
return syms


def classify(sym, table):
if sym in table:
return 'compat-table'
if sym.startswith(('kf_', 'kv_')):
return 'slot+kernel'
if sym in ('printk', 'kallsyms_lookup_name', 'kallsyms_on_each_symbol'):
return 'slot+kernel'
return 'KERNEL-ONLY'


def collect_kpms():
kpms = {}
for p in sorted((ROOT / 'kpms').glob('*/*.kpm')):
kpms[p] = 'current'
for arg in sys.argv[1:]:
path = Path(arg)
candidates = sorted(path.glob('*.kpm')) if path.is_dir() else [path]
for p in candidates:
if p.is_file():
kpms.setdefault(p, 'previous-generation')
return kpms


def main():
kpimg, lkm = kpimg_exports(), lkm_exports()

kpms = collect_kpms()
if not kpms:
# A clean checkout has kpimg.elf but no built .kpm files. Reporting
# PASS here would make the gate succeed without checking anything.
print('FAIL: no .kpm inputs found (build the demos, or pass a '
'directory containing .kpm files)', file=sys.stderr)
return 2

providers = set()
for p in kpms:
providers |= export_names(p)

rows, failures = {}, []
for p, gen in kpms.items():
for sym in sorted(elf_syms(p, 'und')):
k, l = classify(sym, kpimg), classify(sym, lkm)
if k == 'KERNEL-ONLY' and l == 'KERNEL-ONLY' and sym in providers:
k = l = 'module-export' # provided by another KPM
rows.setdefault(sym, {})[p.name] = (gen, k, l)
if 'KERNEL-ONLY' in (k, l):
failures.append((sym, gen, p.name, k, l))

print(f"kpimg exports: {len(kpimg)} LKM table: {len(lkm)} "
f"KPMs checked: {len(kpms)} KPM providers: {len(providers)}\n")
print(f"{'symbol':36} {'kpimg':14} {'lkm':14} {'gen':20} used by")
for sym in sorted(rows):
for mod, (gen, k, l) in sorted(rows[sym].items()):
print(f"{sym:36} {k:14} {l:14} {gen:20} {mod}")

if failures:
print('\nFAIL: symbols unresolvable without a matching kernel symbol:')
for sym, gen, mod, k, l in failures:
print(f" {sym} ({gen}, {mod}): kpimg={k} lkm={l}")
return 1
print('\nPASS: every referenced symbol resolves on both backends')
return 0


if __name__ == '__main__':
sys.exit(main())
176 changes: 176 additions & 0 deletions checks/test_kpm.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/* Host unit tests for the KPM loader: link arena, symbol resolution, and
* cross-module dependency policy. Build:
* gcc -O2 -Wall -Wextra -o /tmp/test_kpm checks/test_kpm.c && /tmp/test_kpm
*/
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "../kernel/include/kpmlink.h"
#include "../kernel/include/kpmsymbol.h"
#include "../kernel/include/kpmdep.h"

struct fixture {
unsigned char image[2048] __attribute__((aligned(16)));
struct kpm_link_state link;
unsigned long known_slot;
unsigned long zero_slot;
};

static unsigned long compat(const char *name, void *context)
{
struct fixture *f = context;
if (!strcmp(name, "kf_known")) return (unsigned long)&f->known_slot;
if (!strcmp(name, "kf_late")) return (unsigned long)&f->zero_slot;
if (!strcmp(name, "compat_api")) return 0x1000;
return 0;
}

static unsigned long kernel_function(const char *name, void *context)
{
(void)context;
if (!strcmp(name, "late") || !strcmp(name, "new_function")) return 0xffff000001000000UL;
if (!strcmp(name, "printk")) return 0xffff000002000000UL;
return 0;
}

static unsigned long kernel_data(const char *name, void *context)
{
(void)context;
return !strcmp(name, "kernel_data") ? 0xffff000003000000UL : 0;
}

static unsigned long slot(unsigned long value, void *context)
{
struct fixture *f = context;
return kpm_link_pointer(&f->link, f->image, value);
}

static void test_resolver(void)
{
struct fixture f = { .known_slot = 0xffff000004000000UL };
unsigned int size = 256;
struct kpm_symbol_resolution r;
assert(!kpm_link_reserve(&f.link, &size, 8));
#define RESOLVE(n) kpm_symbol_resolve(n, compat, kernel_function, kernel_data, slot, &f, &r)
/* Compatibility table hit with a populated pointer slot. */
assert(!RESOLVE("kf_known"));
assert(r.kind == KPM_SYMBOL_FUNCTION_POINTER && *(unsigned long *)r.address == f.known_slot);
assert(r.address != (unsigned long)&f.known_slot);
/* Compatibility slot present but NULL -> fall back to the kernel. */
assert(!RESOLVE("kf_late"));
assert(*(unsigned long *)r.address == 0xffff000001000000UL);
/* No slot at all: a fresh one is allocated and populated. */
assert(!RESOLVE("kf_new_function"));
assert(r.target == 0xffff000001000000UL);
/* Plain (non-pointer) kernel symbol resolves to its own address. */
assert(!RESOLVE("new_function"));
assert(r.address == 0xffff000001000000UL && r.kind == KPM_SYMBOL_DIRECT);
/* kv_* data pointer slot. */
assert(!RESOLVE("kernel_data"));
assert(r.address == 0xffff000003000000UL);
assert(!RESOLVE("kv_kernel_data"));
assert(*(unsigned long *)r.address == 0xffff000003000000UL && r.kind == KPM_SYMBOL_DATA_POINTER);
/* printk is a pointer variable in the KPM headers. */
assert(!RESOLVE("printk"));
assert(*(unsigned long *)r.address == 0xffff000002000000UL);
/* Non-pointer compatibility entry. */
assert(!RESOLVE("compat_api") && r.address == 0x1000);
/* Unresolvable names. */
assert(RESOLVE("missing") == -1 && !r.address);
assert(RESOLVE("") == -1);
assert(RESOLVE(NULL) == -1);
assert(RESOLVE("k") == -1);
assert(RESOLVE("kf") == -1);
assert(RESOLVE("kf_") == -1);
/* No slot allocator: must report arena exhaustion, not success. */
assert(kpm_symbol_resolve("kf_new_function", compat, kernel_function, kernel_data,
NULL, &f, &r) == -2);
#undef RESOLVE
}

static void test_link(void)
{
unsigned char image[256] __attribute__((aligned(16))) = { 0 };
unsigned int size = 17;
struct kpm_link_state state;
unsigned long plt;
struct kpm_link_slot *s;
unsigned long base = 0x10000000UL;

assert(sizeof(struct kpm_link_slot) == KPM_LINK_SLOT_SIZE);
assert(!kpm_link_reserve(&state, &size, 2));
assert(state.offset == 32 && size == 96);
plt = kpm_link_plt(&state, image, 0xffff000001234000UL);
s = (void *)plt;
assert(s->insn[0] == 0xd503245f && s->insn[1] == 0x58000070);
assert(s->insn[2] == 0xd61f0200 && s->target == 0xffff000001234000UL);
assert(kpm_link_pointer(&state, image, s->target) == (unsigned long)&s->target);
assert(state.used == 1);
/* Slots are deduplicated by target. */
assert(kpm_link_plt(&state, image, 0x4000));
assert(!kpm_link_plt(&state, image, 0x5000));
/* Capacity and image-size bounds. */
size = KPM_LINK_MAX_IMAGE;
assert(kpm_link_reserve(&state, &size, 1) == -1);
size = 16;
assert(kpm_link_reserve(&state, &size, ~0UL) == -1);
/* Branch range check (+-128MB, 4-byte aligned). */
assert(kpm_link_branch_in_range(base, base - (1UL << 27)));
assert(kpm_link_branch_in_range(base, base + (1UL << 27) - 4));
assert(!kpm_link_branch_in_range(base, base + (1UL << 27)));
assert(!kpm_link_branch_in_range(base, base - (1UL << 27) - 4));
assert(!kpm_link_branch_in_range(base, base + 1));
}

static int put_calls;

static void put_cb(void *provider, void *ctx)
{
(void)provider;
(*(int *)ctx)++;
}

static void test_deps(void)
{
void *deps[4];
unsigned int count = 0;
int a = 0, b = 0;
void *pa = &a, *pb = &b;

/* Recording is idempotent per provider. */
assert(kpm_dep_record(deps, &count, 4, pa) == 1);
assert(kpm_dep_record(deps, &count, 4, pa) == 0);
assert(kpm_dep_record(deps, &count, 4, pb) == 1);
assert(count == 2);
assert(kpm_dep_record(deps, &count, 4, (void *)1) == 1);
assert(kpm_dep_record(deps, &count, 4, (void *)2) == 1);
assert(count == 4);
/* Full table must be reported, never silently accepted. */
assert(kpm_dep_record(deps, &count, 4, (void *)3) == -1);

/* Forgetting an edge invokes the callback exactly once. */
put_calls = 0;
assert(kpm_dep_forget(deps, &count, pa, put_cb, &put_calls) == 1);
assert(put_calls == 1);
assert(kpm_dep_forget(deps, &count, pa, put_cb, &put_calls) == 0);
assert(put_calls == 1);
assert(count == 3);
assert(kpm_dep_forget(deps, &count, pb, NULL, NULL) == 1);
assert(count == 2);
/* Remaining entries are the two synthetic providers, order-independent. */
assert((deps[0] == (void *)1 || deps[0] == (void *)2) &&
(deps[1] == (void *)1 || deps[1] == (void *)2) && deps[0] != deps[1]);
assert(kpm_dep_record(NULL, &count, 4, pa) == -1);
assert(kpm_dep_record(deps, &count, 0, pa) == -1);
}

int main(void)
{
test_link();
test_resolver();
test_deps();
puts("PASS: link arena, symbol resolution, dependency policy");
return 0;
}
13 changes: 13 additions & 0 deletions doc/en/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,19 @@ static long my_init(const char *args, const char *event, void *reserved)
}
```

## Cross-KPM Symbol Exports (LKM style)

KPMs can publish symbols to later KPMs the way an LKM uses `EXPORT_SYMBOL`:

```c
int my_helper(int a, int b);
KPM_EXPORT(my_helper);
```

- `KPM_EXPORT` places the symbol in the `.kpm.export` section; later KPMs reference it with a plain `extern`, the loader resolves it by name and records the dependency.
- A module whose exports are still imported refuses to unload (`-EBUSY`), matching `rmmod` in-use semantics; importers release their providers on unload, and a failed load rolls references back.
- Behaviour is identical on the kpimg and LKM backends.

## Loading and Managing Modules

Use the SuperCall API from userspace to manage KPMs at runtime:
Expand Down
13 changes: 13 additions & 0 deletions doc/zh-CN/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,19 @@ static long my_init(const char *args, const char *event, void *reserved)
}
```

## 跨模块符号导出(LKM 风格)

KPM 支持类似内核模块 `EXPORT_SYMBOL` 的跨模块符号导出:

```c
int my_helper(int a, int b);
KPM_EXPORT(my_helper);
```

- `KPM_EXPORT` 将符号写入 `.kpm.export` 段;后加载的 KPM 直接 `extern` 引用即可,加载器按名解析并记录模块依赖。
- 仍被其他 KPM 引用的模块拒绝卸载(返回 `-EBUSY`),与 `rmmod` 的 in-use 语义一致;引用方卸载或加载失败时自动回滚引用计数。
- kpimg 与 LKM 两个后端行为一致。

## 从用户空间加载和管理模块

使用 `supercall.h` 中的 API 在运行时管理 KPM:
Expand Down
Loading
Loading