我赌你手机里那些每天刷三小时的App,有一半是用“不安全”的C++写出来的,但你有几次真见它崩了?
内存安全,这词儿最近几年在编程圈里简直像宗教口号。各个门派拿着它当令箭,互相指责对方代码是“筛子”。但吵归吵,代码该崩还是崩,漏洞该出还是出。到底哪种语言能让你夜里睡得踏实,这是个值两毛钱的问题。
安全原教旨主义者画像
总有那么一拨人,眼睛里揉不得半点沙子。他们认定,内存安全是个非黑即白的开关。要么完全安全,要么就是完全不安全。这种二极管思维,在编程语言的论战里格外显眼。
这帮人手里通常攥着一把尺子。尺子上的刻度只有零和一。他们量C语言,零分。量C加加,零分。量Zig,零分。量Rust,嗯……看情况。如果代码里出现了unsafe关键字,那这把尺子就无情地打出了零分。标准极其严苛,严苛到有点不近人情。
但有意思的是,这把尺子在不同场合下,刻度似乎会自己调整。当他们面对自己钟爱的工具链时,尺子上的刻度就模糊了。妥协,让步,这都是成熟的表现。可一旦切换到别的语言,那把尺子又变回了锋利的手术刀。这种双重标准,让旁观者看着都累。
安全原教旨主义者最大的问题,不是他们追求安全,而是他们追求的是纯度。是一种理论上的、无瑕疵的、数学证明级别的安全。他们想要的是完全无菌的隔离仓,却忘了写代码的人总得呼吸。
新来的搅局者
最近冒出个叫Fil-C的项目,像个愣头青一样冲进了战场。它的目标很简单粗暴:让C和C加加的代码,也能自动获得内存安全。原理不算太复杂,它把垃圾回收和一种叫InvisiCaps的指针追踪技术绑在一起。编译出来的程序,一旦有越界访问或者释放后使用,直接崩溃给你看。
这玩意儿确实有两把刷子。它就像给老旧的电线杆子装上了漏电保护器,虽然没法把整个电网换成新的,但至少能避免你摸一下就被电飞。Zig语言的作者一看,这思路不错,立马宣布要在自己的编译器里搞个类似的编译模式,标题还起了个扎眼的说法,大意是“引入一种真正安全(不像某语言)的编译模式”。
这火药味,隔着屏幕都能闻到。Fil-C的作者在社交平台上也没少发表高见,直言不讳表达对Rust生态的不屑。他的逻辑链条很直接:Rust有unsafe关键字,等于开后门,开后门的语言谈不上安全。只有Fil-C这种从底层硬性拦截的方案,才算真功夫。
如果事情到这儿,顶多算是技术路线之争。但问题在于,这股新势力开始将矛头从技术转向了动机。他们开始质问:那些口口声声说在乎内存安全的Rust程序员,为什么不拥抱更安全的Fil-C?如果他们不拥抱,就说明他们爱的不是安全,而是Rust这个玩具本身。这个帽子扣得又大又沉。
非黑即白的陷阱
把安全当成一个绝对属性,这事儿本身就挺荒谬的。好比你说一辆车是否安全,如果它时速超过两百公里就自动熄火,那算安全吗?算。但你会买吗?大多数人不会。
现实世界里的安全,从来都是权衡的艺术。是成本、性能、兼容性、开发效率之间那个脆弱的平衡点。Fil-C带来的代价是具体且沉重的:它与普通编译的二进制文件不兼容,这意味着你不能随便替换系统里的关键动态链接库。它的垃圾回收会引入不确定的暂停,在某些场景下,这种暂停比内存泄漏更致命。性能上,它可能慢上那么几倍。
这些代价,对于浏览器内核、游戏引擎、操作系统底层或者数据库系统来说,几乎是不可承受的。没人愿意为了杜绝一个潜在的内存漏洞,就把整个系统的吞吐量砍掉一半。你电脑里那款3A大作,如果因为内存安全而每帧卡顿三秒钟,你大概会直接把电脑扔出窗外。
相反,对于那些逻辑不太复杂、对性能没那么敏感的工具类程序,用上Fil-C这类技术就非常划算。比如一个图片格式转换器,压缩个文件,慢个零点几秒根本没人察觉。但你不能因为勺子喝不了汤,就说勺子是个废物。每种工具都有它适合的战场。
用数字打破圣战
与其在道德高地上互相扫射,不如低头看看真实世界的数据。Google的Android团队公布过一组数字,在超过五百万行Rust代码里,只发现了一个潜在的内存安全漏洞。按照每百万行代码计算,密度是零点二。
同样的项目里,C和C加加的历史数据是多少呢?大约每一百万行,就有接近一千个内存安全相关的问题。这根本不是一个数量级的比较。差了三个数量级,也就是一千倍的差距。
当然,有人会说,Rust的项目都还比较新,跑的年头短,暴露的问题自然少。这话有一定道理,但也不全对。新项目本身也意味着更少的遗留债务和更现代的架构,这本身就是优势。但无论如何,零点二对比一千,这组数字本身就在大声说话。
如果安全原教旨主义者连零点二这个数字都无法容忍,认为只要是大于零就是不可接受的,那他们大概只能去用形式化验证过的Coq语言写操作系统了。但那玩意儿写出来的系统,可能得等到下个世纪才能开机。极致的纯粹,往往通向的是极致的无用。
崩溃与漏洞之间
Fil-C把越界访问变成了程序崩溃。这在安全角度,确实是巨大的进步。一个拒绝服务的漏洞,总比一个能让人远程执行任意代码的漏洞要好得多。防火墙能把大部分攻击者挡在外面,但如果一个非法访问就能让你的服务进程挂掉,这依然是运维人员的噩梦。
如果你运营着一个需要百分之九十九点九九九可用性的在线服务,你会愿意看到集群里的机器因为偶发的内存访问错误而频繁重启吗?崩溃虽然比被黑好,但它依然是一种故障。修复故障需要人,人需要时间,时间就是金钱。
更何况,在不少攻击场景里,让程序崩溃本身就是攻击的一环。比如通过不断触发崩溃,耗尽系统的看门狗资源,或者利用崩溃后的重启状态,绕过多因子认证。安全是个系统工程,不是一个内存检查器能包打天下的。
从这个角度看,Rust的“大部分安全”策略,显得务实得多。它在编译阶段就拦截了绝大多数问题,让有问题的代码根本跑不起来。对于剩下那一点点需要绕过检查的高危操作,用unsafe明确标记出来,让审查变得有迹可循。这种做法,像是一个精明的工程师,而不是狂热的传教士。
谁在害怕现实
回到那个经典的挑衅:如果你真在乎安全,就该抛弃Rust,投奔Fil-C。这个说法听起来铿锵有力,但仔细一想,它忽略了一个最基本的事实:世界上绝大多数项目,根本用不着在Fil-C和Rust之间做选择。它们甚至连内存安全这个词都没怎么听说过。
Facebook的HHVM虚拟机,或者MySQL的存储引擎,它们确实需要极致的内存控制,Fil-C的垃圾回收可能会拖慢它们的步伐,Rust的借用检查器可能会让它们的架构师抓狂。但它们毕竟是少数。
更多的项目,是那些管理后台的API服务,处理订单的中间件,或者推送通知的小工具。这些代码用Go写,用Java写,甚至用Python写,同样活得很好。它们依赖的是垃圾回收机制,或者干脆是解释型语言自带的内存管理。内存安全对于它们来说,就像是汽车的安全气囊,平时根本感受不到存在。
所以,当安全原教旨主义者只盯着Rust的unsafe说事儿,而对海量用Python写的、可能藏着无数SQL注入漏洞的代码视而不见时,他们真正的动机就很值得玩味了。他们可能不是真的害怕内存漏洞,他们只是讨厌隔壁那栋楼里的邻居。
实用主义者的生存法则
真正在泥地里搬砖的开发者,心态往往更平和。他们知道,没有银弹。选择一种技术,意味着接受它的全部优缺点。他们愿意用Rust的编译时长,去换取运行时不会突然爆炸的安心感。他们也愿意接受在某些冷门平台,Rust的生态还不够丰富。
他们也会密切关注Fil-C这类项目,心里默默盘算,明年重构那个老旧的C模块时,要不要试试这个新工具。他们不站队,他们只解决问题。他们的工具箱里,锤子、扳手、电钻都有,他们不会因为喜欢电钻,就去砸烂所有的锤子。
对于他们来说,“内存安全”是一项重要但非唯一的指标。代码的可读性、社区的活跃度、库的完善程度、甚至招聘的难易度,都是需要放在天平上称量的砝码。在绝大多数的业务场景里,写出逻辑正确的业务代码,远比纠结一个指针是否悬空要难得多。
安全原教旨主义者把记忆体错误当成了万恶之源,这在他们的世界里或许是对的。但在真实的软件工程世界,逻辑错误、需求变更、产品经理拍脑袋的决定,这些带来的破坏力,远比一个段错误要可怕得多。
所以,下次再有人用“你的语言不安全”来指责你时,你可以微笑着告诉他:是的,它不完美,但它能让我在周末晚上安安稳稳睡觉,而不用爬起来修服务器。
这份从容,比任何形式的绝对主义都来得更有力量。
“如果你需要纯手工打制一把绝对安全的椅子,那你最好有在地上坐一辈子的觉悟。”