--- article.previous.md +++ article.md @@ -1,55 +1,61 @@ -# 《Mac is running out of time》全文(校正版) +# 《Mac is running out of time》 > 来源:YouTube [The PrimeTime] > 时长:00:10:24 > 视频ID:Q9GAJ_ka4l4 > 链接:https://www.youtube.com/watch?v=Q9GAJ_ka4l4 -> 说明:本文已由 AI 进行全文校正,修正了语音识别错误和口语填充词。 +> 说明:以下文字按视频原讲述顺序整理,口语、重复和明显噪声已做轻度收束。 --- -有一类 bug,仅凭其表现条件就能立刻判断出它的本质。举个典型的例子:如果你发现程序出了问题,只要在代码里加一个 console.log 打印语句,bug 就消失了;而当你删掉这个打印语句后,bug 又立刻重现——这几乎可以确定是一个与时间相关的 bug。因为打印操作本身会带来性能开销,稍微拖慢了程序运行速度,恰好让原本因时序问题暴露的缺陷不再触发。一旦移除打印,系统恢复到原来的执行节奏,bug 便重新出现。这类现象在软件开发中非常著名,被称为“Hyzen Bug”——即一种在被观察或调试时就会消失或行为改变的软件缺陷。 +## 49天的死亡倒计时:Mac的隐藏定时炸弹 -简单来说,仅仅去“看”它,它就自己不见了。这类问题通常都和时间有关。还有一类 bug,如果我只说出一个数字,可能有些人立刻就会冒冷汗。准备好了吗。49天、17小时、2分钟、47秒。 +有一类 bug,仅凭其表现条件就能立刻判断出它的本质。举个典型的例子:如果你发现程序出了问题,只要在代码里加一个 console.log 打印语句,bug 就消失了。而当你删掉这个打印语句后,bug 又立刻重现——这几乎可以确定是一个与时间相关的 bug。因为打印操作本身会带来性能开销,稍微拖慢了程序运行速度,恰好让原本因时序问题暴露的缺陷不再触发。一旦移除打印,系统恢复到原来的执行节奏,bug 便重新出现。 -我知道大多数人听到这个数字一脸茫然,但希望看完这段视频后,你也会感到一阵寒意——因为这是一个极其糟糕、令人窒息的时间点,而且你应该立刻意识到问题所在。在深入之前,先来个小提醒:别再让你的 AI 代理在电脑上随意联网了。这是最容易自取灭亡的操作。因此,今天视频的赞助商 Kernel.sh 正是为此而生——一个超快、开源的 AI 代理基础设施,让你的代理能真正访问互联网。它能在不到 30 毫秒内启动一个甚至上千个云端浏览器,认证也自动处理。 +这类现象在软件开发中非常著名,被称为“Hyzen Bug”——即一种在被观察或调试时就会消失或行为改变的软件缺陷。简单来说,仅仅去“看”它,它就自己不见了。这类问题通常都和时间有关。 -目前已有超过 3,000 支团队在生产环境中使用,包括 Framer 和 Cash App。别再给你的代理“装弱化器”了,让他们用上真正的浏览器吧。现在就去 kernel.sh,让他们自由上网。欢迎回来。我想向大家展示一份非常精彩的技术文档,可惜最近网站改版,导致所有文字变成白色,即使我手动调整 CSS 也无法改变,系统就是拒绝变色。 +还有一类 bug,如果我只说出一个数字,可能有些人立刻就会冒冷汗。准备好了吗?49天、17小时、2分钟、47秒。我知道大多数人听到这个数字一脸茫然,但希望看完这段视频后,你也会感到一阵寒意——因为这是一个极其糟糕、令人窒息的时间点,而且你应该立刻意识到问题所在。 -所以只能打开阅读器来看。这篇文章标题是《我们在 Mac OS TCP 网络中发现了一个倒计时炸弹》,它会在精确的 49 天、17 小时、2 分钟、47 秒后引爆。这个发现来自一家名为 Photon 的公司,他们用一批 Mac 电脑持续监控 iMessage 服务。奇怪的是,这些 Mac 每次运行到 exactly 49 天 17 小时 2 分钟 47 秒时,就会突然崩溃——内存开始失控飙升,最终完全无法建立新的 TCP 连接。这听起来很诡异,对吧。 +在深入之前,先来个小提醒:别再让你的 AI 代理在电脑上随意联网了。这是最容易自取灭亡的操作。因此,今天视频的赞助商 Kernel.sh 正是为此而生——一个超快、开源的 AI 代理基础设施,让你的代理能真正访问互联网。它能在不到 30 毫秒内启动一个甚至上千个云端浏览器,认证也自动处理。目前已有超过 3,000 支团队在生产环境中使用,包括 Framer 和 Cash App。别再给你的代理“装弱化器”了,让他们用上真正的浏览器吧。现在就去 kernel.sh,让他们自由上网。 -事实上,这个问题并非只存在于 Photon 公司的设备,而是存在于所有 Mac 之中。只要你让一台 Mac 连续开机超过 49 天 17 小时 2 分钟 47 秒,就一定会遇到同样的问题:当 TCP 连接数达到某个临界点后,系统将彻底无法创建新连接,整台电脑变得像一块“砖头”一样瘫痪。要理解这一点,首先要了解 TCP 连接的基本原理。当你从电脑发起一个 TCP 连接时,会占用本地的一个端口。一台 Mac 有大约 65,000 个可用端口,但实际用于主动连接的只有约一半。 +我想向大家展示一份非常精彩的技术文档,可惜最近网站改版,导致所有文字变成白色,即使我手动调整 CSS 也无法改变,系统就是拒绝变色。只能打开阅读器来看。这篇文章标题是《我们在 Mac OS TCP 网络中发现了一个倒计时炸弹》,它会在精确的 49 天、17 小时、2 分钟、47 秒后引爆。 -当连接关闭后,这个端口并不会立即可用,必须等待一小段时间——这就是所谓的“TIME_WAIT”状态。为什么需要等待。因为互联网很大,数据包可能在传输途中延迟、绕路,甚至在云中“迷路”。如果端口被立刻重用,那些延迟到达的数据包可能会误入新连接,造成混乱和数据损坏,从而彻底破坏连接。因此,系统必须等待一段时间,确保旧连接的所有残留数据包都已消失。 +这个发现来自一家名为 Photon 的公司,他们用一批 Mac 电脑持续监控 iMessage 服务。奇怪的是,这些 Mac 每次运行到 exactly 49 天 17 小时 2 分钟 47 秒时,就会突然崩溃——内存开始失控飙升,最终完全无法建立新的 TCP 连接。这听起来很诡异,对吧? -这个等待时间在标准中被定义为 30 秒左右。30 秒后,端口才真正释放,可以再次使用。第二个关键点是整数溢出(integer overflow)。虽然大多数人都知道,但为了完整起见,我还是快速解释一下:一个字节由 8 位组成,假设所有位都是 1,也就是二进制 11111111(十进制 255)。如果再加上 1,会发生什么。 +事实上,这个问题并非只存在于 Photon 公司的设备,而是存在于所有 Mac 之中。只要你让一台 Mac 连续开机超过 49 天 17 小时 2 分钟 47 秒,就一定会遇到同样的问题:当 TCP 连接数达到某个临界点后,系统将彻底无法创建新连接,整台电脑变得像一块“砖头”一样瘫痪。 -每一位都会从 1 变成 0,并产生进位,一直传递到最高位,最终结果变成 00000000,同时溢出标志位被置为 1。这就是典型的整数溢出。顺便说一句,刚才那条画得特别直的线,简直堪称手绘艺术——那种近乎完美的直线,连我妈都认不出来。不过话说回来,这条线确实太棒了,你总得说点什么吧。我觉得你得夸一夸它。 +要理解这一点,首先要了解 TCP 连接的基本原理。当你从电脑发起一个 TCP 连接时,会占用本地的一个端口。一台 Mac 有大约 65,000 个可用端口,但实际用于主动连接的只有约一半。当连接关闭后,这个端口并不会立即可用,必须等待一小段时间——这就是所谓的“TIME_WAIT”状态。 -最后,你将进位的1带入,结果变成0,再进位一次,溢出位就会被置为1。顺便说一句,那是一条垂直的直线,非常罕见。好了,各位,这真是一段很棒的视频。你们得说点什么,比如“这条线看起来太棒了”,我可期待着呢。这意味着你的数值从255——一个无符号字节所能表示的最大值——直接跳回了0。 +为什么需要等待?因为互联网很大,数据包可能在传输途中延迟、绕路,甚至在云中“迷路”。如果端口被立刻重用,那些延迟到达的数据包可能会误入新连接,造成混乱和数据损坏,从而彻底破坏连接。因此,系统必须等待一段时间,确保旧连接的所有残留数据包都已消失。 -真是令人难过的一天。这种情况被称为整数溢出:你一路飙升到最大值,却瞬间被扔回起点。如果我再努力一点,我觉得还能把这个图画得更好。记住这一点。现在,既然你已经了解了这两个关键信息,Photon最终做了一个实验。 +这个等待时间在标准中被定义为 30 秒左右。30 秒后,端口才真正释放,可以再次使用。 -实验是这样的:我们拿一台即将耗尽寿命的Mac,它几乎要跨过那个临界点,或者已经频繁崩溃。在倒计时五分钟时,我们开始向它发送海量的客户端连接请求。理论上,内部应该达到一种端口占用的平衡状态:每当发起一个新的请求,之前30秒前的旧连接就应该被释放。而他们在这里看到的情况正是如此。抱歉,文字没显示出来,但这里你可以看到,活跃连接数达到了约200个。 +第二个关键点是整数溢出(integer overflow)。虽然大多数人都知道,但为了完整起见,我还是快速解释一下:一个字节由 8 位组成,假设所有位都是 1,也就是二进制 11111111(十进制 255)。如果再加上 1,会发生什么?每一位都会从 1 变成 0,并产生进位,一直传递到最高位,最终结果变成 00000000,同时溢出标志位被置为 1。这就是典型的整数溢出。 -连接不断建立,每30秒就有新连接开启。但当接近这个数量时,你会发现连接数持续攀升,不断累积。实际上,新的连接已经无法建立。原因其实有点出人意料。如果你跳转到旧版 Apple OSS 的 dash XNU 仓库,就能找到真正的 Apple XNU 分发版本——也就是你 Mac 上运行的系统内核。 +顺便说一句,刚才那条画得特别直的线,简直堪称手绘艺术——那种近乎完美的直线,连我妈都认不出来。不过话说回来,这条线确实太棒了,你总得说点什么吧?我觉得你得夸一夸它。 -你可以去查看安全机制、手册页,或者看看 Apple 的许可证。我不确定……哦天,别展示这个。天哪,Apple 的许可证太多了,完全不像 MIT 那种风格。不管怎样,如果你进入 tcp_subr.c 文件,会发现一个非常漂亮的函数,叫做 Calculate TCP Clock。这个函数的作用是同步整个 TCP 协议栈的时间信息,因为每次发送数据包时都会附带时间戳。 +最后,你将进位的1带入,结果变成0,再进位一次,溢出位就会被置为1。顺便说一句,那是一条垂直的直线,非常罕见。好了,各位,这真是一段很棒的视频。你们得说点什么,比如“这条线看起来太棒了”,我可期待着呢。 -它也用来判断某个连接是否已超时,从而释放对应的端口。而这一切都依赖于一个单一的时钟。你看这里,这是获取当前毫秒级时间的方式。如果你习惯用 date.now 或者 odin 的 tick_now 这类方式,可能会觉得这简直不可思议。但这其实是老派获取时间信息的方法,实际上和 Chrome 的 Performance API 非常相似,对吧。 +## 从端口到时间:理解TCP的脆弱机制 -确实如此。而问题就出现在这一行代码上。你注意到它取的是 tv_sec——虽然不知道 tv 具体代表什么,但先接受它——即从机器启动以来经过的秒数,乘以一千,转换成毫秒。接着,它把这个值强制转换为 Uint32。那么,Uint32 的问题在哪。 +这意味着你的数值从255——一个无符号字节所能表示的最大值——直接跳回了0。真是令人难过的一天。这种情况被称为整数溢出:你一路飙升到最大值,却瞬间被扔回起点。如果我再努力一点,我觉得还能把这个图画得更好。记住这一点。 -因为它只能表示大约42亿个数值。秒数乘以千后变成毫秒,而42亿毫秒大约等于49天17小时2分钟47秒。一旦超过这个时间,系统就会发生回滚,从最大值直接跳回0。此时,当前的 TCP 时间又回到了一个极小的数值。然后,系统会加载所有正在进行的 TCP 连接共享的时间戳。 +现在,既然你已经了解了这两个关键信息,Photon最终做了一个实验。实验是这样的:我们拿一台即将耗尽寿命的Mac,它几乎要跨过那个临界点,或者已经频繁崩溃。在倒计时五分钟时,我们开始向它发送海量的客户端连接请求。理论上,内部应该达到一种端口占用的平衡状态:每当发起一个新的请求,之前30秒前的旧连接就应该被释放。而他们在这里看到的情况正是如此。抱歉,文字没显示出来,但这里你可以看到,活跃连接数达到了约200个。连接不断建立,每30秒就有新连接开启。但当接近这个数量时,你会发现连接数持续攀升,不断累积。实际上,新的连接已经无法建立。 -这个临时时间与当前时间进行比较,判断:“嘿,当前临时时间是否小于现在的时间。”——答案是“是的”,于是更新时间。但问题就出在这儿:当前时间刚刚完成一轮回滚,变成了一个很小的数字,比如5000、1000甚至500,不管具体是多少,反正都很小。因此,这个判断条件永远不成立。结果就是,TCP 协议栈内部的时间永远不会向前推进。 +原因其实有点出人意料。如果你跳转到旧版 Apple OSS 的 dash XNU 仓库,就能找到真正的 Apple XNU 分发版本——也就是你 Mac 上运行的系统内核。你可以去查看安全机制、手册页,或者看看 Apple 的许可证。我不确定……哦天,别展示这个。天哪,Apple 的许可证太多了,完全不像 MIT 那种风格。不管怎样,如果你进入 tcp_subr.c 文件,会发现一个非常漂亮的函数,叫做 Calculate TCP Clock。这个函数的作用是同步整个 TCP 协议栈的时间信息,因为每次发送数据包时都会附带时间戳。它也用来判断某个连接是否已超时,从而释放对应的端口。而这一切都依赖于一个单一的时钟。 -也就是说,30秒的 TIME_WAIT 超时永远无法达成。这意味着,旧的端口永远无法被释放。一旦连接数超过大约32000个,你就再也无法建立任何新的 TCP 连接。当然,现有的连接依然正常工作,但所有新连接都会彻底失效。天啊,这全是因为一个 Uint32。 +你看这里,这是获取当前毫秒级时间的方式。如果你习惯用 date.now 或者 odin 的 tick_now 这类方式,可能会觉得这简直不可思议。但这其实是老派获取时间信息的方法,实际上和 Chrome 的 Performance API 非常相似,对吧?而问题就出现在这一行代码上。你注意到它取的是 tv_sec——虽然不知道 tv 具体代表什么,但先接受它——即从机器启动以来经过的秒数,乘以一千,转换成毫秒。接着,它把这个值强制转换为 Uint32。 -你为了节省四个字节,坚持用32位整数而不是64位,结果让所有人都遭殃了。事实上,这本质上和 Y2K38 问题一模一样——只不过 Y2K38 是从1970年开始计算的秒数,使用的是 int32,只能表示21亿秒,大约在2038年左右就会回滚到过去。总之,我觉得这实在太有意思了。这是一个极其精妙的漏洞,强烈建议大家去看原文。文章写得非常深入,真的值得一读。 +那么,Uint32 的问题在哪?因为它只能表示大约42亿个数值。秒数乘以千后变成毫秒,而42亿毫秒大约等于49天17小时2分钟47秒。一旦超过这个时间,系统就会发生回滚,从最大值直接跳回0。此时,当前的 TCP 时间又回到了一个极小的数值。然后,系统会加载所有正在进行的 TCP 连接共享的时间戳。这个临时时间与当前时间进行比较,判断:“嘿,当前临时时间是否小于现在的时间?”——答案是“是的”,于是更新时间。 -我就是对这类技术细节着迷。所以,如果你曾经让 Mac 持续运行太久,然后突然一切崩溃,那很可能就是这个原因。名字叫 PrimeTime,还真是名副其实。 +## 真相揭露:一个32位计时器的致命回绕 + +但问题就出在这儿:当前时间刚刚完成一轮回滚,变成了一个很小的数字,比如5000、1000甚至500,不管具体是多少,反正都很小。因此,这个判断条件永远不成立。结果就是,TCP 协议栈内部的时间永远不会向前推进。也就是说,30秒的 TIME_WAIT 超时永远无法达成。这意味着,旧的端口永远无法被释放。一旦连接数超过大约32000个,你就再也无法建立任何新的 TCP 连接。当然,现有的连接依然正常工作,但所有新连接都会彻底失效。 + +天啊,这全是因为一个 Uint32。你为了节省四个字节,坚持用32位整数而不是64位,结果让所有人都遭殃了。事实上,这本质上和 Y2K38 问题一模一样——只不过 Y2K38 是从1970年开始计算的秒数,使用的是 int32,只能表示21亿秒,大约在2038年左右就会回滚到过去。 + +总之,我觉得这实在太有意思了。这是一个极其精妙的漏洞,强烈建议大家去看原文。文章写得非常深入,真的值得一读。我就是对这类技术细节着迷。所以,如果你曾经让 Mac 持续运行太久,然后突然一切崩溃,那很可能就是这个原因。名字叫 PrimeTime,还真是名副其实。 --- -*全文由 AI 转录并校正生成,仅供参考。* +*本文由 AI 基于转写整理,仅供复查参考。*