我第一次注意到 MODULE_LICENSE 这个文件是在做一台测试机的 ROM 合规审计时。当时要求把固件里所有自带模块的许可证信息整理成一张表结果脚本扫到一堆同名文件有的只有十几个字节有的几百 KB有的还是符号链接根本没法直接拿文件名下结论。后来琢磨明白MODULE_LICENSE 不是用来阅读的它是一张通行证是给机器、给发布时间、给审核流程看的。这篇内容就把 MODULE_LICENSE 的展开和检测完整拆一遍覆盖系统目录下那份许可证文件也覆盖内核模块里那个同名宏适合做 ROM 定制、模块开发、内核编译打包的开发者参考。1. MODULE_LICENSE 到底是什么为什么需要“展开”1.1 两个长得一样但性质不同的“MODULE_LICENSE”先别急着写脚本得先分清我们说的 MODULE_LICENSE 到底指哪个。在实际项目里我遇到过两种形态名字完全一样作用完全不一样。形态常见位置文件内容展开后得到的东西许可证文本文件ROM 固件目录、模块压缩包内许可证名称、SPDX 标识或许可证全文可读的许可协议信息内核模块宏声明内核模块源码.c文件MODULE_LICENSE(GPL)这样的宏调用编译后写入.modinfo段的 license 字段这两种形态都会被检测工具扫描。但如果你用处理文本文件的思路去处理宏声明就会漏掉真实信息反过来用解析宏的方式去看文本文件又会把协议正文当成乱码。所以“展开”的第一步是先确定手里是哪一种。文本文件那个“MODULE_LICENSE”在很多系统目录里是作为一种集中声明存在的。厂商把自带模块、预装应用的许可证汇总到一个目录下方便合规审查。而内核源码里的 MODULE_LICENSE 宏则是让内核加载器知道这块模块的许可证类型判断它能不能使用某些只对 GPL 兼容模块开放的导出符号同时也会影响系统启动时是否把内核标记为“污染”状态。1.2 “展开”这个词背后的三层意思把 MODULE_LICENSE 搞清楚不能只靠cat看一眼。我理解的“展开”至少有三层意思缺一层都容易出问题。第一层是内容展开很多 MODULE_LICENSE 文本文件里只写了一个字符串比如Apache-2.0或GPL-2.0并不是完整协议全文。展开就是要从缩写映射到完整协议名必要时再关联到标准协议全文或者关联到模块自带的 LICENSE 文件。第二层是介质展开这个文件可能躺在 system.img 分区镜像里可能被压缩在模块 zip 包里也可能是个符号链接。你得先把它从镜像或压缩包里“取出来”才能谈得上检测。第三层是宏展开对应内核模块的场景。MODULE_LICENSE(GPL)在源码里是一条宏经过预处理器之后会变成一个全局字符串变量最终被链接进 ELF 文件的.modinfo段。展开这个宏就是去 ELF 里把那段字符串找出来而不是停留在源码文本上。所以检测的人如果只做了“读文件”这一步本质上没有完成展开。真正可靠的检测一定是拿到了展开后的、可被机器核对的真实数据。1.3 展开之后你会看到什么举个我实际处理过的例子。某设备固件里/system/etc/MODULE_LICENSE文件内容是这样的Apache License, Version 2.0就这么一句话。而旁边某个硬件加速模块目录下的 MODULE_LICENSE 可能是SPDX-License-Identifier: GPL-2.0-only再看内核模块的场景。写一个测试模块#include linux/module.h #include linux/kernel.h MODULE_LICENSE(Dual MIT/GPL); static int __init hello_init(void) { printk(KERN_INFO hello module loaded\n); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO hello module unloaded\n); } module_init(hello_init); module_exit(hello_exit);编译之后用modinfo能看到license: Dual MIT/GPL这四种结果都叫 MODULE_LICENSE但自动化检测必须把它们归一化成同一套结构化记录比如“模块名、许可证类型、许可证全文位置、声明来源”才能继续做合规判断。这就是展开动作的真实价值。2. 展开操作把许可证信息从不同形态里捞出来2.1 从系统镜像和模块包里展开文本文件先处理最基础的场景拿到一个固件镜像或者模块 zip 包需要把里面所有 MODULE_LICENSE 文件展开成可读清单。如果是运行中的设备直接通过调试桥看adb shell ls -l /system/etc/MODULE_LICENSE* adb shell cat /system/etc/MODULE_LICENSE如果拿到的是镜像先解包再查看。常见做法是先把system.img转成可挂载格式再挂载到临时目录mkdir -p /tmp/system_mnt sudo mount -o loop system.img /tmp/system_mnt find /tmp/system_mnt -name MODULE_LICENSE* -exec ls -l {} \;注意这里别急着cat。先用file看文件类型用readlink看它是不是符号链接file /tmp/system_mnt/system/etc/MODULE_LICENSE readlink /tmp/system_mnt/system/etc/MODULE_LICENSE我遇到过 MODULE_LICENSE 是指向/system/etc/licenses/LICENSE.apache.txt的符号链接如果不解析链接就cat拿到的是“No such file or directory”展开动作就算失败了一半。如果是模块 zip 包先解压再遍历unzip module.zip -d /tmp/module_unpacked find /tmp/module_unpacked -name MODULE_LICENSE* -exec sh -c echo $1 ; cat $1 _ {} \;展开后建议把每条结果输出成“路径 内容前若干字节 文件大小”的记录便于后面检测脚本处理。不要只打印内容因为有些许可证全文很长终端里刷过去就找不到了。2.2 从内核模块二进制里展开 MODULE_LICENSE 宏内核模块的场景不能看源码要看编译产物。因为宏在预处理阶段就已经展开了源码里写着什么并不重要最终加载时以 ELF 里的.modinfo段为准。先给不熟悉宏展开的读者补一句MODULE_LICENSE(GPL)这种宏最终会被展开成一个名为__module_license的静态字符串扩容放到.modinfo段。编译器不会再在代码逻辑里使用这个字符串它只是留给内核模块加载器和 modinfo 工具看的元数据。所以展开内核模块许可证信息有三种办法。第一种用modinfo查已经安装或编译好的模块modinfo /tmp/test_module.ko | grep -i license第二种用readelf直接读.modinfo段readelf -p .modinfo /tmp/test_module.ko输出里会看到类似这样的键值对[ 0] licenseDual MIT/GPL [ 1] vermagic... [ 2] depends第三种用strings从原始二进制里搜关键字strings /tmp/test_module.ko | grep ^license三种方式里readelf -p .modinfo最直观能一次看到模块的所有元信息而且不依赖工具链版本。strings适合在无法获取完整符号信息的场景下做快速筛查。modinfo适合在目标系统上做运行时确认。需要注意有些模块编译时加了CONFIG_MODULE_UNLOAD等配置.modinfo段会被压缩或优化掉字符串搜索会失效。遇到这种情况优先用readelf或objdump -s -j .modinfo看段内容。2.3 用脚本批量展开整个模块目录的许可证信息单文件查看永远效率不够。我平时会写一个 Python 脚本递归遍历目录把所有 MODULE_LICENSE 统一展开成结构化 JSON 记录。脚本核心逻辑包括识别普通文件、解析符号链接、判断文件类型、提取文本内容。下面这个脚本可以原样拿去用简化了输出格式方便集成到后续检测流程#!/usr/bin/env python3 import json import os import sys import hashlib def safe_read_text(path, limit1024): 安全读取文本避免把二进制文件当字符串打印 try: with open(path, r, encodingutf-8, errorsreplace) as f: content f.read(limit) return content except Exception as exc: return fread error: {exc} def expand_one(path): # 先解析符号链接拿到真实路径 real_path os.path.realpath(path) info { path: path, real_path: real_path, is_symlink: os.path.islink(path), size: os.path.getsize(real_path) if os.path.exists(real_path) else 0, sha256: None, } if os.path.exists(real_path): data open(real_path, rb).read() info[sha256] hashlib.sha256(data).hexdigest() info[preview] safe_read_text(real_path) else: info[preview] broken symlink return info def expand_dir(root): records [] for dirpath, _, filenames in os.walk(root): for name in filenames: if name.upper().startswith(MODULE_LICENSE): full os.path.join(dirpath, name) records.append(expand_one(full)) return records if __name__ __main__: root_dir sys.argv[1] print(json.dumps(expand_dir(root_dir), ensure_asciiFalse, indent2))脚本会在输出里带上 sha256 哈希这是给后续检测用的关键字段。两个模块就算文件名不一样只要哈希一致就说明许可证声明来源相同可以在报告里做聚合。3. 检测方法把展开结果变成可执行的红线3.1 检测到底要测哪些点展开只是把数据捞出来检测才是判断数据能不能过关。根据我在发布流程里踩过的坑建议对每一条 MODULE_LICENSE 记录做四个维度检查。第一个维度是类型合法性。展开后的许可证名称必须落在预置白名单里。白名单不能太宽比如把“自定义”也放进去也不能太窄否则会把合法的 BSD、MIT 误杀。白名单内容跟业务强相关商用闭源产品只允许宽松许可证开源产品或内核模块则要允许 GPL。第二个维度是内容一致性。如果模块的声明文件比如模块属性文件里写的是licenseApache-2.0而 MODULE_LICENSE 文件里写的是GPL-2.0这属于明显的内部矛盾必须拦截。我见过不少模块打包时从网上复制模板模板里自带了一份 MODULE_LICENSE作者忘了改结果声明和文件对不上。第三个维度是完整性。文件大小为零或者只有换行符或者符号链接指向了不存在的目标都属于展开不完整。这种检测不能只靠cat要靠文件元数据和哈希。第四个维度是可追溯性。检测报告里至少要体现模块名、许可证类型、文件哈希、来源路径。合规审计的人拿到报告得能回答“这个许可声明是谁在什么时候带进来的”。没有溯源信息的检测结果等于没检测。3.2 一个可以直接用的 check_license.sh下面这个脚本实现了我上面说的四个维度基础版本。它遍历指定目录查找所有 MODULE_LICENSE 文件读取内容并做关键字匹配输出 PASS 或 FAIL。#!/usr/bin/env bash # # check_license.sh - 展开并检测 MODULE_LICENSE 文件 # 用法: ./check_license.sh 目标目录 # set -u TARGET_DIR${1:?usage: $0 target_dir} ALLOW_LISTApache-2.0 MIT BSD-3-Clause GPL-2.0 GPL-3.0 LGPL-2.1 MPL-2.0 # 归一化许可证文本去掉空白、特殊字符小写 normalize() { tr A-Z a-z | tr -cd a-z0-9 } fail_count0 while IFS read -r -d file; do echo $file if [ -L $file ]; then real$(readlink -f $file) if [ ! -f $real ]; then echo [FAIL] 符号链接指向不存在的文件: $real fail_count$((fail_count 1)) continue fi fi size$(stat -c %s $file) if [ $size -eq 0 ]; then echo [FAIL] 文件为空 fail_count$((fail_count 1)) continue fi # 安全读取文本内容 content$(head -c 4096 $file | tr -d \0) norm$(printf %s $content | normalize) matched for lic in $ALLOW_LIST; do lic_norm$(printf %s $lic | normalize) case $norm in *$lic_norm*) matched$lic; break ;; esac done if [ -n $matched ]; then echo [PASS] 匹配到许可证: $matched else echo [FAIL] 未匹配到白名单许可证内容前80字节: $(printf %s $content | head -c 80) fail_count$((fail_count 1)) fi echo done (find $TARGET_DIR -type f -o -type l | grep -i MODULE_LICENSE | sort -z) echo 检测完成: FAIL 数量 $fail_count exit $fail_count这个脚本覆盖了符号链接、空文件、白名单匹配三个核心点。实际项目里建议再加上哈希比对流程先建立一份“官方许可证样板哈希表”然后把每个文件的 sha256 跟样板表比对能解决“同一种协议两种写法”的问题。3.3 检测规则速查表下面这张表是我在项目里常用的白名单配置。注意它不是标准法律意见只是一个工程默认值具体到某个产品时还要结合法务要求调整。许可证标识判定关键字建议策略常见场景Apache-2.0Apache License, Version 2.0, Apache-2.0推荐应用、库、系统服务MITMIT License, MIT推荐轻量库、脚本工具BSD-3-ClauseBSD 3-Clause, BSD-3-Clause推荐工具链、底层库GPL-2.0GNU General Public License v2, GPL-2.0仅限开源/内核场景内核模块、受 GPL 约束的软件GPL-3.0GNU General Public License v3, GPL-3.0谨慎有额外分发义务LGPL-2.1GNU Lesser General Public License v2.1谨慎动态链接库MPL-2.0Mozilla Public License 2.0谨慎文件级许可的项目表格不能替代展开。真正做检测时建议把每一种许可证的官方文本取一个片段作为关键字而不是依赖模块作者手写的声明。手写声明里常见的“Apache 2”这种简写在归一化后也要能正确归类。3.4 集成到构建流程里的两个推荐位置检测脚本只有跑在正确的时机才有意义。我通常在两个位置集成。第一个位置是 ROM 打包前。在生成最终镜像之前先对编译输出的所有模块目录跑一遍展开和检测。这时候发现问题还来得及改等到镜像都做好了再发现许可证缺失重新打包要浪费半个多小时。第二个位置是模块发布流水线里。模块作者上传产物后流水线立刻做一次检测不合格就直接拦截。检测报告随包归档后续审计直接取记录不用重新扫描。CI 里的集成很简单把检测脚本当作一个独立 joblicense-check: stage: test script: - ./check_license.sh out/modules artifacts: paths: - license_report.txt关键是让流水线“失败”不能只是打印警告。如果失败条件设得太宽松等于没有检测。4. 常见问题与排查技巧实录4.1 检测脚本在容器里总是误报“找不到文件”有一次我在容器里跑 check_license.sh明明挂载了目录却总报一堆找不到文件的错误。排查之后发现宿主机上的 MODULE_LICENSE 是指向绝对路径的符号链接比如/system/etc/MODULE_LICENSE而容器里根本没有/system这个目录readlink -f解析出来的路径不存在就被误报成断链。解决方法是先把符号链接指向的相对路径和容器内的实际根目录拼接。更稳妥的做法是检测前定义好“允许的根目录”只解析到目标目录内部的符号链接外部绝对路径一律标记为警告而不是直接 FAIL。这个坑很隐蔽因为本地机器上容器和宿主机目录结构不一致时脚本表现完全不同。建议在检测脚本里先输出解释环境信息echo PWD$(pwd) echo TARGET_DIR$(readlink -f $TARGET_DIR)4.2 符号链接导致的检测误判另一个常见问题跟符号链接有关。模块压缩包里如果包含MODULE_LICENSE - ../licenses/Apache-2.0.txt直接cat MODULE_LICENSE能读到内容检测也能通过。但如果你把这个文件单独复制到另一个目录符号链接目标跟着丢了检测就会 FAIL。更麻烦的是有些打包工具解压时会自动把符号链接替换成普通文件导致原本指向同一份许可证的多个模块变成了多份内容相同但来源不同的副本。这时候从合规角度要重新确认每个副本的原始版本不能只看哈希一致就认为没问题。我处理这类问题的方法是展开阶段保留符号链接信息检测阶段同时记录“目标路径”和“源路径”。只有符号链接已经失效才 FAIL链接有效但指向外部路径时标记 REVIEW由人工判断。4.3 同一个许可证的多种表达方式白名单关键字匹配最怕的是同一个许可证写了三种风格。比如内核模块里常见的MODULE_LICENSE(GPL v2) MODULE_LICENSE(GPL-2.0) MODULE_LICENSE(GPL)这三种在严格匹配下会得到不同结果。但从许可证类型角度看它们都属于 GPL 兼容。如果直接按字符串精确匹配GPL v2 会被放行GPL-2.0 会被放行但 GPL 单独出现时就要小心因为它有可能指 GPL-1.0也可能只是简写。解决办法是建一张“别名到标准 SPDX 标识”的映射表。脚本在归一化之后先查映射表再走白名单。这个映射表需要长期维护我每次从新版本固件里发现一种没见过的写法就往里加现在已经有几十条了。4.4 只看 license 字段不一定等于真实合规内核模块的.modinfo里写了GPL只能说明模块作者声明了这个模块的许可证并不代表模块里链入的每个符号、每个静态库都满足 GPL 要求。真正严格的合规检测还要结合源码扫描和二进制依赖分析。工程上的折中做法是对每个模块检查它是否使用了 GPL-only 的导出符号。内核源码里这些符号通常用EXPORT_SYMBOL_GPL导出如果在模块的未定义符号表里出现而模块又声明为非 GPL 许可证那基本可以判定有问题。这个检测要做到半自动很复杂我目前是把它拆成两步第一步用现有脚本过滤明显异常第二步把可疑模块交给人工用readelf -s逐项核对。5. 从“检测通过”到“真实合规”的几条经验5.1 检测工具不是免责声明写再漂亮的检测脚本也只是把数据核对过程自动化了。合规的最终依据是模块使用了什么代码、这些代码来自哪里、有没有完整的许可证文本、有没有保留版权声明。MODULE_LICENSE 检测通过只能说明声明文件没有自相矛盾不能说明其实现没有法律问题。所以我在项目里特别强调“留痕”。每次发布前跑完检测会把所有模块的 MODULE_LICENSE 展开结果每个文件的 sha256检测脚本版本检测时间白名单快照统一归档到一个license_reports/目录。这样即使几个月后被问到某个模块为什么用某个许可也能迅速调出当时的原始数据。5.2 如何设计自己的许可证白名单白名单设计不要拍脑袋。我一开始把白名单设得很严只要是 GPL 就一律拒绝结果很多系统底层库没法集成。后来又放得过宽连“Public Domain”都放进去了法务那边又不同意。现在用的思路是分三层第一层是强制禁止名单比如未标识许可证的“自定义”、有明显专利限制的许可证遇到就直接 FAIL。第二层是允许名单根据产品形态配置。商用系统应用层只允许 Apache-2.0、MIT、BSD-3-Clause内核及底层驱动允许 GPL 及 GPL 兼容许可。第三层是待审名单比如 LGPL、MPL、CDDL脚本先不拦截但要生成 REVIEW 报告人工确认之后再进白名单。这三层配置用一个 JSON 文件管理脚本启动时加载避免每次改需求都要改代码。5.3 后续可以扩展的方向这套展开和检测逻辑可以用在很多地方。我目前正在把它扩展成自动生成整个固件的许可证清单也就是把每个模块的 license 字段、MODULE_LICENSE 文件哈希、上游源码仓库地址统一汇总生成类似软件物料清单的 JSON。以后无论是内部审计还是对外提供合规说明都能直接取材。另一个方向是检测时不再只看本地文件而是反查模块携带的源代码包。如果模块目录里同时有LICENSE.md和MODULE_LICENSE可以把两个文件做一次文本归一对齐进一步降低声明与真实文件脱节的风险。这些扩展不需要多复杂的系统核心还是先把展开做好把检测规则做扎实。我在实际项目里最深的体会是MODULE_LICENSE 看起来是个不起眼的小文件但它跨着文件系统、编译流程、合规审计三个领域。只把它当文本cat一下很多隐患发现不了把它真正展开并对齐到检测规则之后反而能倒逼模块整理自己的来源信息和许可证声明。现在每次集成新模块我都会先跑一遍展开脚本和检测脚本宁可多停一分钟也不想发版后再去补合规说明。
阅读完成 · 觉得有帮助?