- 开发工具
- IDE
- 调试器
- 代码质量
【免费下载链接】cangjie_deveco_plugins
导读
本文基于 cangjie_deveco_plugins 仓库中 cjlint 静态检查工具所内置的编码规范文档,深入讲解仓颉(Cangjie)并发编程中的一条基础安全规则G.CON.01「禁止将系统内部使用的锁对象暴露给不可信代码」。文章既给出可直接落地的正例与反例代码,也结合本仓库 cjlint 引擎的源码实现,说明该规则在 IDE 静态检查链路中如何被解析、映射与执行。读完本文,你将掌握仓颉锁对象的正确封装方式,理解公钥锁导致的条件竞争与拒绝服务(DoS)攻击原理,并了解如何通过 cjlint 的规则文档、忽略注释与自定义配置来管理该类缺陷。
一、规则背景:仓颉的锁与 synchronized
仓颉语言通过synchronized关键字与一个ReentrantMutex(可重入互斥锁)对象配合,对代码块进行保护:被synchronized(mtx) { ... }包裹的代码块,同一时间只允许一个线程进入执行。ReentrantMutex支持同一线程重复加锁,其类型与synchronized均声明于标准库std.sync包中,使用时需要先行导入:
import std.sync.*在该并发模型下,锁对象的可见性与封装方式直接决定了同步保护是否可靠。若锁对象被设计为可被外部代码直接访问的公共成员,那么攻击者或不可信代码就可以绕过类自身提供的受保护方法,直接操纵这把锁,从而破坏互斥语义。
该规则在 cjlint 规则体系中的完整编号为G.CON.01,所属类别为并发(CON)规则族,级别为「要求」,即强制必须遵守的编码约束。规则文档位于仓库 tools-extension/cjlint/src/main/resources/docs/G_CON_01.md,与 G.CON.02 至 G.CON.05 共同构成一组面向锁与线程安全的检查规则。
二、为什么不能暴露锁对象:条件竞争与 DoS 攻击
规则描述明确指出:攻击者可以通过获取该ReentrantMutex对象来触发条件竞争(race condition)与死锁,进而引起拒绝服务(Denial of Service, DoS)。
其攻击链路非常直观:
- 锁对象是类的
public成员,任何持有类实例的代码都能读取到它; - 不可信代码在未获授权的情况下对同一把锁执行
synchronized(so.mtx) { ... }; - 若不可信代码在持锁期间执行死循环或长时间阻塞(例如
while (true)循环),锁将被无限期持有; - 其它线程调用
put()等原本受该锁保护的合法方法时,会在synchronized入口处永久阻塞,导致整个对象服务不可用。
防御这个漏洞的最直接方法,就是使用私有锁对象(private lock):将锁声明为private,使外部代码无法获取锁的引用,从根源上杜绝上述攻击路径。
三、正例:使用 private 锁对象封装同步逻辑
规则给出的正例如下:
import std.sync.* class SomeObject { private let mtx: ReentrantMutex = ReentrantMutex() // CODE public func put(x: Object) { synchronized(mtx) { // CODE } } }关键要点:
private可见性:锁对象mtx被声明为类私有成员,包外乃至类外的代码均无法直接引用它;let不可变性:使用let声明,锁引用在初始化后不可再被重新赋值,避免锁对象被替换而引入新的同步漏洞;- 同步入口收敛:外部代码只能通过
put()这样的公开方法间接使用锁,而无法自行加锁、持锁。
将锁对象设置为private类型后,攻击者无法无限持有锁,synchronized代码块的互斥语义始终由类自身掌控。
四、反例:public 锁对象被不可信代码无限持有
规则给出的反例完整展示了一条真实的攻击路径:
import std.sync.* import std.time.* class SomeObject { public let mtx: ReentrantMutex = ReentrantMutex() ... public func put(x: Object) { synchronized(mtx) { ... } } } //Trusted code var so = SomeObject() ... //Untrusted code func untrusted() { synchronized(so.mtx) { while (true) { sleep(100 * Duration.nanosecond) } } }该反例中有三个值得注意的细节:
public let mtx是漏洞根源:锁对象对外可见,so.mtx在类外可直接解引用;- 不可信代码直接复用同一把锁:
untrusted()与put()竞争同一把mtx,合法业务与恶意逻辑共享同步互斥域; - 死循环持锁:
while (true) { sleep(100 * Duration.nanosecond) }使不可信代码在持锁状态下永不退出,put()的调用线程被永久阻塞。
使用public修饰锁对象时,攻击者可以直接无限持有mtx锁,使得其它调用put函数的线程被阻塞,形成典型的锁占用型 DoS。
五、例外场景
规则同时给出了两类允许放宽约束的例外场景:
- 包私有的类可以不受该规则约束:包私有(包内可见)的类无法被包外的非受信代码直接访问,锁对象即使不是
private,外部代码也没有访问入口; - 非受信代码无法获取执行同步操作的对象的场景:如果不可信代码根本无法拿到执行同步操作的对象实例,那么锁对象自然不可能被滥用,可不受该规则约束。
在团队评审或配置检查白名单时,这两类场景应作为排除依据,避免对本身安全的代码产生误报。
六、规则在 cjlint 静态检查链路中的落地
G.CON.01 并非仅仅停留在文档层面,它是仓颉 IDE 静态检查工具 cjlint 内置规则体系的一部分,本仓库中的 cjlint 模块承载了完整的检查、结果呈现与规则文档关联能力。
6.1 引擎与规则文档的关联机制
cjlint 引擎类 CjlintEngine.java 继承CodeLinterEngineProvider,向 IDE 的 code linter 框架注册名为cjlint的检查引擎,声明支持的仓颉语言标识为cj(见 CodeCheckConstantUtil.java 中的LANGUAGE_CANGJIE)。
引擎在执行检查时,会通过getRuleDocPath()将每条缺陷映射回对应的规则文档:规则编号描述串中的点号(.)会被替换为下划线(_),从而定位到docs/目录下的G_CON_01.md等文档文件(对应 CjlintCodeCheckCmd.java 中的getRuleDocPath()逻辑)。也就是说,开发者点击 IDE 检查结果中的缺陷时,跳转打开的正是本仓库 docs 目录下的这份规范文档,规则说明与代码定位一一对应。
6.2 缺陷级别与严重度映射
每条规则文档首行标注的级别(「要求」或「建议」)会映射为检查结果的严重度。在 CjlintCodeCheckCmd.java 的getInfo()方法中:
- 检查器输出
mandatory级别的缺陷映射为严重度2(对应文档中的「要求」,如 G.CON.01); - 检查器输出
suggestions级别的缺陷映射为严重度1(对应文档中的「建议」,如 G.CON.05 双重锁定检查)。
因此,违反 G.CON.01 会被当作必须修复的强制缺陷呈现,而不仅仅是风格建议。
6.3 缺陷的忽略与屏蔽
对于确属例外场景或历史遗留的违规点,开发者可以使用 cjlint 的忽略注释进行屏蔽。引擎通过正则^([A-Z_]{1,10}_\d{1,5})(见 CodeCheckConstantUtil.java)解析规则编号,将G.CON.01转换为!G.CON.01形式的忽略标记(见 CjLintManager.java 的getIgnoreRule())。
IDE 中的「屏蔽缺陷」操作由 PsiHandler.java 在源码 AST 中插入注释实现:
// cjlint-ignore G.CON.01支持整行级忽略,也支持通过文件首行的// cjlint-ignore -start !G.CON.01与文件末尾的// cjlint-ignore -end成对注释屏蔽整个文件的检查。写入忽略注释的过程会自动规避 IDE 格式化器的干扰,保证注释插入后不破坏原有代码风格。
6.4 规则与自定义配置
规则启停可在工程根目录通过cjlint_rule_list.json的RuleList字段配置,解析逻辑见 CjlintRuleListConfig.java;检查范围则可通过工程根目录的code-linter.json5中的files与ignore字段(glob 模式)进一步筛选,映射类见 CodeLinterJson5Config.java。这意味着团队可以将 G.CON.01 等并发规则纳入强制门禁,在 CI 或 IDE 检查中统一执行。
七、从 G.CON.01 到完整的锁安全规范
G.CON.01 解决的是「锁对象不该被谁看到」的问题,而仓颉并发安全是一个完整体系。与之同族的规则还包括(均位于 tools-extension/cjlint/src/main/resources/docs 目录):
| 规则编号 | 主题 | 级别 |
|---|---|---|
| G.CON.01 | 禁止将系统内部使用的锁对象暴露给不可信代码 | 要求 |
| G.CON.02 | 在异常可能出现的情况下,保证释放已持有的锁 | 要求 |
| G.CON.03 | 禁止使用非线程安全的函数来覆写线程安全的函数 | 要求 |
| G.CON.04 | 避免在产生阻塞操作中持有锁 | 要求 |
| G.CON.05 | 避免使用不正确形式的双重锁定检查 | 建议 |
组合使用这些规则可以形成完整的锁安全防线:
- G.CON.01防止锁被外部滥用(封装层面);
- G.CON.02要求手动加锁(
mtx.lock())时必须在finally中释放,避免异常路径泄漏锁,并明确指出synchronized代码块在异常时会自动释放锁,而mtx.lock()获取的锁不会; - G.CON.03禁止子类将基类的线程安全函数覆写为非线程安全实现,要求可继承类的锁策略必须有详细记录;
- G.CON.04避免在持锁期间执行网络、文件、控制台 I/O 与
sleep等阻塞操作,防止性能退化与死锁; - G.CON.05规范双重锁定检查的使用,必要时以
AtomicOptionReference等原子类型保证 happens-before 关系。
八、编码实践建议
结合 G.CON.01 与上述规则,在仓颉并发编程中建议遵循以下实践:
- 锁对象一律声明为
private,并配合let保持引用不可变,除非类本身是包私有的; - 公开方法只暴露业务能力,不暴露锁对象本身,同步细节全部封装在类内部;
- 优先使用
synchronized代码块而非手动lock()/unlock(),由语言运行时保证异常时的锁自动释放; - 确需手动加锁时,将解锁放入
finally,确保所有异常路径都会释放锁(G.CON.02); - 设计可继承类时明确记录锁策略,并在子类覆写同步方法时继续保持线程安全(G.CON.03);
- 持锁期间避免任何阻塞操作,将网络 I/O、文件读写等移出同步块(G.CON.04);
- 对确属例外场景的代码,使用
// cjlint-ignore G.CON.01显式记录豁免原因,保证静态检查结果可审计。
通过 IDE 中 cjlint 引擎的持续检查(引擎注册见 cjlint.xml),团队可以在编码阶段即时发现此类锁暴露缺陷,并将规则文档作为缺陷详情直接下钻查看,形成「检查 — 定位 — 学习 — 修复」的闭环。
结语
G.CON.01 是仓颉并发安全的第一道防线:一把public的锁,可能让整个对象在攻击者手中沦为拒绝服务工具。通过private封装、合理使用synchronized,并借助 cjlint 的规则体系与忽略机制进行过程管控,开发者可以在仓颉中写出既高效又经得起安全审计的并发代码。本文所述规则文档、引擎实现与配置文件均可在 cangjie_deveco_plugins 仓库的tools-extension/cjlint模块中逐一查阅验证。
- 开发工具
- IDE
- 调试器
- 代码质量
【免费下载链接】cangjie_deveco_plugins
相关推荐
大麦自动抢票工具 ticket-purchase:新手 40 分钟跑通 PC 端,附完整避坑指南
大麦自动抢票工具 ticket purchase:新手 40 分钟跑通 PC 端,附完整避坑指南 ticket purchase 是一个开源的大麦自动抢票工具:
GUI 自动化RPA仓颉 FFI 安全规则 FFI.C.5 详解:禁止访问已释放资源(Use-After-Free)与 cjlint 静态检查实践
仓颉 FFI 安全规则 FFI.C.5 详解:禁止访问已释放资源(Use After Free)与 cjlint 静态检查实践 本篇技术指南围绕 cangjie
开发工具IDE调试器代码质量仓颉代码质量优化插件 cj-code-quality-plugin:cjfmt 格式化与 cjlint 静态检查工具链深度解析
仓颉代码质量优化插件 cj code quality plugin:cjfmt 格式化与 cjlint 静态检查工具链深度解析 仓颉(Cangjie)代码质量优
开发工具IDE调试器代码质量
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考