日期漏洞影响 Ubuntu 25.10 自动更新

Ubuntu项目组宣布,Ubuntu 25.10系统内置的基于Rust语言开发的uutils版本date命令存在缺陷,导致自动更新功能失效:

部分 Ubuntu 25.10 系统无法自动检查可用软件更新。受影响的机器包括云部署环境、容器镜像、Ubuntu 桌面版及服务器版安装环境。

公告中提供了受该漏洞影响用户的修复指南。安装rust-coreutils包版本0.2.2-0ubuntu2及更早版本的系统存在该漏洞,该问题已在0.2.2-0ubuntu2.1及更高版本中修复。使用apt命令或其他工具进行的手动更新不受影响。

Ubuntu为推进发行版“氧化化”计划,在25.10版本中启用了uutils和sudo-rs,旨在验证基于Rust的工具是否适用于明年四月发布的长期支持版本。LWN于三月对此项目进行了报道

共有 95 条评论

  1. …将被命名为感恩豚鼠

    • 正因如此我才欣喜他们付诸行动!这种事让人望而生畏实属正常,而我作为参与将Rust引入Linux内核的成员之一,对此深有体会。

      • 我对Rust(或C语言)并无成见,但确实遗憾Rust工具集采用MIT许可证而非GPL许可证。不过这已是另一场辩论了…

        • 如今像coreutils这样的软件似乎难以实现商业价值。尤其当其核心目标是完全兼容现有软件时。无所谓。

          • 这剥夺了促使厂商开源嵌入式固件代码的潜在途径。就像Busybox本身或许难以变现,但它成功推动厂商开源了源代码——包括比Busybox本身更有价值的定制内核补丁。

            • 已有BSD许可的Busybox克隆工具Toybox,例如Android系统就采用它。当然所有BSD系统都有各自的核心工具集版本。无论好坏,我认为这艘船确实已经启航了。

            • 采用安全性更高的语言编写嵌入式设备代码具有净收益(并非指核心工具包是主要安全隐患)。若嵌入式设备采用它,我完全不会介意。

        • > 但这完全是另一场辩论了…
          这并非另一场辩论。当前漏洞正是该决策的直接后果。
          若他们愿意采用原始核心工具集的GNU GPL衍生版本,本可将C代码移植至Rust而非从头重写,如此便能避免引入新漏洞。

            • uutils最初只是堆练习项目,而非追求极致效率的1:1替代方案
            • 他们早已知晓该项目尚未通过完整的GNU核心工具测试套件。
            • > 我认为Canonical应为第二次“我们说过KDE 4.0是开发者预览版而非最终用户版本”的局面负责。

              我在KDE 4公告中未见任何表明其为开发者预览版的表述。此说法源自何处?

            • 我认为这是15年(近20年!)前传言的变形。最初是“当时某些开发者评论称发行版应等待点版本发布”(类似表述),逐渐演变成“KDE 4.0实为开发预览版,从未面向最终用户”。不过我理解这种误传的缘由——KDE 4.0发布时争议极大,导致各方都把责任推给对方 :)。

            • > 我在KDE 4的公告中并未看到任何表明其为开发者预览版的声明。这种说法是从何而来的?

              官方公告确实未提及,但此前邮件列表中曾传达过此信息。普通用户未必会阅读这些内容,但主流发行版的打包人员必须知晓此事。虽然现在找不到具体链接,但我清楚记得当时看到某些发行版竟将这个首个.0版本作为主KDE包时有多惊讶。

              今日找到的唯一链接仅摘录了KDE开发者的说明:https://www.osnews.com/story/19145/kde-400-released/

              “但请注意开发者已明确声明:KDE 4.0并非完整的KDE 4,而是奠定基础的版本——底层系统已准备就绪,但用户可见的界面仍需大量完善。”

            • 我刚发现一条关于KDE 4.0尚未完成的官方声明链接:https://commit-digest.kde.org/issues/2007-12-30/

              "Stephan Binner发布了一则关于即将发布的KDE 4.0的提醒(旨在抑制部分用户过分乐观的预期):

              在大家开始发表对KDE 4.0的看法前,请允许我提醒几点:

              KDE 4.0并非KDE4,而是未来数年KDE 4系列中的首个版本(4.0.0甚至不包含错误修复)。"

            • 上述内容均未提及这是开发者预览版,不应被发行版打包,公告本身也未作此声明,反而链接了已随其发布的发行版。据我所知,所谓上游曾公开明确表态的说法纯属篡改历史。

            • > 以上内容均未表明这是开发者预览版,不应被发行版打包。公告本身也未提及此点,反而链接了已搭载该版本的发行版。据我所知,所谓上游曾公开明确表态的说法纯属篡改历史。

              我认为毫无争议的是,KDE团队在正式传达KDE 4.0.0的“质量”预期方面严重失职,包括当时将其吹捧为最新的“稳定版本”。

              LWN对此事件有篇精彩的报道:

              https://lwn.net/Articles/316827/

              值得注意的是,该文引用了Aaron Seigo [1]在4.0.0版本发布标记后不久的发言:

              "KDE 4.0.0是我们KDE4系列的'会吃掉你孩子的版本',而非KDE 3.5的后续版本。尽管许多用户(包括我本人)已将其作为日常桌面环境使用,证明它确实不会吞噬你的孩子,但这确实是KDE4发布体系中的早期阶段。这是'0.0'版本。KDE4中新增软件的数量令人惊叹,我们将坚持开放路线。"

              ..随后他补充道:

              "必须承认,面对下游发行版的所作所为,要保持积极态度实在困难——他们自诩高人一等,却急于在'酷炫发行版'竞赛中抢占尖端技术,最终伤害了我们(他们和我们)的用户群体。希望这次发行版们能暂缓指责,静心反思自身在4.0版本灾难中扮演的不幸角色。"

            • > 据我所知,所谓上游曾公开明确表态的说法实属篡改历史。

              2007年12月30日关于2008年1月11日周五发布的警告竟是篡改历史?我对日期表述和“篡改”一词的理解与你不同。

            • 这与日历日期无关,但若意图传达KDE 4.0是开发者预览版,你提供的所有链接中均未体现。此前宣称的“向任何发行版明确告知KDE 4.0是开发者预览版”的说法毫无依据。包括发布公告在内的所有关键场合均未出现此类警示。

            • 我认为主流桌面环境的打包者理应掌握比仅阅读发布公告更全面的信息。当时作为关注用户的我都比你更了解情况。

              但愿我使用的软件包没有出自你之手。

              这是我在本讨论串的最后回复。看来我们无法达成共识。

            • > 我认为主流桌面环境的打包人员理应比仅阅读发布公告者更了解情况

              对此我并无异议。我明确提及发布公告,但未将评论局限于此。若有任何公开声明明确表明KDE 4.0仅作为开发者预览版发布,我乐意改变观点。迄今为止,我尚未发现此类证据。

              > 希望我使用的软件包没有出自你之手。

              这种轻蔑实属多余,不过对你而言是个好消息——我已不再参与软件包维护工作。

            • 当时我正参与Fedora的KDE特别兴趣小组(个人从4.0.9版本开始使用)。据我所记,我们当时很清楚这是实验性项目(它曾与KDE3并行打包过一段时间)。

            • > uutils最初只是堆实践项目,并非以最优效率打造1:1替代品的极致优化方案

              我想特别强调:Canonical选择用uutils替代coreutils的决策,以及由此产生的任何后果——无论好坏——都应归因于Canonical的工程决策,而非uutils开发者。他们最初只是出于兴趣完成“我的首个Rust项目”,而非打造“企业级”替代方案。我绝不愿因他人将个人项目发布至网络,就因其被他人作为依赖库而遭到“不够好”的无端指责——这终将导致我们自相残杀。

            • 是的,Canonical似乎奉行“快速行动,打破常规”的信条——若经营的是以剥削用户、损害其心理健康为目的的寄生式数据采集公司,这或许可行;但对操作系统而言绝非良策。

        • 某种程度上,这种情况不一直存在吗?就连glibc都采用LGPL而非GPL。我与你一样始终推崇左抄本许可,但回望过去,当初满怀热忱采纳时预期的方向并未实现。

          有时是因为公司根本无视这些规定。例如,我惊讶地发现微软InTune随附的隧道软件只是重新打包的GPL软件,他们未经署名就重新分发。相反,他们竟在毫无权限的代码上强加了惯用的限制性微软商业许可。我之所以发现此事,是因为Linux版本采用某种无法运行的容器形式。拆解后才揭开真相,最终用经过适配的Debian软件包替换,这些包能及时接收安全更新。苹果则另辟蹊径,彻底规避了左抄权。多数应用商店也引导用户走向这条道路。

          无论哪种方式,事实证明左抄权并未迫使这些公司进行回馈。最令人恼火的规避案例或许是英伟达的内核模块。我无法判断其合法性——据我所知尚未经过法律检验——但无论如何,这确实揭示了左抄权的无效性。

          与此同时,其他公司确实在进行回馈。数量相当可观。有时是因为它们将开源作为营销手段,但更多时候是迫于无奈。内核、GCC和Rust等开源项目发展速度极快,维护补丁的成本过高,因此它们选择将补丁提交给上游项目。

          颇具讽刺意味的是,实际采用左抄权许可与宽松许可的用户群体,竟与我天真设想的完全相反。最热衷使用AGPL及其衍生协议的恰是商业用户——他们试图阻止其他商业用户贩卖其成果。与此同时,开源世界正逐步转向宽松许可。Rust和JS生态便是典型例证。这两个生态似乎并未因此受损,反而获得了海量代码贡献。

          • > 无论如何,左抄权显然未能迫使这些公司回馈贡献。

            • > Linux之所以能超越BSD,恰恰是因为企业进行了大量回馈。

              是的,我认同这点。但你似乎在暗示这是因为GPL迫使他们回馈。

              他们当时有两个选择——要么无义务地向BSD贡献代码,要么选择Linux,而GPL实质上强制这些资本主义实体免费分享成果。你这是在说他们选择Linux是因为它强加了这种义务?

              这种说法难以令人信服,尤其考虑到其他可能的解释:其一是当时AT&T对BSD的法律威胁;其二是Linus始终让向Linux贡献代码比BSD更便捷;其三是BSD对其端口树管控严密,而Linux将此权限下放给GNU和发行版,这才催生了OpenWrt和Alpine等项目。林纳斯对新理念的包容至今仍在发挥作用——Rust语言的接纳便是明证。这种开放生态更易接纳企业为满足新需求而提出的定制化修改。但公平地说,我无从知晓真正驱动力何在,唯独确信与GPL无关。

              无论如何,当这些重量级贡献开始涌现时,Linux支持的硬件范围之广使其成为比BSD更具吸引力的基础平台——无论是否采用GPL。开发节奏加快与内核API不稳定导致维护补丁变得极其繁重,因此无论是否遵循左抄权原则,贡献上游工作都符合开发者的利益。

              正如我所言,若观察当前贡献给宽松许可项目的大量代码(我怀疑其总量甚至超过左抄项目,仅因数量庞大),左抄许可对开源的重要性已不复当年。若企业真需被强制贡献回馈,为何我们拥有TypeScript或Cassandra?我怀疑这对内核也未产生实质影响。

            • > 你似乎在说他们选择Linux是因为它强加了这种义务?

            • > 但选择Linux的开发者必须持续回馈……数十年间这些贡献终将累积。

              你误解了我的观点。他们选择Linux并非因GPL协议,而是出于其他原因。如今我认为正是这些原因使Linux超越BSD取得成功,而非GPL本身。不过我同意,GPL强制要求回馈的机制确实形成了良性循环,一旦启动就加速了整个进程。

              > 有人选择Linux,有人选择BSD

              而选择BSD的人中也有大量回馈者。例如Netflix对FreeBSD的贡献。事实证明,无需GPL也能实现这种回馈。

              > OpenWrt正是诞生于上述机制。

              这确实加快了速度。我怀疑OpenWrt使用那些未提供源代码的二进制内核模块,使得速度提升更为显著。

              众所周知,LinkSys因不认同GPL协议而转用VxWorks系统规避该协议。显然他们对BSD系统更为排斥——尽管BSD免费且包含非GPL用户空间工具,他们仍未采纳。探究背后的原因颇有意思。

              随后正如常有的情况,LinkSys发现开源产品具有市场竞争力。在VxWorks版WRT54G停产多年后,WRT54GL仍可从LinkSys购得(且价格高昂!)。意法半导体旗下esp8266的觉醒之路亦如出一辙——其所有产品最初均为专有设计。促使这两家公司转变方向的并非GPL协议。

              > GPLv3 与 GPLv2 不兼容

              并非如此,前提是使用原始 GPLv2 及其“或后续版本”条款。

            • > 我们知道林克斯不喜欢 GPL,因为他们后来转用 VxWorks 来规避它。

              不,Linksys为后期版本的WRT54G切换至VxWorks是因为其系统要求更低,可使用性能较弱(即更廉价)的硬件[1]。他们继续销售更高规格的硬件作为更昂贵的WRT54GL,据称该产品最终成为“史上最畅销的无线路由器”。

              WRTxx系列(甚至WRT54xx系列)后续产品仍主要(若非完全)基于Linux。

              这显然不是试图规避GPL的组织会采取的行动…

              [1] 采用相同SoC,但内存和闪存减半。

            • > 你的意思是他们选择Linux是因为它强加了这种义务?

              这在经验层面显而易见。尽管BSD拥有巨大先发优势,但Linux最终胜出。

              企业之所以向Linux贡献代码,正是因为他们确信竞争对手无法将这些贡献整合到专有软件中。

              商业模式也反映了这一点。在GPL世界里,常见的是销售支持服务和订阅,在某种程度上也销售双重许可。而在BSD世界里,主流模式是销售专有附加组件和非自由发行版。这些附加组件的自由重实现版本,往往被视为需要解决的问题。

              > 现在回馈给许可宽松的项目的代码量之巨

              曾有十年间,企业围绕宽松的非抄袭左许可展开业务,但这些企业几乎都已失败或转为专有许可。当时贡献的代码量确实庞大,但多数项目现已消亡。MongoDB、Elastic、Pivotal是少数存续者,Cassandra则是个例外。

              这些转变皆源于亚马逊、谷歌等巨头开始向其分叉项目贡献代码。这些分叉项目主要旨在用自由特性替代专有功能,并满足巨头企业追求的高速开发节奏。当时它们被视为重大问题,甚至被指责破坏社会信任。但分叉本应是件好事——大型企业贡献海量自由代码理应受到欢迎。如今这些产品要么完全非自由化,要么仅保留功能阉割的迷你自由版本。

              因此可以安全地得出结论:GPL在实践中具有更强的商业可行性,并直接促成了Linux的巨大成功。

            • 顺便提一句,NeXTSTEP/OPENSTEP或许值得列入你的清单。

              > 这些变化都源于亚马逊、谷歌等巨头开始向其分叉项目贡献代码。

              不过它们的贡献具有选择性。那些被视为竞争优势的功能往往不会回馈开源社区。

            • > 商业模式也反映了这种差异。在GPL生态中,销售技术支持与订阅服务是常见模式

              我认为GPL软件的主流模式并非通过销售支持、订阅或向客户交付源代码获利,而是遵循两种路径:要么内置于路由器、电视、手机或车载收音机(后者离我较远)中,厂商通过硬件盈利;要么通过服务器使用,厂商从软件获利却无需回馈修改代码。采用左抄袭许可证销售软件或订阅服务并未取得巨大成功。ElasticSearch的困境令人记忆犹新,RedHat对Oracle Linux的应对策略亦是如此。

              > 因此可以安全地得出结论:GPL在实践中具有更强的商业可行性,并直接促成了Linux的巨大统治地位。

              Linux在软件领域具有巨大统治地位?作为基础操作系统它确实占据主导地位。但在交付给最终用户的代码行数量上,它仅占极小份额。我的Debian笔记本驱动用户空间的代码行远多于内核。诚然,Debian用户空间多采用左抄许可。但Alpine系统主要采用宽松许可。即便是Fedora,纯左抄许可也仅占30%,混合许可约25%,其余均为宽松许可[0]。

              [0] [https://www.sonarsource.com/blog/the-state-of-copyleft-li…] (https://www.sonarsource.com/blog/the-state-of-copyleft-licensing/#:~:text=截至12月下旬,至少包含部分copyleft组件。)

          • > 令我震惊的是,微软InTune随附的隧道软件竟只是重新打包的GPL软件,

            具体是哪款GPL隧道软件?

      • 我理解替换suid/setgid工具的价值,但想知道非特权工具的安全化实现有何益处?所有安全变更都需要威胁模型支撑,而利用核心工具集并非高价值目标。sudo-rs重写为Rust的案例堪称典范。但据我所知,核心工具集多数组件本就不需要setuid/setgid权限。

        • 非特权工具在各类脚本中仍被root用户频繁调用。若攻击者能构造漏洞(例如创建脚本误触的怪异命名文件),便可获取root权限。

        • 我理解替换suid/setgid工具的价值,但更关心非特权工具的安全化实现有何益处?

          file这类本应稳定运行的工具都存在缓冲区溢出漏洞。在现代环境中,“root”权限已无实际意义:https://xkcd.com/1200/

    • 赞同。关键不在于C版本毫无漏洞——可能并非如此,但谁又能确定?——而在于现有漏洞已被适应性利用。新代码必然伴随新漏洞。

      更令人警醒的是,date命令深处一个微不足道的漏洞竟能悄无声息地破坏无人值守的安全更新!

    • 用Rust重写那些经受数十年实战考验的C工具,从长远看或许是明智之举

      • 既然如此,为何仓库里仍有大量提交记录?简短版本见

        https://gitweb.git.savannah.gnu.org/gitweb/?p=coreutils.git

        显示的最近16次提交甚至不足以追溯一周的开发历史?

        这些工具需要维护,用比C更合理的构建系统重写它们(毕竟C已存在50年),无疑会让维护更轻松。

        至于C语言不会消失的说法,感觉我们已进入这样一个时代:35-40岁以下的年轻人基本不再学习C,因此你可能会惊讶于枯燥成熟的C项目潜在志愿者储备枯竭的速度。

        • > 至于C语言不会消失,感觉我们已身处这样一个时代:35-40岁以下的年轻人基本不再学习它

          我今年32岁,大学时与比我小7岁的同学同窗,当时C语言仍是我们的主修语言。

          虽听说部分院校已削减C语言课程,但它依然存在。

          > 因此那些枯燥成熟的C项目,其潜在志愿者维护者群体可能迅速枯竭的现状,或许会令你惊讶。

          虽然精通C语言的人数可能减少,但真正深谙C语言的程序员比例反而可能提升。自我筛选未必是坏事,我认为C语言专家群体不会大幅萎缩。

          > 这些工具需要维护,若能用比C语言更合理的构建系统重写——毕竟C语言已发展五十年——无疑会让维护工作更轻松。

          C语言在这五十年间已大幅演进,我不确定Rust是否优于C。人们抱怨的多数问题实则源于陈旧的C版本或低质量编译器。尽管最新版GCC仍存瑕疵,但未来数年将持续优化C语言。

          渐进式改进优于彻底转向新语言。Rust的date(1)问题正是例证——与其用新语言编写漏洞,不如持续完善近乎零缺陷的C版本。

          <https://www.joelonsoftware.com/2000/04/06/things-you-shou…>

          • 不仅C语言依然存在,C语言的职位需求甚至超过了Rust(至少在我所在的领域)。

            • 职位需求很大程度上取决于现有代码库的数量,因为它们几乎总是多于新项目。目前没有人会争辩说Rust代码比C代码更多,更有意思的是愿意接受这些职位的人数。

          • 我32岁,大学同学比我小7岁,但C语言仍是我们主修的编程语言。

            恭喜你。我43岁时,大学里试图用Java教所有东西。没错,包括操作系统内存管理这类内容——用没有指针的GC语言教这种东西简直荒谬至极。

            在不破坏现有代码库兼容性的前提下,渐进式改进终究存在局限——这正是人们坚持使用现有语言的首要原因。我想我们都能认同,没人想要一种在兼容性上本质上是全新语言、却仅保留旧名的新语言。

            C语言存在着堆积如山的缺陷,这些缺陷在不破坏兼容性的情况下永远无法修复。

            这种错误在任何语言中都可能发生,包括C语言。开发者启动功能开发后忘记后续处理,绝非能反映语言本质缺陷的典型案例。

            • > 在不破坏现有代码库兼容性的前提下,渐进式改进终究存在局限,

              修复工作缓慢推进却持续破坏旧代码。例如我们已废弃隐式整型声明,也淘汰了K&R函数定义方式。

              近期GCC新增了-Wunterminated-string-initialization警告,用于防范字符数组被初始化为非空终止字符数组的缺陷。

              我们正在讨论为GCC的C语言添加-Wzero-as-null-pointer-constant选项,这同样会导致兼容性中断(因此推进缓慢),但最终很可能被合并。

              ISO C2y(及GCC 16、Clang 21)已引入_Countof()函数,该函数现可统计数组元素数量,并即将支持数组参数计数,这将极大限制数组越界操作的发生。<https://inbox.sourceware.org/gcc-patches/cover.1755161451…>

              > 我认为大家都能认同,没有人希望看到一种在兼容性上本质上是全新语言、却仅保留旧名的新语言。

              就连内核也已转向C语言的新方言。只要破坏性改动循序渐进,便可被接受。某些新诊断机制(如禁止将0作为空指针常量)会破坏现有代码,但若新方言能显著提升安全性,且程序员能相对轻松地处理断裂问题,最终仍会被采纳。

              > C语言存在大量根本性缺陷,若要修复这些缺陷就必然会破坏兼容性。

              这份清单并非如此庞大,部分缺陷已修复,其余正在修复中。诚然某些修复需要破坏性变更,此类变更既已发生,未来仍将持续。

            • C语言的多数缺陷已深深植根于开发者社区,甚至被视作特性。

              虽然表面层面的语法问题或许能逐步修复,但深层架构问题永远无法根治。例如:如何引入类似Rust的Send和Sync特性来防止跨线程错误共享数据?如何消除指针与长度值的分离传递?又该如何修复彻底失效的文本包含系统?

            • > 例如如何引入类似Rust的Send和Sync特性,防止跨线程错误共享数据?

              我近期的所有代码都是单线程的,因此无需此类特性,也不便对此发表评论。:)

              > 如何消除指针与长度值的分离传递?

              我在上文评论中已提及。我认为这是数十年来语言最重要的改进之一,即将实现。

              我正在为GCC编写补丁,目前功能已实现,仅需为边界情况添加诊断信息。

              请参阅此链接:<https://inbox.sourceware.org/gcc-patches/cover.1755161451…>。

              核心思路是通过宏封装接受指针和长度的函数,使宏能安全地接收数组并将其分解为正确的指针和长度参数。

              以下是封装strftime(3)的示例:

              #define strftime_a(dst, fmt, tm) strftime(dst, countof(dst), fmt, tm)

              首先说明此封装宏的安全性:

              countof() 要求输入为数组,违反此约束将引发编译时错误。该函数虽已存在,但若传入栈上对象则失效。我正在开发的补丁将使countof()也能处理数组参数,并返回声明的元素个数。

              要实现该功能,必须将所有获取指针和长度的函数进行封装,未封装的调用将成为潜在故障点(类似Rust的unsafe代码)。目标是完全避免或最大限度减少此类调用。

              其次,我将说明如何禁止未封装的调用。

              该方案的核心思想是:strftime(3) 函数应仅允许通过 strftime_a() 进行调用。可在函数原型中添加 [[deprecated]] 属性标记,随后封装函数可借助 _Pragma 在宏内部禁用该诊断提示。此方案需要编译器的配合支持。

              另一种可能更简便的方法是在构建系统中设置脚本:查找所有此类封装函数(若采用统一命名规则如末尾添加_a则可行),检查代码中是否存在对原始函数的直接调用,并将所有违规情况报告出来。

              在上方链接中,你可以找到一个完全无需手动指定指针长度的概念验证程序;它既使用了malloc(3),又向函数传递了数组参数。

              > 你会如何修复这个彻底失效的文本包含系统?

              抱歉,我认为这是个特性。我喜欢它的运作方式。:)

              若您对#include有具体疑虑请提出,但在我看来,文本化特性恰恰使其简单而优秀。

            • > 若您对#include有具体疑虑请提出,但在我看来,文本化特性恰恰使其简单而优秀。

              从软件层面而言:

              – 难以通过“约定”之外的方式隐藏实现细节
              – 若C语言能提供机制,让消费者可使用部分成员而其他字段受限(同时支持手动结构布局场景)将更理想
              – 头文件保护机制是现实需求(#pragma once可能因硬链接、符号链接、挂载绑定等引发问题)
              – 缺乏命名空间(无法预知a.h何时会干扰b.h
              – 采用config.h模式而非更精准的配置选择(参见:相关构建痛点)

              从构建系统角度:

              – 头文件搜索隐式进行;为确保构建真正可靠,应依赖所有搜索路径直至目标文件被找到(而非默认不存在)
              – 难以获取头文件的高级信息:难以判断哪些头文件属于哪个“库”,导致工具无法实现“无需搜索库X,因实际未使用”或“虽包含X的头文件,但因Y隐式提供其头文件而被检索到”等优化
              – 仅因单个函数实现决策变更,便触发全项目范围的config.h缓存刷新

              其中部分问题确实属于“微不足道”或“无人关注的边缘案例”范畴,但我更倾向于采用结构化解决方案。

            • > 从软件层面:

              [… 隐藏细节 …]

              我不喜欢隐藏细节。直接告知用户不要依赖该特性,并使用明确标注为实现细节的命名。这大概属于(至少部分)C程序员眼中的特性范畴。

              > – 头文件保护机制是现实需求(#pragma once可能因硬链接、符号链接、绑定挂载等问题失效)

              这不算大问题。现有代码检查工具能检测头文件保护是否符合规范(若从外部复制粘贴,可直接发现此类错误)。即使不用这些工具,编写脚本检查每个头文件的保护机制是否与其路径名一致也轻而易举。

              > – 缺乏命名空间(你永远无法预知a.h何时会干扰b.h

              这取决于程序员的规范习惯。C语言中的命名空间类似a_foo和b_foo。
              实际中很少引发问题。

              C语言甚至可实现命名空间(通过结构体技巧),但因成本效益不佳而鲜少使用;下划线方案既经济又可靠。

              > 从构建系统角度:
              >
              > – 头文件搜索是隐式的;要实现真正可靠的构建,每个目标文件都应依赖所有搜索路径中的头文件,直到目标文件被确认不存在为止。

              没错。系统头文件可视为稳定,但除此之外,每个目标文件确实需要递归依赖构建系统中包含的所有文件。编译器通过-M -MP选项即可轻松处理。

              > – 难以获取头文件的高级信息:例如哪些头文件属于哪个“库”,这会影响工具判断“你无需搜索库X,因为实际未使用它”或“你包含了X的头文件,但实际找到的是Y隐式提供的头文件”等场景

              iwyu(1) 完全解决了这个问题。详见<https://include-what-you-use.org/>。

              > – 仅因单个函数实现决策变更,便触发全项目范围的config.h缓存刷新

              你厌恶autotools(及其他构建系统)。我也同样厌恶。我采用手写makefile,完全规避此类问题。

            • 我不喜欢隐藏东西。直接告诉用户别依赖这个,用明确标注为实现细节的命名。我想这大概符合(至少部分)C程序员眼中的设计理念。

              我发现“大家都是成年人”这种准则远远不够,无法避免为了向后兼容而保留极其愚蠢的行为。海勒姆定律(Hyrum's Law)确实存在,而我认为最好的解决方式就是从一开始就不提供这些东西。

              > 没错。系统头文件可以假定稳定,但除此之外,每个目标文件都需要递归依赖构建系统中包含的所有文件。编译器通过-M -MP选项就能轻松处理。

              不,并非如此。我并非此意。我的观点是:若在搜索<stdio.h>时你检查/some/user/path/stdio.h是否存在,则你依赖该文件不存在。唯有完全无污染的构建系统才会尝试捕捉此类细节。

              > iwyu(1) 完全解决了这个问题。详见<https://include-what-you-use.org/>。

              该工具会告知你所需的*头文件*。但谁能保证你的pkg-config --cflags调用不再必要?毕竟你已不再使用该工具定位的任何软件包关联头文件?同样地,你凭什么断定头文件frobnitz.h在构建系统中确实需要关联pkg-config --cflags frobdoodle

              > 我使用手写makefile,不存在此类问题。

              那么你是否按TU(技术单元)管理-D标志?

            • > 我的意思是,若在搜索<stdio.h>时你检查/some/user/path/stdio.h是否存在,就意味着你依赖该文件不存在。只有完全无污染的构建系统才会尝试捕捉这类细节。

              我似乎仍未理解。请进一步说明。

              > 凭什么断定你的pkg-config --cflags调用不再必要?毕竟你不再使用该工具定位的任何包关联头文件?

              嗯,明白了。据我所知,这确实无解。不过我认为这并非头文件的重大问题。

              > 同理,凭什么断定头文件`frobnitz.h`在您的构建系统中必须关联`pkg-config –cflags frobdoodle`?

              文档规范。手册页设有LIBRARY章节,此类信息应在此处说明。在 libc 函数中,其呈现形式如下(参见 printf(3) 示例):

              LIBRARY
              标准 C 库(libc,-lc)

              需要 pkgconf(1) 的库应在该部分进行说明。

              > 那么你是否按 TU 基础管理 -D 标志?

              是的。

            • > > 我的意思是,若在搜索<stdio.h>时你检查/some/user/path/stdio.h是否存在,则你依赖该文件不存在。只有完全无污染的构建系统才会尝试捕捉此类情况。

              > 我仍未能理解。请进一步说明。

              假设你有:

              “`
              // gcc -I/home/alx/include main.c

              #include <stdio.h>

              int main(int argc, char* argv[]) {
              printf(“%d args!n”, argc);
              return 0;
              }
              “`

              若直接编译此代码,使用/usr/include/stdio.h并无问题。但若随后创建/home/alx/include/stdio.h,又凭什么让构建系统知道该翻译单元已过时?因为重新编译时会得到不同结果。问题在于-M等工具不会报告这种“依赖于不存在文件”的情况(因为Makefile和ninja都无法表示此类依赖关系)。Hermetic构建能解决此问题,因为它会单独进行资源发现,为编译本身设置隔离环境。

            • $(TU_d): $(builddir)/%.d: $(SRCDIR)/% Makefile $(pkconf_file) | $$(@D)/ $(CC) $(CFLAGS_) $(CPPFLAGS_) -M -MP $(DEPHTARGETS) -MF$@ $<

            • > 仅凭“约定”难以隐藏实现细节

              这其实并不准确。C语言实际上能相当轻松地隐藏实现细节,只要你愿意使用堆分配对象。只需定义如下接口:

              extern foo;

              foo *foo_new();
              foo_status foo_action(foo *);
              void foo_finish(foo *);

              若需增强安全性,可将最后一个参数改为双指针,使foo_finish能“回溯”至调用方并清空其引用(但无法防止调用方另存其他指针——若担忧此问题,可改用弱引用系统实现)。

              若需支持栈分配,则需额外操作:通过foo_xxxx函数指定分配大小,并用foo_init()函数进行初始化。

              基于内置模板(字面模板或有效模板)实现泛型的语言,其实现细节泄露程度远超C语言。

            • 我深知堆隐藏技巧。我指的是GNU Cauldron演讲中讨论的功能:https://www.youtube.com/watch?v=bYxn_0jupaI

              本质上,这是为特定字段提供结构体访问权限以提升性能,同时隐藏其他不应被篡改字段的方法。但同时需要控制ABI布局以满足紧凑性或兼容性要求。

            • 也可以实现部分暴露对象。

              // 公共头文件
              // foo.h

              typedef struct {
              // 公共字段
              int x;
              int y;
              } foo;

              // foo_private.h

              typedef struct {
              foo pub;
              // 私有内容
              } foo_priv;

              通过 foo_new() 函数分配 foo_priv 结构并返回 &(foo_priv).pub;各类 foo_xxx(foo *) 函数可将 foo * 转换回 foo_priv *。用户端获取的是功能受限的 ‘foo’ 结构,实现端可在末尾添加任意内部扩展。此方法可扩展为允许对象在层次结构中任意组合,例如参见Linux源代码中的include/linux/conrtainer_of.h。

              个人而言,我不会采用这种方案。我会直接依赖 API 中的函数获取信息——可通过链接器版本映射和/或重定向机制管理长期兼容性。若需为不同实例实现自定义功能,可提供函数指针结构作为 API 接口,即运行时接口。

            • > 但你可能还需控制 ABI 布局以满足紧凑性或兼容性要求。

              公共字段与私有字段可交错排列以实现最优布局。

        • 显示最近 16 次提交甚至不足以追溯一周的开发历史?

  2. 注意Ubuntu 25.10仍在使用GNU实现“高风险”命令(如cp、mv、rm等)。应尽快移除该依赖,以便在Ubuntu 25.10普及或下个LTS版本临近前,尽早识别数据损坏风险。在Unix系统中复制文件涉及大量边界情况,这些情况因不同文件系统甚至内核漏洞而倍增。

    此外所有uuutil工具都存在SIGPIPE处理的根本性缺陷https://github.com/uutils/coreutils/issues/8919

    此外还存在争议性接口变更,例如提供12种获取sha3值的方式https://github.com/uutils/coreutils/issues/8984

    我衷心祝愿他们成功,但此事需谨慎处理。

    • > 祝他们好运,但此事需谨慎处理。

      我简直无法(想象中)给这条评论点够赞。

      致熟悉1976年电影《网络》的观众:

      “你已触碰Unix的原始力量,_你_必_将_偿_还_!!!”

      Clemmitt

    • 奇怪的是,/usr/bin/false 是指向 Rust 版本的符号链接,而 /usr/bin/true 却是指向 GNU C 版本的符号链接。

      不知这是否是刻意为之。

      (“true”和“false”是bash内置命令,因此/usr/bin下的命令可能并不常用。)

      • Rust尚未成为唯一真理。

      • >/usr/bin/false 是指向 Rust 版本的符号链接,但 /usr/bin/true 是指向 GNU C 版本的符号链接

        uutils-md5sum 最近也出现故障[1],因此像 /bin/true 这样敏感的程序(仅允许返回特定值!)基于已知可靠的实现是理所当然的。

        [1] https://www.phoronix.com/news/Ubuntu-25.10-Coreutils-Make

        • 不过GNU的true在某些情况下会返回非零值。:)

          $ /bin/true; echo $?
          0
          $ /bin/true –help > /dev/full; echo $?
          true: write error: No space left on device
          1

      • all

        • 摘自https://github.com/uutils/coreutils

          > 若不希望构建多调用二进制文件,而倾向于将实用工具分别编译为独立二进制文件,这种做法同样可行。

          我认为这是发行版需要做出的决策。

          • 另请注意,GNU核心工具集可构建为多调用二进制文件。此处的性能测试表明,开销并非Rust特有的问题,而是动态链接器加载多调用二进制文件所链接的完整库集所产生的开销。

            • 我同意,但需说明二进制文件增大的主要原因在于编译时关于panic行为的选项(abort与unwind机制),以及标准库为通用场景设计的实现方式——其中包含冗长的回溯信息和错误输出,这些都导致最终二进制文件体积膨胀。

              我仍然认为这是发行版的决策范畴。这是在更优的调试诊断能力与二进制文件体积之间权衡的结果。

              若尝试使用重新编译的Rust标准库版本并设置panic = abort,多数二进制文件的体积与GNU版本相当(确实并非全部,但部分文件还额外添加了功能)。

      • 没错,许多操作通过创建指向/bin/true的符号链接禁用了.d目录下的脚本。

        由于我们在多调用二进制文件中通过argv[0]进行分派,当工具被符号链接名称调用时,我们便无法找到二进制文件。

        我们现已部署硬链接农场,可在存在硬链接时进行解析,但因需挂载/proc目录而略显复杂。

  3. 有趣的是自动更新似乎被赋予了高优先级。
    对于智能手机成瘾者这或许合理(因害怕错过更新),
    但对专业人士而言,即便是安全补丁也无需在一周内获取。

    至于服务器…多数情况下即使涉及极端安全漏洞
    也因其他优先事项而被搁置…从功能冻结区…到冰河期。

    至少这次问题证明测试新Rust代码并非徒劳…
    但仍质疑Rust对所有漏洞是否真能为资深程序员带来实质效益…
    这更像是炒作而非可验证的事实。

  4. 我认为Ubuntu有必要(择期而非当下)针对此次事件发布事故报告/事后分析。

    该问题最初于10月16日(恰好一周前)在https://pad.lv/2127970提交报告,而Ubuntu 25.10发布也恰好是一周前。最初提交者是在自研备份脚本静默失败的背景下提及该漏洞,并于昨日将修复方案提交至候选稳定更新仓库,同时给出了(合情合理的)解释说明为何未将其列为当日紧急级别。

    今晨有人指出该修复破坏了无人值守升级功能。据我观察,直到此时该问题才被追踪为安全漏洞,目前该包已同时发布在(prod)稳定更新仓库和更精简的安全更新仓库中。

    实际漏洞仅在于未实现对date -r <file>命令的支持。该问题https://github.com/uutils/coreutils/issues/8621及其实现支持的拉取请求[https://github.com/uutils/coreutils/pull/8630%5D (https://github.com/uutils/coreutils/pull/8630)均于今年9月12日提交,两天后即经审核合并至主分支。显然,该修复版本晚于Ubuntu快照的任何发布版本。

    最令我惊讶的是该命令竟默默接受了-r参数却毫无反应——实际差异文件([https://github.com/uutils/coreutils/commit/88a7fa7adfa048…] (https://github.com/uutils/coreutils/commit/88a7fa7adfa048dabdffc99451d7aba1d9e6a9b6)) 来看,参数解析器显然支持该选项却未实现相应功能。若该命令能返回参数解析错误,问题本可更早被发现。令人费解的是,参数解析器的实现者竟未添加“if -r, throw ‘todo’”的处理逻辑。但更耐人寻味的是,此问题竟未被静态检测发现。Rust编译器在警告未使用变量方面表现出色。(公平地说,多数C编译器及其他语言也具备此功能,但据观察Rust的警告噪音更小,且我见过更多Rust代码库将此类错误视为硬性失败,远超启用-Werror的C代码库。此外Rust还提供#[must_use]指令,若需彻底规避可采用此方案。) 然而此处实际并不存在未用变量——通过查询标志值即可从解析参数对象中获取该值。

    我思考是否值得设计一种Rust参数解析API:当解析后的命令行标志或参数在代码中从未被使用时,能在编译时触发未用变量警告。现有解析器配合足够智能的代码检查工具或许也能实现类似功能。无论哪种方式,编译时无法检测此类错误都与Rust重写核心工具集的理念相悖——该理念认为工具应承担检查职责,而非依赖开发者编写完美代码。

    我认为Ubuntu和uutils开发者手动审核所有被解析器解析却未实现的参数也极具价值。若此类模式出现一次,很可能并非孤例。

    • 如果他们采用clap的参数指定方式,这种情况很可能被识别为未使用的分支,但为了完全兼容旧接口的行为,这显然不是可选方案。

      • 在未先构建稳健可靠的选项解析器的情况下就实现了uutils?

      • 遗憾的是,常规编译器和clippy似乎都未能捕捉到此问题:[https://play.rust-lang.org/?version=stable&mode=debug…] (https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=b31404a21123a29dd8674def3da79c95)

        我猜从编译器的角度看,该结构体成员被“使用”了——因为它被传递给了宏/派生解析器实现会对其赋值并进行其他操作。因此用户代码未使用该成员的事实并未被标记。

        • 我确实写过不少超出其设计初衷的shell脚本。Python在巧妙运用其特性时表现良好。例如:

          def git(*args):
          return subprocess.check_output((‘git’,) + args, encoding=‘utf-8’).strip()

          对我而言,其核心优势在于无需纠结转义规则。我编写的某些工具仍保留在Bash中,因为它们无需处理复杂的参数传递逻辑。但当你频繁遇到`“${arg[@]}”`这类表达时,就会发现能随时调用元组和规范字符串才是真正的解脱。

          • 根据经验,我用大型语言模型迁移shell脚本到Python的效果相当不错。只需简要描述脚本功能、要求转换为Python,再粘贴原始脚本即可获得良好结果。正如你所言,这类模式非常简单,而LLM擅长此类机械转换。

            shell脚本的优势在于即使在极简系统也能运行,而Python无法保证初始环境就具备运行条件。

          • 我发现不使用shell脚本的主要原因在于处理关联数据块(即结构体类数据)时,shell脚本表现欠佳,尤其在函数返回值方面。

            不过我认为静态Rust二进制文件比Python更适合作为替代方案,因为它无需运行时系统即可稳定运行。

          • 对我而言,最大的好处就是不必纠结转义规则。我写的一些工具至今仍用Bash,因为它们不需要处理复杂的参数传递逻辑。但一旦你频繁遇到“${arg[@]}”这类表达,就会发现真正需要的其实是随手可得的元组和规范字符串。

            啊,我完全赞同。真希望有个支持这种特性的shell。另外我的某些shell脚本可能不够安全——虽然不该在恶意环境中调用,但谁知道什么时候会冒出个“小鲍比·泰布尔”呢。
            shell最打动我的特质(且难以在其他语言中流畅复现)就是管道中进程的组合能力。或许这与我处理的文件/数据类型有关。

            • shell脚本中规范的错误处理很困难,虽然可行,但脚本很快就会变得难以阅读。

              Python的运行时问题确实存在,加之新版本持续涌现的兼容性变更,意味着测试覆盖率必须达到100%——而这几乎不可能实现。

              使用单一Cargo生成的二进制文件进行部署,则简单得多且安全得多。

            • 是否存在编译型语言支持将源代码嵌入二进制文件(作为附加注释区块)?

              我认为多数二进制文件会因此过度臃肿。对于传统C/C++代码,若不了解编译器参数,纯源代码的参考价值有限,但可作为代码考古的起点。

              我手头有工作中的旧二进制文件,其原始源代码及我们添加的补丁都已遗失。

            • RPM通过提供双重源包解决了此问题:*.srpm(便于重新编译)及调试源包(用于gdb等调试器)。

            • 但这仅在安装二进制文件的仓库离线前完成源代码安装时才有效。

            • 我知道C#确实支持将输入源代码嵌入输出二进制文件,至少存在这种方案。当然,它嵌入的是所有预处理器操作后的结果,这对依赖预处理器的语言而言会造成严重问题。

            • 此外,当出现问题时,采用“编辑/运行”而非“编辑/编译/运行”的循环模式更便于调试。我认为在非性能关键的底层实现场景中,编译型语言并不适用。

            • 另一方面,许多导致罕见错误的运行时场景往往出现在特定代码路径中——要让程序运行到该路径本身就非同小可。因此强大的静态检查能在此类场景中省去大量麻烦。

            • 安装程序和系统管理脚本这类典型场景是否也如此?我没有数据支撑,无法断言,但直觉告诉我:使用编译型语言处理这些任务的麻烦远大于收益。

            • 至少自从我开始用Rust处理这类任务后,再没遇到过像以前用shell脚本时那种蠢错误——比如程序在八月和九月会崩溃,因为月份前导零导致月份变为八进制,使得08和09成为无效数字。

            • 那恭喜你。但你用的肯定是个很奇怪的shell:

            • 据我所知,多种语言都将八进制定义为“以0开头的数字”。不记得FORTRAN是否如此,但印象中写八进制字节时确实用三位数表示。我知道十六进制需加0x前缀,但八进制不就是直接用0吗?

              我确实用过“月份加1,超过12则进位”的日期运算逻辑,减法逻辑类似。

              祝好,
              Wol

            • 这种日期运算在1月31日这类日期上会出错(例如)。我是Remind的作者,拥有30多年实战检验过的日期计算经验。:)

    • 背景很有意思,谢谢!

      整个故事在多个层面都存在谬误。此类缺陷根本不该存在,而核心功能仍存在大量缺失的事实,就足以立即否定用它替代全球最大Linux发行版中核心工具集的设想。

      核心工具集有合规性测试套件。替代方案必须先通过这些测试,才能讨论在生产环境中替换的可能性吧?

      不知Ubuntu将用户定位为何种群体,但移除GPL许可代码绝不至于重要到这种程度。

  5. 我好奇的是:自动更新为何依赖date -r而非stat()?难道自动更新是个shell脚本?

    (我以为Ubuntu使用unattended-upgrades——这是调用C++ apt库的Python脚本;或桌面版可能用packagekit——这是调用C++的C语言库。)

    • > 我想知道的是,自动更新为何依赖date -r而非stat()?自动更新程序是shell脚本吗?
      >
      > (我以为Ubuntu使用unattended-upgrades,这是调用C++ apt库的Python程序;或桌面系统可能使用packagekit,这是调用C++的C语言程序。)

      在将产品支持扩展至基于*.deb的发行版时,我深感震惊——Debian/Ubuntu的打包逻辑竟如此依赖过时的shell脚本。这种现象无处不在:构建脚本、包签名、仓库管理、服务脚本、cron任务、升级逻辑等,全都用bash脚本实现。红帽虽尝试将这些功能迁移至Python并取得部分成效,但某些工具因该选择而运行极其缓慢。

      展望未来,我认为正确方向是将所有这些内容转换为Rust语言,转向uutils
      正是迈出的第一步。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注


京ICP备12002735号