工程趣聞 2026 年 8 月 5 日

2026-08-05 — 用影片編碼器算邏輯閘、抓出 Nix 沙箱裡的隱藏輸入、99 行 C 寫一個 Lisp

primary=https://sharedobject.blog/posts/vp8-combinatorial-logic/ primary=https://fzakaria.com/2026/07/30/the-nix-sandbox-is-a-hidden-input primary=https://github.com/Robert-van-Engelen/tinylisp/blob/main/tinylisp.pdf primary=https://github.com/Robert-van-Engelen/tinylisp

把 VP8 畫面預測模式接成邏輯閘:Video2NAND 的組合邏輯實驗

sharedobject.blog · 2026-07-11

部落格 sharedobject.blog 的一篇文章示範了如何把 VP8 影片編碼器裡的畫面內預測(intra-frame prediction)模式接成真正的邏輯閘。作者不是在講怎麼把電路訊號編碼進影片畫面裡儲存,而是讓解碼器在還原畫面時「順便」算出 NAND 閘該有的輸出。整個實驗只利用 VP8 關鍵畫面(keyframe)的三種區塊預測模式:H_PRED、V_PRED 與 TM_PRED

原本的問題

VP8 對每個區塊會用鄰近已解碼區塊的像素去預測目前區塊的值,再疊上殘差修正。H_PRED 直接複製左側像素、V_PRED 複製上方像素,這兩種都只是單純搬移,適合拿來當「訊號線」傳遞 0(黑)或 255(白)。真正關鍵是 TM_PRED(True Motion Prediction),公式是用左方像素加上方像素再減去左上角像素,結果會被 clamp 在 0 到 255 之間。

predicted = clamp(left + top - top_left, 0, 255)

這條公式在輸入被限制成純黑(0)或純白(255)時,行為會退化成一組固定的布林運算,這正是整篇文章的槓桿點。只要控制好區塊周邊的黑白排列,TM_PRED 就能被逼成邏輯閘

採用的方法

NOT 閘的做法是把上方一整排設成白(255)、左方一整排設成黑(0),此時 TM_PRED 退化成 255 - input,正好是反閘。AND 閘則反過來安排周邊像素,讓公式收斂成 A + B - 255:兩個輸入都是 255 時輸出才會是 255,其餘情況因 clamp 到 0 而輸出黑。NAND 閘不需要新公式,把 AND 的輸出區塊接上一個 NOT 區塊就完成了。

ABAND (A+B-255, clamp)NAND
000255
02550255
25500255
2552552550
  • H_PRED / V_PRED:搬移訊號,充當「線路」
  • TM_PRED(周邊全白全黑):NOT 閘
  • TM_PRED(周邊排列成 A+B-255):AND 閘
  • AND 串接 NOT:NAND 閘

實際效果

目前公開的成果停在 NAND 這一層,還沒組成加法器或更完整的電路,作者預告後續會有進一步文章。NAND 本身已是通用邏輯閘,理論上足以拼出任意組合邏輯,只是巨塊能承載的像素狀態有限,規模化很快會碰上區塊相依限制。這手法呼應了早年「用影片格式塞資料」的把戲,但這次算的不是儲存,而是解碼器內建的像素運算。

原始來源:sharedobject.blog


Nix 的 sandbox 路徑其實是隱藏輸入:從 OpenJDK bootstrap 挖出的可重現性漏洞

fzakaria.com · 2026-07-30

Farid Zakaria 在部落格 fzakaria.com 記錄了他用 Guix 重現 OpenJDK bootstrap 建置時踩到的坑:同一份 .drv 衍生檔案,在不同機器上因為 sandbox 掛載內容不同,算出了不一樣的結果。問題根源是 Nix 的沙箱路徑(sandbox-paths)並不算在建置的輸入裡,卻實際上會影響建置輸出。

原本的問題

Nix 的可重現性假設建立在:同一份 .drv 在任何機器上建置,只要輸入雜湊相同就該得到相同輸出。但 Nix 預設會把 /bin/sh 掛進 sandbox,這個路徑是在編譯 Nix 本身時寫進二進位檔的,寫在 src/libstore/globals.cc 裡:

#if (defined(__linux__) || defined(__FreeBSD__)) && defined(SANDBOX_SHELL)
    sandboxPaths = {{"/bin/sh", {.source = SANDBOX_SHELL}}};
#endif

Guix 分支出去的版本沒有預設掛 /bin/sh。作者原本寫了一段預期會因為找不到 shebang 解譯器而失敗的建置腳本,結果在 Nix 上卻悄悄執行成功,輸出的雜湊和「理應失敗」的版本一樣正常,只是內容早就不對了。

採用的方法

作者用一個最小化例子重現這個現象:一段檢查 /truth 檔案是否存在的 shell 腳本。

derivation { builder = "/bin/sh"; args = [ "-c"
  "if [ -f /truth ]; then read -r x < /truth; else x=4; fi" ]; }

正常情況下 /truth 不存在,x 會是預設值 4。但只要加上 --option extra-sandbox-paths "/truth=/tmp/truth",把外部檔案掛進 sandbox,同一份 .drv、同一個輸入雜湊,就能讓 x 變成 /tmp/truth 裡寫的任何值。sandbox-paths 從頭到尾沒有出現在 derivation 的雜湊計算裡。

實際效果

作者指出這不是單純的 bug,而是設計上的兩難:如果把 sandbox 路徑(例如掛進去的 busybox)也算進輸出雜湊,不同機器上 busybox 的 store 路徑不同,會直接打斷 binary cache 的可攜性,同一份建置在別台機器上永遠對不上快取。文章引用 Ken Thompson 的《Reflections on Trusting Trust》,把這個現象類比成信任鏈中看不見的那一環——讓分散式快取得以成立的機制,恰好也是讓沙箱內容能被悄悄替換的那個機制。

原始來源:fzakaria.com


99 行 C 寫一個 Lisp:tinylisp 如何靠 NaN boxing 塞進 8K 記憶體

GitHub · Robert van Engelen · 討論見於 2026-08-04 Lobsters

Robert van Engelen 的專案 tinylisp 附上一篇論文,詳細說明怎麼用剛好 99 行 C 程式碼寫出一個能跑 REPL 的完整 Lisp 解譯器,內含 21 個內建原語(primitive)。整個直譯器連同資料全部塞進約 8K 記憶體,靠的是把所有 Lisp 值都編碼進 IEEE 754 雙精度浮點數裡的 NaN boxing 技巧。

核心機制

NaN boxing 利用一個冷知識:IEEE 754 的 double 只要指數欄位全部是 1、尾數不是全 0,這個值就是 NaN,而 NaN 的尾數位(payload)在規格上是「不理會」的,可以自由塞資料。tinylisp 把型別標籤和陣列索引塞進這段 payload,讓一個 double 同時能表示數字本身、cons 對、原語函式或閉包之一,不需要另外的 tagged union 結構。整個環境是一個大小為 N=1024 的 cell 陣列,cons 對就是兩個指向這個陣列的索引配對,car/cdr 只是陣列存取。

採用的方法

解譯器沒有另外實作追殺垂死物件的回收器,而是用一個簡化版垃圾回收搭配線性配置的 cell 陣列,把管理成本壓到最低;文中提到的優化版本改用參考計數(reference counting),執行速度可以快上十倍以上。21 個核心原語涵蓋求值與控制流(eval、quote、cond、if、lambda、define、let*)、列表操作(cons、car、cdr、pair?)、算術與邏輯運算,足以撐起遞迴、閉包與柯里化函式。

  • 求值/控制:eval、quote、cond、if、lambda、define、let*
  • 列表:cons、car、cdr、pair?
  • 算術:+、-、*、/、int
  • 邏輯:<、eq?、or、and、not

實際效果

基礎的 99 行版本採用靜態範疇(static scoping)與詞法閉包,支援巢狀 lambda 與柯里化,但不含尾呼叫優化,遞迴太深會吃光 call stack。作者另外提供擴充版「extras」,多加 16 個原語(read、print、load、catch、throw、trace 等)與巨集支援;千行等級的完整版本則換上 mark-sweep 或 Cheney's copying garbage collector。這篇論文更像是把 McCarthy 最初 Lisp 論文的精神,用能實際編譯執行的 99 行 C 重新示範一次。

原始來源:tinylisp.pdfGitHub repo


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