工程趣聞 2026 年 8 月 26 日

2026-08-26 — Rust enum 塞進 64 位元字組加速 17%、Go netpoll 32 位元別名 bug、Python str.lower() 引發 IDNA 安全漏洞

primary=https://pointersgonewild.com/2026-08-25-replacing-a-rust-enum-with-a-64-bit-word/ primary=https://sigma-star.at/blog/2026/08/go-runtime-netpoll-bug/ primary=https://sethmlarson.dev/when-str-lower-is-a-security-vulnerability

把 16 bytes 的 Rust enum 壓進 64 位元字組,直譯器效能提升 17%

pointersgonewild.com · 2026-08-25

pointersgonewild.com 部落格作者在自製語言直譯器 Plush(maximecb/plush)中,把原本佔 16 bytes 的 Value enum 改寫成 8 bytes 的 tagged 64-bit word,整體 benchmark 平均加速 17%。這是作者自己動手的效能改寫,不是外部回報的 bug。改動集中在直譯器最熱的路徑:值的表示法(value representation)。

原本的問題

舊版 Value 是標準的 Rust tagged enum,列出 UndefNilInt64(i64)Float64(f64)String(*const Str)Object(*mut Object) 等十餘種變體。enum 只需要 8 bits 存放 discriminant,但 Rust 的對齊規則把整個結構撐大到 16 bytes,因為最大的變體資料本身就要 8 bytes,再加上獨立存放的 tag。

enum Value {
    Undef, Nil, False, True,
    Int64(i64), Float64(f64),
    String(*const Str), HostFn(&'static HostFn),
    Fun(FunId), Closure(*mut Closure),
    Cell(*mut Value), Object(*mut Object),
    Array(*mut Array), Dict(*mut Dict),
    Class(ClassId),
}
// size_of::<Value>() == 16

在陣列與物件圖走訪這類指標追逐(pointer chasing)workload 裡,16 bytes 的值大小直接讓記憶體頻寬需求與 cache line 占用翻倍。以 binary_treelinked_list 這類物件密集的 benchmark 最明顯,因為每個節點都要多讀一次沒用到的 padding。

採用的方法

新版把 Value 改成 #[repr(transparent)] struct Value(u64),用最低幾個位元當 tag:000 給 fixnum(整數左移 2 bit 直接塞入)、010 給 flonum(浮點數用 rotate-left 編碼,引用 Melançon 等人 2024 年論文 arXiv:2411.16544 的 self-tagging 手法)、001/011 分別給「用位址比較」與「用內容比較」的兩種指標、101 給 nil/true/false 等 immediate。其中一個位元專門標記這個值能否不拆箱直接比較,讓字串與其他指標共用同一套 tag 判斷邏輯。

#[repr(transparent)]
pub struct Value(u64);

const TAG_FIXNUM: u64 = 0b000; // 62-bit 整數
const TAG_PTR_ID: u64 = 0b001; // 指標,用位址比較
const TAG_FLONUM: u64 = 0b010; // 浮點數,rotl 編碼
const TAG_PTR_VAL: u64 = 0b011; // 指標,用內容比較
const TAG_IMM: u64   = 0b101; // nil / true / false ...

這個設計讓整數的建構與判斷都是純位元運算,不必再走 enum 的 discriminant 分支。作者比較了編譯後的機器碼:舊版的加法運算拆成 4 個不連續 basic block、夾帶多次 stack 溢出,共 52 條指令、24 次記憶體存取、4 個分支;新版是一段連續程式碼,36 條指令、9 次記憶體存取、1 個分支。

實際效果

記憶體方面,mlp benchmark 的 peak RSS 降低 37%,物件密集的 binary_tree 也大幅下降;原本就很小的最低堆積用量在輕量 benchmark 上沒有變化。效能方面整體平均如標題所述提升 17%,binary_treelinked_list 因記憶體用量直接減半而受益最大,fib 在幾乎不觸及記憶體的情況下仍提升約 15%,nbodymlp 則因浮點數仍需 rotl 編碼的額外成本,只提升 10–12%。測試機器為 MacBook Air M5,作者也指出這次收益主要來自省下的記憶體頻寬,而非分支預測。

原始來源:pointersgonewild.commaximecb/plush @ 6b71f8c


32 位元機器上的 Go netpoll 幽靈 bug:一個位址別名問題

sigma-star.at · 2026-08-25

奧地利安全公司 sigma-star 的部落格於 2026 年 8 月 25 日發文,追蹤一個只在 32 位元嵌入式 Linux 上出現的 Go runtime 崩潰:runtime: netpoll: eventfd ready for something unexpected。這個問題最早由使用者 lorki97 在 golang/go#72900 回報,受影響版本為 Go 1.23.21.24.01.24.1,而且程式必須連續跑上好幾天才會觸發。

原本的問題

Go 1.23 為 netpoll 加入 eventfd 喚醒機制,把 epoll 事件的 data 欄位塞進兩種不同的指標:netpollEventFd 的原始指標,以及帶了 fdseq tag 的 pollDesc 指標(低 4 bytes 放 tag、高 4 bytes 放位址)。runtime 判斷事件來源時把 ev.Data 轉成 uintptr 比對,在 32 位元系統上 uintptr 只有 4 bytes,等於只看了這個 8-byte 欄位的下半部。

// 32-bit 系統上 uintptr 只有 4 bytes
if uintptr(ev.data) == uintptr(unsafe.Pointer(&netpollEventFd)) {
    // 誤判:這其實是某個 pollDesc 的 fdseq tag
}

當程式跑得夠久、fdseq 計數器累積到數百萬次循環後,tag 值恰好會撞上 &netpollEventFd 位址下半部的數字,造成 socket 的 pollDesc 被誤判成 eventfd,進而觸發 runtime 的防呆 panic。64 位元平台不受影響,因為 uintptr 有完整 8 bytes 可比對;32 位元 big-endian 系統也不受影響,因為位址恰好落在被讀到的那一半。

採用的方法

sigma-star 團隊在自家 32 位元嵌入式裝置上重現後回頭比對 Go 原始碼,確認這是純粹的位元寬度別名(aliasing)問題,不是邏輯錯誤。Go 團隊最初的修正嘗試(CL 658755)被打回,最後採用的做法是不再讓 netpollEventFd 用原始指標存進同一個欄位,而是改成一個帶 tag 的 nil pollDesc,讓 ev.Data 欄位裡永遠只存在同一種格式的 tagged pointer。

  • 問題只在 32 位元 little-endian 上出現(嵌入式常見的 ARM、i386)
  • 觸發門檻是 fdseq 累積到約 300 萬次左右的 pollDesc 回收循環
  • 修正案 golang/go#81037 已合併為 CL 819800

實際效果

修正後,ev.Data 欄位不再混用兩種指標格式,32 位元系統的比對邏輯不會再因為只讀到半個欄位而誤判。長時間執行的服務型程式特別容易踩到只在極端計數器數值下才現形的別名 bug,一般開發用的 64 位元機器完全測不出來,只有嵌入式裝置連續跑上數天以上才會現形。

原始來源:sigma-star.atgolang/go issue #72900


當 str.lower() 是安全漏洞:Python IDNA 實作跟錯了 Unicode 版本

sethmlarson.dev · 2026-08-26

Python 安全開發者 Seth Larson 在 2026 年 8 月 26 日發文,說明標準庫 encodings/idna.pystringprep 模組裡一段呼叫 str.lower() 的程式碼,如何演變成正式的安全漏洞 CVE-2026-17084。問題不是 locale,而是Python 內建的 Unicode 版本一直往前走,但 RFC 3454 要求的比對規則凍結在 Unicode 3.2.0

原本的問題

IDNA 2003(RFC 3454 StringPrep)的 Table B.3 規定了一套字元大小寫對應表,設計時明確綁定 Unicode 3.2.0 的定義。CPython 的實作裡,查不到 exceptions 表的字元就直接呼叫 str.lower() 收尾。

def map_table_b3(code):
    r = b3_exceptions.get(ord(code))
    if r is not None:
        return r
    return code.lower()

str.lower() 用的是直譯器當前捆綁的 Unicode 版本(文章寫作時是 17.0.0),而不是 RFC 3454 指定的 3.2.0。只要某個字元在兩個版本之間變更了大小寫對應規則,map_table_b3 就會回傳一個 RFC 沒有預期的結果。

採用的方法

文章舉切羅基字母 U+13A0(Ꭰ)為具體例子:同一個網域名稱字元,依 RFC 3454 規則編碼應得到 xn--58da,但用 Unicode 17.0.0 的大小寫規則編碼卻得到 xn--kz9aa兩種編碼結果指向不同的網域,version drift 讓同一個輸入在不同 Python 版本下解析成不同目標,這正是 IDNA 編碼分歧可以被利用的縫。

  • 受影響模組:Lib/encodings/stringprep.pyTools/unicode/makeunicodedata.py
  • 修法:讓產生 Unicode 資料表的工具排除「Unicode 3.2.0 尚未定義」或「後續版本改變了屬性」的字元,不再讓 str.lower() 用當前版本規則覆蓋 StringPrep 該用的舊規則
  • 修正 PR:python/cpython#155293,對應議題 GH-155292,標記為 type-security

實際效果

修正已於 2026 年 8 月 18 日由 CPython 核心開發者 @encukou 合併,並回溯到 3.14 與 3.15 分支。任何規格文件寫死引用某個特定 Unicode 版本的地方,都不能偷懶用語言內建、會隨版本升級改變行為的字串函式去實作,否則規格與實作會隨著直譯器升級逐漸分岔。

原始來源:sethmlarson.devCPython PR #155293


End of article
0
Would love your thoughts, please comment.x
()
x