资讯动态

Linux内核模块GPL检查机制:从EXPORT_SYMBOL_GPL到Tainted标记

发布时间:2026/10/9 3:19:01 来源:尧图企业网站定制
你有没有试过这种情况花半小时编译好一个内核模块insmod一敲系统日志里直接甩给你一句module uses symbol __class_create from kernel, but is not GPL模块就地阵亡。或者反过来模块明明加载进去了但dmesg里从此多了一个难看的Tainted: P内核背上“不干净”的信用记录。这一切的幕后推手就是 Linux 内核模块体系里的 GPL 许可证检查机制。很多人对 GPL 的印象停留在“开源协议”四个字上但进了内核它远不止一纸声明而是一整套能跑起来的代码逻辑导出符号分等级、模块声明有格式、加载通过的模块还会被内核记账打标。这篇文章我就把这套“许可证检查”链路从头到尾拆开看它到底判断什么、信任什么、又能被什么糊弄过去顺便用三个最小模块把三种典型结局都跑一遍。适合写过或准备写内核模块、但一直对 GPL 相关的报错和 taint 含义一知半解的开发者。1. 先搞清楚内核模块和 GPL 的关系为什么这么拧巴1.1 内核选 GPL 而不是 LGPL是一开始就定下的立场很多做应用开发转过来的朋友容易混淆 GPL 和 LGPL因为两者从字面上看都是“开源”但约束力完全不同。LGPL 允许你的代码与闭源代码链接在一起而不强制开源所以很多基础库为了推广会选 LGPL比如 ffmpeg 就有 LGPL 构建配置只要不链接 GPL 组件就能以 LGPL 方式发布。但 Linux 内核没有走这条路从一开始就锁死在 GPLv2 上。这不是技术能力问题而是立场问题。Linus 在多个场合表达过同一个观点内核模块一旦调用内核核心 API就是基于内核的衍生作品应该受到 GPL 约束。内核社区的主流看法是所谓“通过标准接口写个外围驱动就不算衍生作品”的说法站不住脚因为模块和内核共享地址空间、直接调用内部函数、依赖内部数据结构这和链接一个库没有本质区别。内核既然作为整体以 GPL 发布那调用它的模块也应当以 GPL 兼容许可证发布。这就是为什么在内核源码里你会发现它不像普通项目那样只放一份 LICENSE 文件就完事而是把许可证的要求直接做进了符号导出机制、模块加载器和运行时标记系统里。所以当你加载一个非 GPL 模块时触发的不是某个“法务接口”而是一连串真实的代码分支。1.2 许可证在内核里的三个落点GPL 检查在内核模块体系里其实分三层每一层的作用和检查时机都不一样很多人把这三层混为一谈排查问题时就容易抓瞎。第一层是MODULE_LICENSE宏。这是模块自己写在.c文件里的一段字符串最终会写进.modinfo段。modinfo xxx.ko能看到它的值。它是内核判断模块“是否 GPL 兼容”的第一依据也是最关键的依据。第二层是内核符号导出分类。内核函数通过EXPORT_SYMBOL或EXPORT_SYMBOL_GPL暴露给模块前者是公共符号谁都能用后者是 GPL-only 符号非 GPL 模块引用了就过不了加载检查。第三层是 taint 标记。内核在加载非 GPL 或 out-of-tree 模块后会设置/proc/sys/kernel/tainted的对应位dmesg里也会显示Tainted: P O之类的字样。这相当于“信用记录”不影响你继续使用但在内核维护者眼里这个内核已经不算“干净”了。这三层的关系可以这样理解模块声明 license内核按声明决定是否允许它引用 GPL-only 符号加载成功后按模块性质记账打标。其中第一层是核心因为后两层都以模块的 license 声明作为输入。2. 内核到底是怎么做许可证检查的2.1 符号分两类公共符号和 GPL-only 符号先看include/linux/export.h里的宏定义内核源码中所有导出符号最终分成两种#define EXPORT_SYMBOL(sym) __EXPORT_SYMBOL(sym, ) #define EXPORT_SYMBOL_GPL(sym) __EXPORT_SYMBOL(sym, _gpl)两者编译后会被放进不同的 sectionGPL 导出的符号放在__ksymtab_gpl段普通导出的放__ksymtab段。模块加载时内核会检查模块引用的符号落在哪个段再结合模块自身的 license 决定放行还是拒绝。看几个典型例子导出方式典型接口说明EXPORT_SYMBOLprintk、kmalloc、register_chrdev、mutex_lock、proc_create、ioremap基础但“通用”的接口闭源模块也在用EXPORT_SYMBOL_GPL__class_create、device_create、tracepoint_probe_register、synchronize_rcu、大量security_*钩子设备模型、RCU、tracepoint、安全框架等“内核魂魄”接口你会发现一个规律越接近内核核心机制的接口越倾向用EXPORT_SYMBOL_GPL像设备模型、RCU、tracepoint、安全框架这些都被视为内核的“内部器官”内核社区不希望闭源模块把它们当作普通 API 来调。反倒是printk、kmalloc这类极其基础、历史包袱重的接口一直保持着普通导出闭源驱动也要依赖它们才能完成最基本的操作。2.2 加载模块时内核到底在查什么从用户态来看insmod xxx.ko只是敲一条命令但内核里走的是一条不短的链路insmod触发finit_module系统调用进入内核后由load_module处理其中和许可证检查直接相关的是符号解析阶段。模块里引用的每个外部符号都会经过find_symbol查找过程中调用check_symbol做校验。这段逻辑的简化版是这样的static bool check_symbol(const struct symsearch *syms, struct module *owner, unsigned int symnum, void *data) { struct find_symbol_arg *fsa data; if (!fsa-gplok) { if (syms-license GPL_ONLY) goto fail; } if (syms-license UNUSED_SYMBOL) goto fail; return true; fail: pr_warn(%s: module uses symbol %s from kernel, but is not GPL.\n, fsa-mod-name, kernel_symbol_name(...)); return false; }注意这里有一个名为fsa-gplok的布尔值它就是前面说的“第一层判断”的结果——模块的 license 声明是否 GPL 兼容。如果模块 license 不兼容而它引用的符号恰好在GPL_ONLY集合里check_symbol直接返回 false符号解析失败模块加载被拒。不同内核版本这个函数的实现位置和命名有差异但核心逻辑十几年没变过。你看到的报错信息里符号名可能从__class_create变成class_create_phys但错误文案永远是那句经典的module uses symbol ... but is not GPL。2.3 那行 MODULE_LICENSE 为什么能“说了算”很多人第一次看license_is_gpl_compatible的实现时会愣住因为检查方式极其原始——纯字符串匹配int license_is_gpl_compatible(const char *name) { if (!name) return 0; if (strcmp(name, GPL) 0) return 1; if (strcmp(name, GPL v2) 0) return 1; if (strcmp(name, GPL and additional rights) 0) return 1; if (strcmp(name, Dual MIT/GPL) 0) return 1; if (strcmp(name, Dual BSD/GPL) 0) return 1; if (strcmp(name, Dual LGPL/GPL) 0) return 1; if (strcmp(name, Dual MPL/GPL) 0) return 1; return 0; }看到没有只要模块里的MODULE_LICENSE字符串恰好是这些值之一gplok就会被置为 true模块就可以引用所有 GPL-only 符号。内核不会去检查你的源码是否真的开源不会去验证你公司是否真的打算遵守 GPL甚至不会警告你“你确定吗”。所以标题里说的“绕开 GPL”用更严谨的说法是让内核的字符串匹配成立。这种信任模型是典型的“法律声明 事后追责”——内核只负责在技术上尊重你的声明至于声明的真实性它默认由法律来管。这也解释了为什么社区对“声明 GPL 但不开源”的驱动极其反感因为这在技术上无法识别却在法律上构成了明显的 GPL 违约。另外还有个细节编译阶段modpost工具其实会提前扫一遍模块引用的符号。它发现你 license 是Proprietary却引用了 GPL-only 符号时会打出WARNING: modpost: ... is a GPL-only symbol。但编译产物照样生成真正的拦截发生在加载时。3. 最小实验三个模块三种结局3.1 准备一个干净的内核模块工程理论说再多不如跑一遍。先准备环境以 Ubuntu 22.04、内核 5.15 为例sudo apt install linux-headers-$(uname -r) build-essential然后做一个标准的内核模块工程文件结构如下obj-m gpltest.o KERNEL_BUILD : /lib/modules/$(shell uname -r)/build all: make -C $(KERNEL_BUILD) M$(PWD) modules clean: make -C $(KERNEL_BUILD) M$(PWD) clean模块代码先留空后面根据场景替换。注意一点如果发行版开启了强制模块签名Ubuntu 默认开自编译的未签名模块加载时会报Required key not available之类的错误。为了专注验证 GPL 机制本身建议在虚拟机里用一个自编译内核或者按发行版文档配好 MOK 签名。签名检查和许可证检查是两条独立链路后面专门说。3.2 场景 A闭源声明只用公共符号——能加载但背了 taint第一个场景模拟一个只用公共符号的闭源模块。用最经典的字符设备注册接口#include linux/module.h #include linux/kernel.h #include linux/fs.h #define DEVICE_NAME gpltest static int major_no; static int __init gpltest_init(void) { major_no register_chrdev(0, DEVICE_NAME, NULL); if (major_no 0) return major_no; pr_info(gpltest: register_chrdev ok, major%d\n, major_no); return 0; } static void __exit gpltest_exit(void) { unregister_chrdev(major_no, DEVICE_NAME); pr_info(gpltest: unregistered\n); } module_init(gpltest_init); module_exit(gpltest_exit); MODULE_LICENSE(Proprietary);注意这里用的是register_chrdev它是EXPORT_SYMBOL导出的公共符号所以加载流程不会触发 GPL 拦截。编译、加载make sudo insmod ./gpltest.ko dmesg | tail -5结果是能正常加载dmesg里能看到gpltest: register_chrdev ok, major240。但如果你看一眼系统的 taint 状态cat /proc/sys/kernel/tainted如果加载前是 0这时会变成 4097。拆开看1 是TAINT_PROPRIETARY_MODULE表示加载了闭源模块4096 是TAINT_OOT_MODULE表示加载了非内核树内模块。自编译模块天然就是 out-of-tree所以 4096 无论如何都会加上。启动日志里会显示Tainted: P OP 对应闭源O 对应 out-of-tree。这个场景说明一件事闭源模块并不是不能用只要避开 GPL-only 符号内核完全允许你加载代价是背一个 taint 标记。3.3 场景 B闭源声明碰了 GPL-only 符号——直接拒绝加载第二个场景模拟一个碰了 GPL-only 符号的闭源模块。把代码换成创建设备类的写法#include linux/module.h #include linux/kernel.h #include linux/device.h static struct class *cls; static int __init gpltest_init(void) { cls class_create(gpltest_cls); if (IS_ERR(cls)) return PTR_ERR(cls); pr_info(gpltest: class created\n); return 0; } static void __exit gpltest_exit(void) { class_destroy(cls); pr_info(gpltest: class destroyed\n); } module_init(gpltest_init); module_exit(gpltest_exit); MODULE_LICENSE(Proprietary);先别急着加载编译时就会注意到不同makemodpost会打出一行警告WARNING: modpost: __class_create [.../gpltest.ko] is a GPL-only symbol但仍会生成.ko文件。接下来加载sudo insmod ./gpltest.ko这次直接失败insmod: ERROR: could not insert module gpltest.ko: Invalid module format配合dmesg | tail -5看原因gpltest: module uses symbol __class_create from kernel, but is not GPL.整个模块被拒之门外一个字符都没运行到。原因就是class_create底层调用__class_create而它是EXPORT_SYMBOL_GPL导出的模块 license 却是Proprietarycheck_symbol直接不放行。这个命令细节值得记一下报错里的符号名和实际调用的 API 名可能不一样因为很多内核 API 是宏封装底层导出的可能是另一个函数排查时要以报错信息里的符号名为准。3.4 场景 Clicense 改成 GPL——一行之差从拒绝到通过第三个场景最简单把MODULE_LICENSE(Proprietary)改成MODULE_LICENSE(GPL)其他代码完全不动MODULE_LICENSE(GPL);重新编译再加载make sudo insmod ./gpltest.ko dmesg | tail -5这次加载成功gpltest: class created正常打印。再看 taint 状态cat /proc/sys/kernel/tainted此时数值是 4096只有 out-of-tree 标记没有闭源标记。也就是说仅凭一行字符串模块从“非法引用 GPL only 符号”变成了“合法引用 GPL only 符号”。这就是整个机制最微妙的地方内核信任的是一句声明而不是声明背后的实际开源状态。实际操作中要清醒一点这种改法只解决了技术检查不解决法律问题。如果你的模块实际是闭源的、代码没有按 GPL 发布那声明MODULE_LICENSE(GPL)再加载在法律层面就是明确违约。本文只讨论技术机制不构成任何法律意见商业产品请务必咨询专业律师。4. 避坑要点与常见问题4.1 tainted 标志位逐位读懂taint 机制常被人忽略但它其实是个很实用的排查工具。内核的 taint 状态是一个位掩码记录在/proc/sys/kernel/tainted常见位的含义如下数值dmesg 字母含义1P加载过闭源模块2F使用insmod -f强制加载过模块4S检测到不匹配的 SMP 配置8R使用rmmod -f强杀过模块16M非 GPL 模块使用了 GPL-only 符号旧内核1024C加载过 staging 驱动2048I内核在绕开平台固件 bug4096O加载过 out-of-tree 模块8192E内核开启签名验证但加载了未签名模块16384L发生过错乱锁soft lockup排查问题第一步永远是看这个文件比如一个内核崩溃报告如果 tainted 里有 P 和 O内核维护者第一时间就会怀疑是闭源驱动的问题。反过来如果你自己写的是开源模块就要确保 tainted 里没有非预期的位——比如调试时手滑用了insmod -f后续所有稳定性问题都会被怀疑到强载头上。4.2 模块签名和许可证检查是两条独立链路很多新手把模块加载失败都归结为 GPL 问题但发行版环境下更常见的其实是签名问题。内核的模块签名机制是另一个独立的检查点CONFIG_MODULE_SIG_FORCE开启时任何未签名或签名验证失败的模块都会被拒报错是Required key not available或Module signature verification failed。这两种问题的排查方向完全不同。看到but is not GPL找 license 和符号的问题看到Required key not available找签名和 MOK 的问题。如果你只是想做 GPL 机制实验最简单的办法是把实验环境切到一个自编译内核、编译时关掉模块签名强制或者干脆用qemu虚拟机加载自编译内核避免被发行版的安全策略干扰。4.3 常见问题速查表现象原因处理思路insmod失败module uses symbol X from kernel, but is not GPL模块引用了 GPL-only 符号license 声明不是 GPL 兼容换用普通导出符号或确认项目确实开源后改正MODULE_LICENSE加载后dmesg出现Tainted: P加载了 license 声明为非 GPL 的模块功能通常正常但会失去内核社区的“精选支持”排查问题时要主动说明insmod失败Required key not available发行版强制签名模块未签名配 MOK 或切换到自编译内核编译时modpost警告GPL-only symbol模块 license 与引用的符号不匹配这是预警告真正的拦截在加载时尽早改方案modprobe自动加载失败但insmod能成功modprobe按依赖和配置加载可能有签名或 depmod 问题先用insmod直连观察真实报错再排查depmod4.4 给闭源驱动开发者的一些实在建议如果你确实在做一个不能开源但需要内核模块的商业项目现实的做法是提前做符号依赖审计不要等到加载失败才来改。写一个脚本把模块的未定义符号全列出来逐个在内核源码里查导出方式凡是EXPORT_SYMBOL_GPL的要么绕开、要么设计成由用户态完成。思路一把逻辑尽量往用户态放内核模块只做最薄的那层中转。很多功能用ioctl、mmap、netlink、sysfs就能覆盖真正必须留在内核态的代码并不多。思路二评估能否只依赖EXPORT_SYMBOL的公共符号实现需求。像register_chrdev、proc_create、文件操作集、网络协议栈注册这些历史悠久的接口基本都还是普通导出闭源驱动的可行空间比想象中大。思路三接受 taint 是闭源驱动的“身份标识”不要试图消除它。内核社区对 taint 的态度是“这是你的选择我们尊重但也请别指望我们为 bug 兜底”。很多上游维护者在看到 tainted 内核报告 bug 时第一句话就是“先加载 GPL 模块复现一下”。至于通过声明MODULE_LICENSE(GPL)来通过检查的做法技术上确实是这么走但项目上我不建议任何人把它当作默认策略。内核社区对这类行为的容忍度很低而且一旦你的商业项目做大许可证违约成为竞争对手攻击你的切入点代价远比你省下的那点重构成本大。最后再分享一个经验我在自己做嵌入式商业产品时始终把“内核模块的许可证策略”当成技术架构的一部分来设计而不是一个编译开关。在项目早期就把每个内核 API 的导出类型查清楚列成一张表开会时放到桌面上讨论。这样做的好处是产品做到一半不会突然被某个 GPL-only 符号卡住也不会有人为了赶进度偷偷把 license 改成GPL导致后面整个产品陷入法律被动。内核的 GPL 机制说到底就是为了逼大家做这个思考想明白了系统才能真的省心。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑