1 điểm bởi GN⁺ 2023-10-30 | 1 bình luận | Chia sẻ qua WhatsApp
  • ISO cài đặt tối thiểu của NixOS đã được tái dựng độc lập và giống hệt từng bit với bản phân phối của Hydra, cho thấy có thể xác minh sự khớp giữa nhị phân phân phối và mã nguồn
  • Lần xác minh này tái lập không chỉ các gói có trong ISO mà cả chính quá trình tạo ISO, kiểm tra phạm vi rộng hơn so với việc chỉ tái lập từng gói
  • Việc tái dựng bắt đầu từ NixOS 20.03 VirtualBox appliance, sử dụng revision 63678e9f3d3a của nixpkgs, và tắt phụ thuộc vào bộ nhớ đệm nhị phân bằng --option substitute false
  • Nếu OVA năm 2020 hoặc git được tải xuống có backdoor tinh vi, chúng vẫn có thể là vector tấn công, nên việc xác minh dựa trên một hệ thống được bootstrap hoàn toàn vẫn còn là việc cần làm
  • Tái dựng ISO tối thiểu là một cột mốc quan trọng, nhưng các nhiệm vụ tiếp theo là loại bỏ những giải pháp vòng tạm thời, tái lập thêm nhiều phương tiện cài đặt, xây dựng hạ tầng tái dựng độc lập định kỳ và công cụ chứng minh bản dựng

Khả năng tái lập được xác minh từ ISO tối thiểu

  • Đã build lại độc lập nixos-minimal ISO build do Hydra công bố và thu được kết quả giống hệt từng bit
  • Phạm vi tái lập được chia thành hai trục
    • Tất cả các gói đi vào ISO
    • Chính quy trình build tạo ra ISO
  • Các gói cần thiết để build ISO nhưng không nằm bên trong ISO cũng được build cùng, và không phụ thuộc vào nhị phân đã được cache
  • Build có thể tái lập cung cấp một đường tin cậy để kiểm tra liệu nhị phân phân phối có trung thành với mã nguồn hay không, và có bị sửa đổi trong pipeline build như Hydra hay không

Quy trình tái dựng và giới hạn

  • Việc tái dựng được thực hiện bằng cách khởi động NixOS 20.03 trong một VirtualBox appliance mới
    • Cấp đủ CPU và bộ nhớ, rồi mở rộng đĩa lên khoảng 65GB
    • Sau khi cài git, clone nixpkgs và checkout revision 63678e9f3d3a
    • Với --option substitute false, build các mục cần thiết trên máy cục bộ thay vì lấy từ bộ nhớ đệm nhị phân
  • Quy trình có bao gồm các biện pháp tạm thời để né các vấn đề đã biết
  • Vẫn còn các giới hạn về mặt niềm tin chuỗi cung ứng
    • Nếu OVA năm 2020 hoặc git được tải xuống có backdoor tinh vi, chúng vẫn có thể là vector tấn công
    • Sẽ tốt hơn nếu tái dựng trên một hệ thống được bootstrapped hoàn toàn, nhưng hiện vẫn chưa đạt tới giai đoạn đó
    • Tiến triển liên quan đang tiếp tục trong thread dự án bảo mật chuỗi cung ứng nixpkgs

Khác biệt giữa công bố năm 2021 và kết quả lần này

  • Năm 2021 từng có công bố rằng ISO tối thiểu có thể tái lập 100%, nhưng khi đó mới chỉ tái lập riêng lẻ các gói cần cho build ISO, còn việc tái dựng ISO thực tế vẫn có khác biệt
    • Nguyên nhân là các vấn đề còn tồn tại trong cache của Hydra và cách tạo ISO
    • Sau đó, trong lúc các vấn đề được sửa, lại xuất hiện các hồi quy như vấn đề upstream của Python 3.10, và phải đến tuần này toàn bộ chuỗi mới trở lại trạng thái có thể xác minh
  • Bước tiếp theo là loại bỏ các giải pháp vòng tạm thời, tái lập nhiều gói hơn, và tái lập các phương tiện cài đặt khác như Gnome ISO
  • Cũng cần hạ tầng tái dựng độc lập định kỳ và các công cụ chia sẻ, tiêu thụ chứng minh bản dựng như trustix

1 bình luận

 
GN⁺ 2023-10-30
Các ý kiến trên Hacker News
  • Việc build lại ISO tối thiểu từ mã nguồn là một cột mốc ấn tượng trên hành trình hướng tới một hệ thống có thể build tái lập được dựa trên mã nguồn
    Guix gần đây cũng đạt được một thành tựu trực giao nhưng ấn tượng không kém trên cùng hành trình: bootstrap toàn bộ toolchain compiler từ một binary 357 byte duy nhất có thể tái lập, không cần các blob compiler binary khác
    Có lẽ một ngày không xa, hai hướng này sẽ được kết hợp để có thể build tái lập toàn bộ bản phân phối từ mã nguồn
    https://guix.gnu.org/en/blog/2023/the-full-source-bootstrap-...

    • Thật tuyệt, và thật vui khi thấy vẫn có những người đang chiến đấu vì điều đúng đắn giữa những phản ứng kiểu “nếu có backdoor thì dù sao mọi người cũng nhận cùng một backdoor thôi mà?”
      Điểm cốt lõi mà nhiều người không hiểu là mục tiêu không phải chứng minh rằng sản phẩm đầu ra đáng tin 100%, mà là chứng minh rằng sản phẩm đầu ra trung thành 100% với mã nguồn
      Nghĩa là nếu phát hiện chuyện đáng ngờ như một backdoor bí mật, ta sẽ luôn có thể tái lập nó một cách dứt khoát
      Về phía kẻ xấu, điều đó đồng nghĩa không còn chỗ để chạy, cũng không còn nơi để trốn
    • Tuy chưa đi xa tới mức stage0 của Guix, nhưng tại NixCon có một bài trình bày thú vị về việc bootstrap Nix từ TinyCC: https://media.ccc.de/v/nixcon-2023-34402-bootstrapping-nix-a...
    • Việc binary của compiler dùng để bootstrap chỉ có 357 byte thật sự ấn tượng
    • Nếu chỉ 357 byte thì không biết có nhất thiết cần một binary tái lập được không
      Có lẽ có thể tự tay ghi tài liệu cho toàn bộ 357 byte mã máy để con người hiểu được
  • Có thể đây là câu hỏi ngớ ngẩn vì tôi chưa từng làm việc kiểu này, nhưng tôi thắc mắc tại sao khả năng tái lập không phải là hành vi mặc định
    Nếu đã biên dịch hai bản phần mềm từ cùng một mã nguồn, tôi không rõ điều gì ngăn chúng trở nên hoàn toàn giống nhau mỗi lần
    Tôi biết có nhiều thành phần chuyển động, nhưng vẫn chưa thật sự hiểu khác biệt phát sinh như thế nào

    • Có rất nhiều nguyên nhân cụ thể, và có lẽ timestamp là vấn đề phổ biến nhất
      Có thể xem danh sách các vấn đề thường gặp tại đây: https://reproducible-builds.org/docs/
      Nhìn chung, điểm mấu chốt là các developer không kiểm thử xem build có tái lập được hay không
      Nếu việc này được đưa vào kiểm thử release, thường thì nó sẽ tiếp tục được duy trì ở trạng thái tái lập được
    • Có rất nhiều nguyên nhân
      Các ví dụ điển hình khiến build trở nên không tái lập một cách rõ ràng là timestamp và thông tin tác giả
      Cũng có những chỗ âm thầm phá vỡ khả năng tái lập mặc định; chẳng hạn nhiều runtime không định nghĩa thứ tự các mục trong hashmap, và compiler đôi khi duyệt hashmap đó để tạo binary
    • Có thể là do tính song song
      Có thể có những tác vụ không độc lập với thứ tự, và tùy trạng thái CPU mà binary tạo ra hơi khác nhau, dù tất cả đều là kết quả đúng
    • Nhóm Go gần đây đã đăng một bài về những gì họ đã làm để khiến toolchain Go hoàn toàn tái lập được: https://go.dev/blog/rebuild
    • Đôi khi là do thuật toán ngẫu nhiên, đôi khi là do vấn đề hiệu năng, ví dụ khi không sắp xếp thứ gì đó thì nhanh hơn
      Đôi khi nguyên nhân cũng là metadata phụ thuộc vào thời gian hoặc môi trường, hay thứ tự thực thi của thread
  • Xin lỗi vì tôi không rành, nhưng tôi từng nghĩ một trong những lý do chính để NixOS tồn tại là khả năng tái lập
    Tôi tưởng những vấn đề như thế này đã được giải quyết rồi
    Tôi chỉ dùng NixOS khoảng 2 giờ và muốn thử Hyprland; vì Hyprland cần cấu hình đôi chút nên tôi nghĩ dùng cấu hình của người khác trên NixOS sẽ dễ hơn so với các distro khác
    Nhưng việc tìm cấu hình cũng khó, tôi tìm được khoảng 3 gist ngẫu nhiên trên GitHub nhưng không cái nào chạy, nên đã bỏ cuộc

    • NixOS có ưu điểm là mọi thứ được build trong sandbox riêng, chỉ với các dependency được khai báo rõ ràng và được hash
      Vì không phụ thuộc vào toàn bộ môi trường hệ thống như các distro thông thường, trong nhiều trường hợp nó đã tạo ra cùng một binary mỗi lần
      Tuy nhiên bản thân quá trình build ở nhiều package có thể không mang tính quyết định, nên chỉ điều này không lập tức đem lại khả năng tái lập hoàn toàn
    • Khả năng tái lập có hai nghĩa
      Nghĩa mà bạn đang nghĩ đến là khi dễ dàng build lại các package binary thì dùng cùng phiên bản dependency, cùng tùy chọn build, v.v.
      Tức là không nên phát sinh lỗi biên dịch mới kiểu “trên laptop của tôi thì chạy được”
      Nghĩa đang được nói ở đây là mọi artifact build trở thành binary giống nhau tới từng byte
      Nó không được phụ thuộc vào tên máy, thời điểm biên dịch, thứ tự các file hoàn tất biên dịch trong build song song, v.v.; và hướng này khó hơn rất nhiều
    • Nix là một công cụ khó học
      Nếu chưa quen, thì trong tình huống “tôi muốn thứ gì đó chạy được ngay bây giờ”, nó gần như không phải là công cụ nên chọn
      Khi nói “tái lập được” trong NixOS, ý nghĩa gần hơn là “với cùng mã Nix, có được cùng hành vi chương trình”
      Nó giống với điều mọi người kỳ vọng ở Dockerfile, ở mức nhằm giải quyết các vấn đề như “trên máy tôi thì chạy được” hoặc “lần trước thì chạy được”
      Trong khi đó, “build tái lập được” nhắm tới việc artifact được tạo trên các máy khác nhau giống nhau tới từng bit
      Nhờ vậy có thể xác minh code có được build từ một tập mã nguồn cụ thể hay không, tạo thêm một lớp bảo mật
      Tôi cũng tò mò bạn đã dùng từ khóa tìm kiếm nào khi tìm cấu hình
      Tìm “nixos configuration” thì có các kết quả như https://github.com/search?q=nixos%20configuration&type=repos..., và chỉ riêng Hyprland cũng thấy khá nhiều như https://github.com/search?q=wayland.windowManager.hyprland&t...
  • Nên xem https://github.com/donovanglover/nix-config
    Đây là cấu hình dựa trên Flake, có Hyprland và nhiều thứ khá ổn
    Hiện tại NixOS không phải là công cụ phù hợp cho người yếu tay hoặc người thiếu thời gian
    Hy vọng một ngày nào đó điều này sẽ thay đổi, nhưng nếu chịu khó vượt qua thì sẽ nhận được lợi ích

  • Cần nhớ rằng tính tái lập của Nix / NixOS / Nixpkgs là tính tái lập của nguồn
    Nếu nguồn thay đổi thì bạn sẽ được cảnh báo, nhưng điều đó khác với tính tái lập của binary, vốn có thể khác đi sau mỗi lần build
    Tính tái lập binary của Nix / NixOS / Nixpkgs, ít nhất là về mặt hệ thống, thường không được kiểm thử kỹ
    Guix, Arch Linux, Debian xử lý tính tái lập binary tốt hơn Nix / NixOS / Nixpkgs
    Tài liệu: https://r13y.com/ (Nix*) / https://tests.reproducible-builds.org/debian/reproducible.ht... (Debian) / https://tests.reproducible-builds.org/archlinux/archlinux.ht... (Arch Linux) / https://data.guix.gnu.org/repository/1/branch/master/latest-... (Guix, có thể tải chậm; bản cache là https://archive.is/lTuPk)

    • Trong ngữ cảnh này, “tính tái lập” có hai định nghĩa
      Tính tái lập đầu vào nghĩa là “vô hiệu hóa cache hoàn hảo đối với đầu vào”
      Nix và Guix làm điều này hoàn hảo theo thiết kế, và đôi khi còn gây ra quá nhiều lần rebuild
      Debian và Arch Linux không xem đây là mối quan tâm chính, và xử lý vấn đề gói nào cần build lại khi một file nguồn cụ thể được cập nhật bằng các cách tạm thời như trigger rebuild thủ công
      Tính tái lập đầu ra nghĩa là “quy trình build mang tính xác định và luôn tạo ra cùng một binary”, đây là chủ đề của bài gốc
      Nix build package trong sandbox nên có ích, nhưng không phải giải pháp vạn năng
      Ở điểm này Nix cũng cùng hội cùng thuyền với Debian và Arch Linux
      Trên thực tế, các bản phân phối thường gửi patch giúp tăng tính tái lập lên upstream, và các bản phân phối khác cũng được hưởng lợi
      Trong ngữ cảnh này, https://reproducible.nixos.org là đối ứng với các liên kết khác đã nêu; tôi đồng ý rằng báo cáo của Nix ít chi tiết hơn, nhưng điều đó không có nghĩa tính tái lập binary của Nix tệ hơn
      Nếu đọc thành “Nix chỉ giỏi tính tái lập đầu vào, còn tính tái lập binary thì không giỏi” thì đó là sai
      Chính cột mốc đó mới là điều đang được chúc mừng ở đây
    • Có lẽ nên đọc bài viết
      Bài này không chỉ nói về binary, mà còn nói đến việc tái lập theo từng bit cả cách chúng được đóng gói thành ISO
      r13y.com đã lỗi thời, và nếu nhớ không nhầm thì phần thiếu dưới 1% cũng là do một regression Python ở upstream
      Tính tái lập của bản thân binary, nếu loại trừ phần đóng gói ISO, đã đạt được từ vài năm trước
      Khi đi ra ngoài các package thuộc ISO lõi, việc so sánh trở nên phức tạp
      Cách xử lý package có những khác biệt tinh tế nhưng quan trọng trong ngữ cảnh này; nhiều package kiểu có thể nằm trong AUR của Arch thì ở Nix lại là package thông thường, và phần lớn các package upstream dạng -bin đơn giản là không cần thiết trong Nix
      Nhìn chung Nix giúp dễ tạo các build tái lập hơn, nhưng không phải lúc nào cũng khả thi bất kể Nix, và thường cần patch
      Cộng thêm việc kho package mặc định của Nix có hơn 80.000 package, trong khi Arch có dưới 15.000 nếu không tính AUR, thì so sánh theo phần trăm không hữu ích lắm
      Một hiểu lầm rất phổ biến là nghĩ rằng hash trong đường dẫn Nix store dựa trên output của build, nhưng thực ra nó dựa trên toàn bộ tất cả source và input dùng để build binary trong môi trường cô lập, bất kể đó có phải binary hay không
      Vì vậy nó không đem lại đúng các lợi ích bảo mật mà mọi người kỳ vọng, nhưng đổi lại cho phép cả phần mềm không build tái lập được vẫn có thể dùng dưới dạng phân phối tương đối tái lập, với tính năng, thiết lập compiler, phiên bản dependency, user, cấu hình... giống nhau
    • Tôi nghĩ đó chẳng phải chính là nội dung mà tài liệu đầu tiên và bài gốc đang nói đến sao
      Tức là kiểm tra xem khi build cùng một source trên các máy khác nhau thì binary có giống nhau không
      Điểm chính là binary không thay đổi sau mỗi lần build
      Cách test cũng nói rằng mỗi build được chạy hai lần ở các thời điểm khác nhau, trên phần cứng khác nhau và kernel khác nhau
    • Tôi không biết Arch Linux có kiểm thử tính tái lập
      Nhìn thì thấy ghi là tái lập được 85,6%: https://reproducible.archlinux.org
      Không biết với NixOS, nơi có hơn 80.000 package trong repository chính thức, sẽ cần bao nhiêu công sức
  • Nếu xét mục tiêu hay thực tế của nixpkgs thì điều này hoàn toàn không đúng
    Bài gốc nói về việc tái tạo một ISO tối thiểu dạng nhị phân chứa nhiều gói nhị phân

  • Thật mỉa mai đến buồn cười khi dự án OpenBSD đang miệt mài đi theo hướng hoàn toàn ngược lại
    OpenBSD có các offset địa chỉ duy nhất và được ngẫu nhiên hóa cho mỗi lần cài đặt
    Tôi hiểu rằng hai mục tiêu bản dựng tái lập được và bản cài đặt độc nhất là trực giao với nhau và có thể đạt được đồng thời, nhưng tính hai mặt này vẫn buồn cười

    • Nếu có thể ngẫu nhiên hóa offset địa chỉ bằng một seed được cung cấp thì vẫn có thể chứng minh khả năng tái lập
      Hoặc cũng có thể ngẫu nhiên hóa offset khi chương trình khởi động để tăng bảo mật trong khi vẫn giữ được khả năng tái lập
      Khi đó offset sẽ thay đổi mỗi lần chạy
    • OpenBSD thực hiện linking được ngẫu nhiên hóa tại thời điểm khởi động
      Bản thân gói vẫn có thể tái lập được
      Toàn bộ việc ngẫu nhiên hóa được thực hiện cục bộ sau khi tải gói xuống và xác minh checksum
  • Giờ thì tôi chỉ mong người bảo trì các gói, giống như gần như mọi bản phân phối Linux khác đã làm từ thập niên 90
    Như vậy mọi người mới có thể phần nào biết rằng mã mà họ build là cùng một mã do những cá nhân đã biết gửi lên và rà soát
    Trước khi chữ ký được chuẩn hóa, thật khó tưởng tượng việc dùng Nix trong môi trường production để bảo vệ thứ gì đó có giá trị

    • Tôi nghĩ các maintainer gói Nix gần với việc cung cấp một giao diện hữu ích giúp dễ dàng kết hợp phần mềm hơn
      Phần lớn họ không đưa ra bảo đảm nào về nội dung gói
      Kỳ vọng một bảo đảm có ý nghĩa từ chữ ký của họ giống như kỳ vọng nhân viên giao hàng hỗ trợ sản phẩm vậy
      Cũng không cần phải tin rằng gói không bị đóng gói với ý đồ xấu
      Vì Nix có các bản dựng tái lập được, nếu không muốn phụ thuộc vào cache nhị phân thì cứ xem derivation và tự build
      Nội dung cốt lõi có độc hại hay không rốt cuộc là vấn đề giữa nhà phát triển và người dùng
      Nếu các bản phân phối khác khiến người ta tin điều ngược lại, tôi nghĩ đó gần như là sự gây hiểu lầm
      Ngoại lệ tôi nghĩ đến là Tails, nhưng Tails không rộng như Nix
    • Tôi tò mò bản phân phối nào làm như vậy
      Nhìn thì có vẻ Debian không còn làm thế nữa mà hệ thống build ký vào bản build, Fedora cũng không làm, Arch thì không chắc nhưng có vẻ cũng không
      Hệ thống build của NixOS ký tất cả artifact build bằng khóa riêng của nó và xác minh chữ ký khi tải xuống
      Nếu tiếp cận theo kiểu hoang tưởng, ít nhất Nix cũng giúp dễ dàng build trực tiếp từ mã nguồn mọi thứ
  • Đây là một cột mốc rất ấn tượng, xin chúc mừng những người đã làm được điều này
    Ngay cả khi thực sự build lại ISO vẫn có khác biệt, và nguyên nhân được nói là do các vấn đề còn lại trong cache Hydra và cách ISO được tạo
    Tôi tự hỏi có ai có thể giải thích họ đã sửa “cách ISO được tạo” như thế nào không
    Trước đây tôi từng thử tạo ISO tái lập được nhưng không thể khiến hệ thống tệp tạo extent một cách quyết định

    • Với NixOS, phần đó nằm trong mục “đã tái lập như thế nào” của bài viết
      Bước cuối của quá trình đó tạo ISO trong thư mục ./result/iso
      Có vẻ thứ bạn đang tìm là lệnh mà bản build đó gọi, nhưng tôi không rõ bạn muốn tìm bước nào
      Ví dụ lời gọi xorriso nằm ở đây: https://github.com/NixOS/nixpkgs/blob/master/nixos/lib/make-...
  • Để làm được việc này chẳng phải phải đánh lừa thời gian hệ thống sao?
    Thời gian thường đi vào binary theo cách này hay cách khác

    • Thực tế timestamp có lẽ là nguồn phi quyết định phổ biến nhất
      Phổ biến đến mức nhiều compiler đã triển khai SOURCE_DATE_EPOCH, một biến gần như là chuẩn trên thực tế để giả lập timestamp: https://reproducible-builds.org/docs/source-date-epoch/
    • Tôi tò mò liệu bạn có thể đưa ví dụ chuyện đó xảy ra theo cách nào và vì lý do gì không
    • Chỉ cần hoàn toàn không đưa timestamp vào mọi bản build, hoặc đặt về 0 để ngày build ở đâu cũng thành năm 1970
  • Điều này chẳng phải giúp giải quyết vấn đề Ken Thompson viết trong “Reflections on Trusting Trust” sao?
    Nếu có thể bootstrap hoàn toàn toàn bộ hệ thống từ mã nguồn, có vẻ sẽ khó hơn để cài những thứ như compiler bị cắm backdoor

    • Thực ra là có giúp, nhưng không phải một “giải pháp” hoàn chỉnh
      Về lý thuyết vẫn có thể có một backdoor tinh vi trong môi trường nơi ISO được build
      Nếu thật sự muốn giải quyết vấn đề đó, có thể xem Diverse Double Compiling(https://dwheeler.com/trusting-trust/) hoặc bootstrap toàn bộ môi trường(https://bootstrappable.org/)
      Mục “cách tiếp cận trên có vấn đề bootstrap không?” trong bài viết cũng liên quan
      Dù vậy, chỉ riêng việc tái lập bản build cũng đã giúp rất nhiều trong việc khiến kiểu tấn công đó ngày càng ít khả thi hơn
  • Gần đây vì công việc nên tôi ở trong hệ sinh thái Red Hat
    Tôi tò mò điều này so với những thứ như Fedora Silverblue, Ansible, Fedora Silverblue + Ansible thì thế nào

    • Trong hệ sinh thái Fedora, thứ gần nhất với NixOS ISO builder và khả năng tái lập của nó là osbuild / imagebuilder: https://www.osbuild.org/guides/introduction.html
      Imagebuilder tuyên bố có khả năng tái lập, nhưng theo tôi biết thì phần lớn rpm được cài dưới dạng gói nhị phân chứ không phải từ mã nguồn
      Vì vậy nếu tất cả gói đầu vào cũng không tái lập được thì đó không phải khả năng tái lập theo nghĩa nghiêm ngặt
      Nếu phần giải thích về build gói từ mã nguồn, tạo image bản phân phối và xử lý khả năng tái lập không thật sự hợp với bạn thì có lẽ bạn không phải độc giả mục tiêu chính
    • Nix là một OS khai báo, mô tả OS nên trông như thế nào
      Trong khi đó Ansible chỉ định các bước mà OS cần làm theo

Silverblue và Nix, ngoài điểm chung là đều là bản phân phối Linux, thì về bản chất là hai hướng tiếp cận độc lập với nhau
Silverblue là một nỗ lực nhằm thay đổi cách phân phối phần mềm bằng cách chỉ dùng container trên một host bất biến
Nếu bạn đang tìm một lựa chọn thay thế Ansible mô phỏng Nix ở một mức độ nào đó bằng Jsonnet và theo dõi trạng thái, Etcha đáng để xem qua: https://etcha.dev

  • Ansible áp dụng các thay đổi khả biến lên OS theo từng đơn vị tác vụ
    Nix thì bất biến
    Thay đổi mới được tạo hoàn toàn mới, và chỉ sau khi build thành công thì tất cả package mới được “liên kết tượng trưng” vào hệ thống hiện tại
    Fedora Silverblue dựa trên ostree https://github.com/ostreedev/ostree
    Nó hoạt động giống git đối với cây root, nhưng để áp dụng thay đổi thì phải khởi động lại toàn bộ hệ thống
    Nix dùng cách liên kết tượng trưng các package, nên không cần khởi động lại hệ thống
    Có giải thích chi tiết hơn ở đây: https://dataswamp.org/~solene/2023-07-12-intro-to-immutable-...