2 điểm bởi GN⁺ 2024-12-22 | 1 bình luận | Chia sẻ qua WhatsApp
  • Scylla đề xuất một lộ trình cho phép biên dịch trực tiếp sang Rust an toàn bằng cách dần cấu trúc lại mã C gốc, thay vì chuyển C hiện có sang Rust unsafe rồi mới sửa tiếp
  • Mục tiêu là tập con applicative của C bao gồm xử lý dữ liệu, phép toán con trỏ, luồng điều khiển có cấu trúc và mã có tính di động; loại trừ mã phụ thuộc vào goto, ép kiểu số nguyên-con trỏ, mẹo con trỏ, bitfield và untagged union
  • Việc chuyển đổi gồm 2 bước: hạ Clang AST xuống Mini-C rồi chuyển sang Rust an toàn, đồng thời làm tường minh các hành vi tinh vi của C như integer promotion, chuyển đổi ngầm định, biểu thức gán và tăng trước/sau
  • Phần khó nhất là số học con trỏ, được thay bằng cách phân tách dựa trên Rust slice và split_at_mut/split_at; đồng thời cũng xử lý suy luận tính khả biến, suy ra trait và lựa chọn giữa Box với borrow
  • Nhóm đã đánh giá trên SymCrypt của Windows, HACL★, thuật toán nén lõi của bzip2, parser/serializer CBOR của EverParse và một phần Microsoft FrodoKEM; trong quá trình chuyển đổi cũng phát hiện undefined behavior ở bzip2 và FrodoKEM

Lộ trình chuyển đổi C→Rust mà Scylla chọn

  • Động lực chuyển mã C hiện có sang Rust bắt nguồn từ vấn đề an toàn bộ nhớ
    • Nghiên cứu của Google và Microsoft ước tính 70% lỗ hổng bảo mật liên quan đến xử lý bộ nhớ sai
    • Doanh nghiệp và chính phủ đang khuyến nghị dùng các ngôn ngữ an toàn bộ nhớ như Rust cho những hệ thống quan trọng về an toàn
  • Rust có lợi thế rõ ràng cho mã mới, nhưng rất khó viết lại toàn bộ mã C công nghiệp đã được kiểm thử và debug
  • Các công cụ chuyển C→Rust tự động hiện nay phần lớn xuất ra Rust unsafe để hỗ trợ toàn bộ C
    • Chúng cho phép các thành ngữ kiểu C như unchecked pointer hay transmutation của Rust
    • Khi đó bảo đảm an toàn bộ nhớ tĩnh biến mất, làm suy yếu mục tiêu dùng một ngôn ngữ an toàn
  • Quy trình phổ biến là lấy đầu ra Rust unsafe làm điểm khởi đầu, rồi refactor lặp đi lặp lại sang Rust an toàn
    • Đã có các phân tích tĩnh được đề xuất để đổi raw pointer sang borrow an toàn của Rust, hoặc khôi phục các trừu tượng Rust từ biểu diễn mức thấp
    • Tuy nhiên các công cụ refactor còn rời rạc, và hỗ trợ c2rust refactor đã dừng từ năm 2022
  • Thay vì sửa Rust unsafe đã sinh ra, Scylla chọn cách dần cấu trúc lại chính mã C gốc để có thể biên dịch sang Rust an toàn

Tập con C được hỗ trợ và các mẫu bị loại trừ

  • Mục tiêu của Scylla là tạo ra phép chuyển đổi có thể dự đoán được và sinh mã Rust gần với C gốc
  • Đối tượng được hỗ trợ là tập con applicative của C
    • Mã thao tác và xử lý dữ liệu
    • Mã dùng số học con trỏ
    • Mã có luồng điều khiển có cấu trúc
    • Mã có tính di động
  • Những codebase phụ thuộc vào các mẫu sau sẽ không được hỗ trợ
    • goto
    • Sử dụng biểu diễn đối tượng thông qua ép kiểu số nguyên-con trỏ
    • Mẹo con trỏ
    • Bitfield
    • Untagged union
  • Lập trình viên có thể áp dụng rewrite có chủ đích và annotation lên mã C để Scylla hiểu được
    • Có thể viết lại các mẫu aliasing không phù hợp với borrow checker của Rust
    • Có thể cung cấp cho Scylla thông tin rằng tagged union nên được dịch thành ADT mức cao

Mini-C: ngôn ngữ trung gian giảm bớt tính mơ hồ của C

  • Scylla bắt đầu từ AST frontend của Clang rồi trước tiên chuyển sang một ngôn ngữ tên là Mini-C
  • Mini-C xử lý nhánh, vòng lặp, con trỏ, dereference và lấy địa chỉ giống C, nhưng có ngữ nghĩa “no-surprises”
    • Mọi số nguyên đều có độ rộng cố định
    • Integer promotion và integer conversion của C được biểu diễn bằng cast tường minh
    • Không cho phép untyped pointer như void *
  • Khác với C, Mini-C là ngôn ngữ biểu thức
    • Phép gán không trả về giá trị
    • Các cú pháp C như e1 = e2 = e3, p[i++] sẽ được desugar trong Mini-C
    • Biểu thức kiểm tra trong vòng lặp và câu điều kiện phải có kiểu bool, không phải int như trong C
  • Khi chuyển typed AST của Clang sang typed AST của Mini-C, Scylla làm tường minh các hành vi ngầm của C
    • Điều chỉnh biểu thức điều kiện về bool
    • Điều chỉnh chỉ số mảng về size_t
    • Đổi các chuyển đổi ngầm trong đối số gọi hàm và vế phải của phép gán thành cast tường minh
    • Phản ánh usual arithmetic conversions của chuẩn C trong các phép toán số học
  • Việc chuyển đổi giả định rằng mã C là có tính di động và không phụ thuộc vào mô hình dữ liệu của C
    • Ví dụ, không kỳ vọng có mã hoạt động khác nhau tùy long là 4 byte hay 8 byte
    • Ở thời điểm configure, hệ thống dò mô hình dữ liệu của kiến trúc đích và chuyển unsigned int thành kiểu độ rộng cố định như uint32_t

Tổng hợp ADT và tuple

  • Mini-C cung cấp ADT, tuple và pattern matching ở mức cao hơn C
  • Tagged union được dịch thành ADT thông qua annotation
    • Dạng đích là cấu trúc như { int tag; union { t0 case0; ...; tn caseN }}
    • Giả định giá trị tag chạy từ 0 đến N, và giá trị tag khớp với thứ tự các case trong union
  • Scylla chuyển kiểu tagged union đã annotate thành kiểu variant
    • Khi tạo giá trị, nó kiểm tra .tag = i.casej = e có khớp nhau không
    • Nếu khớp, nó chuyển thành giá trị constructor tương ứng
    • Nếu payload và tag không khớp, nó sẽ không dịch sang Mini-C
  • Việc truy cập field của tagged union chỉ an toàn khi biết trạng thái tag hiện tại
    • Scylla nhận diện các mẫu như if (x.tag == i) { ... x.casei } hoặc switch
    • Sau đó chuyển thành dạng match x with | Ci v -> ...
    • Truy cập case khác của union bị xem là invalid và gây lỗi chuyển đổi
  • Tuple cũng có thể được tổng hợp bằng annotation
    • Struct có n field sẽ được chuyển thành tuple n ngôi
    • Truy cập field sẽ đổi thành truy cập field của tuple
    • Tuple được định kiểu theo cấu trúc nên có lợi thế về mut-polymorphism

Chuyển từ Mini-C sang Rust an toàn

  • Mini-C cung cấp biểu diễn chương trình C đầy đủ với annotation kiểu, sau đó được chuyển sang Rust an toàn
  • Có ba khó khăn chính
    • Loại bỏ số học con trỏ của C
    • Làm tường minh tính khả biến và aliasing
    • Tự động cung cấp các cấu trúc thành ngữ Rust như trait
  • Việc chuyển kiểu con trỏ khá phức tạp vì Rust biểu diễn con trỏ khác C
    • Rust phân biệt Box<T>&T
    • Rust cũng phân biệt con trỏ một phần tử và con trỏ nhiều phần tử như &T&[T]
    • Mảng trong Rust là giá trị và không tự động decay thành con trỏ như C
  • Chiến lược cơ bản là biên dịch mọi kiểu pointer của C thành slice borrow &[T] trong Rust
    • Cả con trỏ trên stack lẫn heap về cơ bản đều thành slice borrow
    • Cả con trỏ một phần tử và nhiều phần tử cũng mặc định thành slice borrow
    • Tính khả biến sẽ được suy luận tự động ở một bước riêng
  • Scylla dùng heuristic và annotation thủ công để dịch một số con trỏ thành Box<T>
    • Các hàm như T *create() được xem là tạo allocation mới, không có tham chiếu toàn cục, có thể được dịch thành fn create() -> Box<T>
    • Phân tích này được áp dụng đệ quy theo kiểu fixed-point vào cả định nghĩa struct và variant
    • Các struct còn chứa borrow sẽ có lifetime parameter

Ràng buộc của chuyển đổi giữa Box, slice và array

  • Trong Rust, cần chuyển đổi tường minh giữa mảng, slice borrow và Box, vì vậy bước chuyển sang Rust của Scylla hoạt động theo hướng kiểu
  • Các quy tắc chuyển đổi sẽ chèn coercion để đổi mảng hoặc boxed slice thành slice borrow
    • Mảng sẽ thành slice borrow theo dạng như &x[..]
    • Boxed slice có thể được đổi thành borrow
  • Cũng có thể chuyển theo chiều ngược lại
    • Slice hoặc mảng có thể được nâng thành allocation trên heap để đổi thành Box<[T]>
  • Việc chuyển ngược này có thể tạo ra khác biệt về ngữ nghĩa sao chép
    • Trong C, mảng và con trỏ có thể trỏ cùng một vùng nhớ
    • Trong Rust, Box::new(x) có thể tạo ra một bản sao của x
    • Các mảng kiểu cơ bản như mảng số nguyên không có cách opt out khỏi trait Copy, nên Rust có thể âm thầm sao chép
  • Khi việc chuyển đổi này xảy ra, Scylla loại biến gốc khỏi môi trường để cấm dùng về sau
    • Nếu chương trình C gốc vẫn tiếp tục dùng biến đó, sẽ phát sinh lỗi chuyển đổi
    • Lập trình viên cần sửa mã C trước khi chuyển để thể hiện ý định rõ ràng hơn

Biến số học con trỏ thành tách slice trong Rust

  • Chương trình C thường không chỉ truy cập mảng qua một base pointer duy nhất, mà còn chia mảng thành chunk hoặc duy trì con trỏ vị trí hiện tại để duyệt
  • Rust không cho phép số học con trỏ tùy ý, mà cung cấp cách tách slice bằng split_at_mut hoặc split_at
    • split_at_mut là primitive từ bỏ quyền sở hữu slice gốc để lấy hai sub-slice
    • Điều này giữ được bất biến của Rust rằng dữ liệu mutable phải có một owner duy nhất
  • Để khớp số học con trỏ của C với cách phân tách trong Rust, Scylla đưa vào split tree
    • Mỗi con trỏ C được ánh xạ tới một split tree
    • Split tree thay đổi phụ thuộc vào luồng điều khiển
    • Nó theo dõi tại một điểm chương trình cụ thể thì truy cập con trỏ C phải được đổi thành truy cập slice Rust nào
  • Vì con trỏ C không có thông tin độ dài, Scylla giả định các chunk không chồng lấp lên nhau
    • Nếu chủ đích là chồng lấp, kiểm tra kiểu của Rust sẽ không cho qua và lập trình viên phải viết lại mã C
    • Để phép chuyển đổi có thể dự đoán được, hệ thống tránh backtracking và xử lý theo hướng tiến
  • Trong ví dụ, mảng 32 byte abcd được chia thành bốn vùng limb, mỗi vùng 8 byte
    • Trong C, người ta dùng số học con trỏ như abcd + 0, abcd + 16, abcd + 8, abcd + 24, không theo thứ tự trái sang phải
    • Bản dịch Rust lưu lịch sử gọi split_at_mut trong split tree để tìm đúng sub-slice

Các đối tượng đánh giá và undefined behavior được phát hiện

  • Triển khai Scylla dùng Clang để nhận đầu vào là mã C hiện có và xuất ra Rust an toàn
  • Tập đánh giá bao gồm một phần của nhiều dự án C hiện có
    • Một phần của SymCrypt trên Windows
    • Một phần thư viện mật mã HACL★
    • Phần lõi của thuật toán nén bzip2
    • Parser và serializer nhị phân CBOR của thư viện EverParse
    • Triển khai primitive mật mã hậu lượng tử FrodoKEM của Microsoft
  • Các trường hợp này cho thấy tập con applicative của C mà Scylla nhắm tới có thể bao phủ nhiều ứng dụng nhạy cảm về bảo mật
  • Trong quá trình chuyển đổi, nhóm cũng nhận diện và báo cáo undefined behavior tồn tại trong mã C gốc của bzip2 và FrodoKEM

1 bình luận

 
GN⁺ 2024-12-22
Các ý kiến trên Hacker News
  • Điều quan trọng là công trình này nhắm tới “các codebase C đã được kiểm chứng hình thức
    Mã C hệ thống nói chung không được kiểm chứng hình thức, nên đó là một câu chuyện khá khác

    • Dù vậy có vẻ cũng không thể tin cậy hoàn toàn. Ngay cả mục 2.2 của bài báo cũng nói rằng các ràng buộc do quy tắc chuyển đổi đưa vào có thể tạo ra khác biệt ngữ nghĩa tinh vi
      Ví dụ, một con trỏ trỏ tới mảng C trên stack có thể được dịch trong Rust thành Box<[u8]>, giống như một con trỏ sở hữu bản sao mới trên heap. Nếu mã gốc phụ thuộc vào việc con trỏ thực sự trỏ tới mảng đó, mã đã dịch có thể âm thầm hoạt động sai
      Với tính năng tự động dịch tập con C++ an toàn bộ nhớ trong dự án scpptool của tôi, có lẽ nó sẽ xử lý bằng cách chuyển mảng sang các kiểu thay thế và iterator để giữ nguyên ngữ nghĩa ban đầu
      Dự án của OP có thể chỉ xử lý những đoạn C dễ chuyển sang Rust an toàn, nhưng xét độ khó của vấn đề thì kết quả vẫn đáng được tôn trọng và cũng cho thấy một mức hữu ích nhất định
    • Cái này có quá nhiều lưu ý, gần như là quảng cáo thổi phồng
      Ngay từ đầu, họ không dịch C thực tế, mà sửa để mã viết bằng F* được phía compiler C xuất ra Rust. Họ không đối mặt với mã C thực tế phức tạp, cùng lắm chỉ xử lý một Mini-C bị giới hạn kiểu như thứ mà một compiler đồ chơi có thể phát ra
      Ngay trong bài gốc cũng nói rằng nếu chương trình C ban đầu phụ thuộc nhiều hơn vào x thì bản dịch sẽ báo lỗi và yêu cầu lập trình viên sửa mã nguồn; điều đó có nghĩa là họ mong C đã được viết sẵn theo phong cách làm hài lòng borrow checker của Rust
      Nó giống kiểu diễn đạt học thuật “Hình 4 trình bày các quy tắc đẹp đẽ, nhưng triển khai thực tế dựa vào vô số mẹo”
      Tệ hơn nữa, họ nói các trường hợp chồng lấp có thể phân biệt tĩnh sẽ gây lỗi biên dịch, còn nếu không thì mã Rust có thể panic khi chạy. Việc biến một chương trình C đã được kiểm chứng hình thức thành một chương trình Rust “giờ có thể crash” là khá kỳ lạ
      Gọi HACL* là một codebase C đã được kiểm chứng hình thức hiện có cũng không chính xác. HACL* tuy được biên dịch sang C, nhưng không phải là thư viện C mà được viết bằng một ngôn ngữ hoàn toàn khác
      Tiêu đề trung thực đáng lẽ nên là “Biên dịch một tập con của F* sang Rust an toàn một phần, được hình thức hóa một phần”
    • Bản thân Rust đã được kiểm chứng hình thức chưa? Theo tôi biết thì chưa
    • Tôi tò mò C được kiểm chứng hình thức chính xác là gì, và tại sao không có nhiều hơn
    • Tôi tò mò khác biệt cốt lõi là gì. Có thể dùng compiler flag để cưỡng chế tuân thủ không?
  • Năm 2002, các nhà nghiên cứu đã công bố bài báo về Cyclone, một phương ngữ an toàn của C, và khi port thủ công mã C sang Cyclone, họ đã phát hiện các lỗi an toàn trong mã C hiện có
    Những chuyển đổi C kiểu thủ công hoặc tự động như vậy không chỉ có tiềm năng tăng mức độ chấp nhận các ngôn ngữ an toàn hơn, mà còn giúp lộ ra các bug hiện có
    [1] https://www.researchgate.net/profile/James-Cheney-2/publicat...

    • Cyclone không còn được hỗ trợ, dự án nghiên cứu cốt lõi cũng đã kết thúc, và các developer đã chuyển sang việc khác
      Nhiều ý tưởng của Cyclone đã đi vào Rust, và mã vẫn có thể làm cho chạy được nếu bỏ công sức, nhưng không build ngay được trên các nền tảng 64-bit hiện đại
      http://cyclone.thelanguage.org
  • Tôi đã thử port một vài dự án, gồm cả dự án C, sang Rust bằng C2Rust làm bước đầu tiên, và rút ra vài kết luận

    1. Khi chuyển chương trình C sang Rust, ngay cả khi có unsafe, các ràng buộc mạnh của Rust, chẳng hạn kiểm tra biên và signature nghiêm ngặt, thường khiến bug lộ ra nhanh chóng
    2. Tôi cho rằng chuyển đổi C→Rust tự động không thể được giải quyết hoàn toàn. Thiết kế của chương trình C về cơ bản khác Rust, và để làm cho an toàn cần tái thiết kế đáng kể
    3. Trong một số trường hợp, việc chuyển từ C sang Rust mà vẫn bảo toàn chính xác ngữ nghĩa là bất khả thi, vì tính không an toàn có thể nằm ngay trong thiết kế
      Dù vậy, công cụ là thiết yếu cho việc port, và công cụ càng tiến bộ thì quá trình sẽ càng trơn tru hơn
    • Tự động chuyển đổi sang “Rust nhanh và an toàn” thì khó, nhưng tự động chuyển đổi sang Rust an toàn nói chung thì dễ hơn nhiều
      Chỉ cần biểu diễn bộ nhớ như một mảng và coi con trỏ là chỉ số trong mảng đó. Khi đó có thể biểu diễn các hành vi của C như số học con trỏ không kiểm tra hay union mà không phải đấu với borrow checker, đồng thời vẫn bảo toàn ngữ nghĩa. Trong C→Java, kỹ thuật tương tự đã được dùng từ lâu
      Tất nhiên giá trị của kiểu chuyển đổi này khá mơ hồ. Về cơ bản nó giống biên dịch C sang wasm nhưng chậm hơn, và dù mã kết quả về mặt kỹ thuật là “an toàn”, các vấn đề vẫn còn: buffer overflow có thể tạo ra trạng thái sai, hoặc dangling pointer có thể cho phép truy cập dữ liệu trong ngữ cảnh lẽ ra không được phép
    • Về nguyên tắc tôi đồng ý với câu “tính không an toàn có thể nằm trong thiết kế”, và theo kinh nghiệm của tôi cũng cảm thấy rất rõ như vậy, nhưng sẽ tốt hơn nếu có một ví dụ đơn giản để làm cuộc thảo luận cụ thể hơn
  • Tôi là tác giả. Có lẽ sẽ hữu ích nếu tổng hợp vài điểm từ nhiều luồng thảo luận

    1. Đây là bài báo học thuật đăng lên arxiv, không phải công bố sản phẩm mới tuyên bố đã giải quyết vấn đề C→Rust. Nó được nộp cho một hội nghị PL, nên đối tượng độc giả và kỳ vọng khác với một bài trình bày tại sự kiện mã nguồn mở như FOSDEM
    2. Cách tiếp cận khá đơn giản. Xuất phát từ ràng buộc dịch C sang Rust an toàn, chúng tôi xem xét việc cần có một tập con nhỏ của C hoạt động tốt, suy luận phân tách slice, bản dịch có thể báo lỗi, chương trình có thể bị dừng, v.v. Chúng tôi đánh giá trên đối tượng mà mình có, tức C được nhúng trong F*, và cho thấy dưới ràng buộc này nó mở rộng khá tốt trên các thư viện C quy mô lớn dùng trong phần mềm phổ biến như Firefox, Python, v.v. Chúng tôi không tuyên bố có thể tự động viết lại Firefox bằng Rust
    3. Nghiên cứu vốn diễn ra như vậy. Chúng tôi thấy có một điểm thú vị trong không gian thiết kế, không tuyên bố giải quyết mọi vấn đề, nhưng nghĩ đây là một ý tưởng có thể mở ra tiến bộ tiếp theo trong lĩnh vực dịch C→Rust. Một công cụ hiện có nào đó cũng có thể dùng cách tiếp cận này cho phần mã phù hợp với tập con, còn phần không phù hợp thì fallback sang unsafe Rust
    4. Đây chưa phải bản cuối. Chúng tôi đang xây dựng frontend C thực tế bằng libclang, đồng thời cũng đang tìm cách bảo đảm Rust sinh ra không tạo truy cập ngoài phạm vi. Chẳng hạn, chúng tôi đang nghĩ tới việc xuất các điều kiện kiểm chứng sang Z3. Nếu reviewer cho rằng cần thêm nhiều việc, chúng tôi sẽ cải thiện rồi nộp lại; còn nếu họ thấy lĩnh vực này đang sôi động và người khác có thể hưởng lợi từ ý tưởng này nên chấp nhận bài thì càng tốt
  • Điều tôi thật sự thắc mắc là tại sao phải làm theo hướng này
    Nếu đây là công nghệ có thể thật sự chuyển đổi ứng dụng công nghiệp từ C sang Rust, thì có vẻ cũng có thể giúp bọc thép các ứng dụng C hiện có dễ hơn. Chỉ cần tạo ra các phân tích để đưa vào công cụ hiện có như bộ phân tích tĩnh hoặc trình sinh test
    Tương tự, cũng có thể sinh wrapper an toàn để cho phép viết mã mới bằng Rust bên cạnh phần C đã được kiểm chứng. Mã mới hưởng lợi từ Rust, mã cũ được xác nhận là an toàn, và giao diện cũng trở nên an toàn hơn
    Một trình dịch hoàn chỉnh có thể là lý tưởng. Vì về lâu dài, codebase dùng một ngôn ngữ vẫn tốt hơn. Nhưng với C/C++ hiện có, nhu cầu lớn nhất vẫn là bảo đảm an toàn chỉ bằng một nút bấm với ít cảnh báo sai. Cũng có thể tự động sửa các cấu trúc xấu ngay trong C, giống công cụ compiler của Google hay Mayhem của ForAllSecure

    • Một số chương trình C không thể làm cho an toàn, nên câu “nếu có công nghệ chuyển ứng dụng công nghiệp sang Rust thì có thể bọc thép ứng dụng C dễ hơn” là không đúng
      Có thể vì chúng dựa vào hành vi không xác định hoặc hành vi không được đặc tả, hoặc vì nếu thêm các kiểm tra an toàn phù hợp thì miền đầu vào được chấp nhận sẽ bị thu hẹp quá nhiều đến mức vô dụng
      Dịch sang một ngôn ngữ an toàn trong các trường hợp như vậy khách quan là tốt hơn, vì có thể giữ được sức biểu đạt của đầu vào trong khi bảo đảm tĩnh hành vi đúng khi chạy
      Khái niệm “C đã được chứng minh ngoài thực địa” cũng khó có thể coi là tồn tại, như vô số lỗ hổng nghiêm trọng đã cho thấy. Thứ thực sự tồn tại chỉ là C hoạt động đủ tốt, đủ thường xuyên để trông có vẻ hữu ích
      Mã cũ thường chỉ được giả định là an toàn nhờ may mắn, chứ không được chứng minh. “Chứng minh” có ý nghĩa cụ thể, nhất là trong ngữ cảnh các bài báo như thế này, và đại đa số mã C không được chứng minh theo tiêu chuẩn toán học nghiêm ngặt. Trong khi đó, hệ thống kiểu của Rust đã được chứng minh là đúng về mặt toán học
      Một trình dịch toàn bộ phụ thuộc vào việc có thể từ bỏ điều gì. Nếu chấp nhận hy sinh hiệu năng, miền đầu vào, phạm vi đầu ra, độ dễ đọc của mã, v.v. thì có thể làm được ở mức nào đó; nhưng khi bắt đầu muốn một trình dịch vừa sound vừa complete trên mọi phương diện này thì vấn đề sẽ xuất hiện
  • Nếu dịch ngây thơ sang Rust thì chẳng phải sẽ trộn lẫn phần an toàn và phần unsafe sao? Khi đó công việc thủ công có vẻ chỉ cần kiểm tra tính an toàn của các vùng unsafe. Gần giống như khi viết Rust từ đầu
    Nếu 90% kết quả không phải unsafe thì có vẻ đã là lợi ích khá lớn

    • Thực tế là đúng vậy. Đã từng có người dùng c2rust chuyển OpenJPEG thành unsafe Rust cấp thấp
      OpenJPEG vốn được biết là segfault với một test case nhất định, và khi chạy test đó trên bản Rust thì nó segfault ở đoạn mã Rust tương ứng cùng vị trí. Ít nhất là tương thích
      Nhưng cách tiếp cận đó là ngõ cụt. Muốn tiến lên, trình dịch phải nhận diện các idiom phổ biến của C và nâng chúng lên dạng tự nhiên của ngôn ngữ đích. “Biên dịch” sang Rust sẽ tạo ra thứ Rust kinh khủng, đầy các lời gọi hàm thao tác con trỏ kiểu C không an toàn
      Vấn đề nâng cấp lớn nhất phần lớn liên quan đến con trỏ. Thành quả hứa hẹn nhất của bài báo này là đã tìm ra cách thay số học con trỏ C bằng slice của Rust. Slice có thể làm hầu hết những gì số học con trỏ C làm, và giờ đã có người tự động hóa bản dịch đó. Những phép toán con trỏ không thể dịch được nên bị nhìn nhận rất đáng nghi
      Sẽ hữu ích nếu nghĩ rằng con trỏ thô trỏ tới mảng trong C ngầm có kèm độ dài. Độ dài đó không hiện ra trong mã nguồn C, nhưng tồn tại đâu đó như một hàm của trạng thái chương trình. Nó có thể là hằng số, kích thước yêu cầu malloc, hoặc tham số hàm. Với lập trình viên bảo trì, việc tìm độ dài mảng thường không quá khó
      Đây có thể là bài toán phù hợp với LLM. Kiểu như hỏi “hãy nhìn đoạn mã này và tìm độ dài của mảng foo là bao nhiêu”, rồi để một trình dịch không phải LLM dùng câu trả lời đó để dẫn hướng chuyển đổi sang Rust. Nếu LLM sai, Rust sẽ báo lỗi chỉ mục hoặc có mảng quá lớn, nhưng sẽ không mất an toàn. Các idiom biểu diễn thông tin kích thước mảng trong C đủ khuôn mẫu để phần lớn có thể đoán đúng. Đặc biệt, LLM còn có thể đọc cả chú thích
    • Dịch ngây thơ gần như sẽ tạo ra mã Rust gần như toàn bộ là unsafe. Vì nó sẽ dùng con trỏ thô khắp nơi thay vì reference
      Mã C không được viết với mô hình aliasing và các ràng buộc của borrow checker trong Rust, nên rất khó dịch thành reference
  • Chỉ là biên dịch một tập con rất nhỏ của C mà thôi. Trên thực tế có thể nhỏ đến mức vô dụng
    Tôi không kỳ vọng nhiều vào cách tiếp cận này. Chắc chắn sẽ đụng phải giới hạn của những gì có thể làm bằng phân tích tĩnh mã C. Hơn nữa, việc chọn Rust làm đích khiến vấn đề khó một cách không cần thiết. Mô hình sở hữu của Rust quá khác với cách các chương trình C thực tế vận hành

    • Mô hình sở hữu của Rust đủ gần để dịch C. Nó chỉ có kiểu tường minh và mạnh hơn, nên bộ dịch phải hiểu mã C tự do đang muốn làm gì rồi ánh xạ sang các idiom của Rust
      Ví dụ, buffer trong C đương nhiên có độ dài, nhưng trong C độ dài không được gắn tường minh với con trỏ. Vì vậy bộ dịch phải suy luận chương trình C theo dõi độ dài như thế nào rồi chuyển thành slice. Ngay cả khi độ dài là một biến tường minh cũng không dễ, và sẽ còn rắc rối hơn nếu độ dài được tính toán hoặc được biểu diễn dưới dạng “con trỏ sau phần tử cuối”
      Các pattern C như bool should_free_this_pointer cũng có thể chuyển sang enum Owned/Borrowed của Rust, nhưng phải suy luận allocation nào liên kết với boolean nào, và phạm vi an toàn thực sự của biến thể borrowed nằm ở đâu
    • Có thể tốt với vai trò ngôn ngữ giao diện. Hữu ích cho binding
    • Rốt cuộc có lẽ mọi người sẽ ném LLM vào, rồi nói rằng việc hallucinate hàng loạt mã trông có vẻ đúng cũng không sao
      Dù vậy tôi đồng ý rằng tạo Rust đúng idiom từ C tùy ý sẽ rất khó. Có thể nói chỉ đạt mức “đại khái đúng”
  • Tôi tò mò việc này so với tính năng chuyển đổi C của Zig như thế nào
    Zig có vẻ rất giỏi trong việc tạo môi trường hỗn hợp: mã mới viết bằng Zig, mã cũ giữ bằng C, thực hiện chuyển đổi hoặc tương tác, và thậm chí đóng vai trò trình biên dịch C
    Chắc hẳn có lý do rất tốt khiến các maintainer nhân Linux không xem Zig là phương án thay thế C thay vì Rust. Tôi không biết đủ để suy đoán, nên mong những người hiểu rõ hơn giải thích

    • Rust không hẳn là “phương án thay thế C”, mà là một công cụ được bổ sung vào C. Đó là công cụ mà Torvalds và những người khác nhìn thấy giá trị nên cho phép đưa vào kernel, còn phần lớn mã kernel sẽ tiếp tục được viết bằng C
      Tôi không phải maintainer kernel, nhưng nếu đoán hai lý do lớn khiến Rust được chọn hơn Zig, có thể là vì ngôn ngữ này cung cấp đảm bảo tại thời điểm biên dịch tốt hơn và tốc độ chấp nhận cũng nhanh hơn
      Các công ty lớn trong ngành đang làm rất nhiều để cung cấp mã Rust native cho API hoặc các binding Rust được duy trì. Các nhà phát triển Windows cũng đang viết lại một phần kernel của họ bằng Rust. Đây là một chuyển động đã kéo dài khá lâu, và tôi hy vọng nó không dừng lại
      Các maintainer có thể cảm thấy Zig không đem lại đủ lợi ích so với C. Nhiều người trong số họ vẫn còn phản đối cả Rust
    • Theo tôi hiểu, phần lớn maintainer kernel không muốn thay C bằng bất cứ thứ gì
      Zig có khả năng tương tác với C tốt hơn Rust rất nhiều, nhưng không an toàn bộ nhớ và cũng chưa ổn định. Việc Zig được chấp nhận trong thế giới C có thể sẽ tăng đáng kể, nhưng khó xem là cạnh tranh trực tiếp với Rust
      Ở khu vực của tôi, không ai áp dụng Rust, còn những người C++ vẫn ở lại với C++. Ban đầu cũng có chút quan tâm đến Rust, nhưng nó không bám rễ được ở bất kỳ công ty nào tôi biết. Có thể vì lý do tương tự như việc Go tăng trưởng mạnh ở các công ty trẻ nhưng không thâm nhập tốt vào các công ty Java/C# truyền thống. Dù có hợp lý về mặt kỹ thuật, đó vẫn là một bài toán quản lý thay đổi khổng lồ
      Zig đang có đà ở phía các chương trình không cần cấp phát bộ nhớ động, nhưng ngoài đó ra thì chưa nhiều
    • Zig vẫn chưa đủ trưởng thành để được cân nhắc cho kernel
      Vẫn thường xuyên có các thay đổi phá vỡ tương thích; điều đó tốt cho Zig hiện tại, nhưng không tốt cho một codebase khổng lồ và sống lâu như Linux. Lỗi trình biên dịch cũng xuất hiện
      Tôi nói vậy dù nhìn chung thích hướng đi của Zig
    • Zig vẫn chưa đạt 1.0 và hoàn toàn không có đảm bảo tương thích ngược. Hầu như chưa được dùng ở đâu, và dù một số phần có vẻ hứa hẹn, nó vẫn chưa chứng minh được giá trị
    • Có thể là vì Zig không an toàn bộ nhớ
  • Tôi tò mò liệu một công cụ như C2Rust có thể tận dụng việc này để tạo ra mã đúng về mặt hình thức hay không
    Tôi cũng tò mò các tác giả đã làm thủ công đến mức nào, hay họ có chạy thứ gì đó để sinh mã Rust không. Nếu có thì tôi không biết mã tạo ra Rust nằm ở đâu, và cũng không thấy liên kết tới kho nguồn

    • Trong bài báo có nói rằng sau khi quá trình review kết thúc, tức nhìn chung là sau khi bài báo được xuất bản chính thức, họ sẽ công bố kết quả phát triển này dưới giấy phép mã nguồn mở
  • Nếu thư viện C hoạt động được, tức không được chứng minh hình thức là không có vấn đề nhưng phần lớn chạy tốt, thì tôi tò mò tại sao không dùng unsafe Rust để dịch
    Nhìn chung Rust còn thiếu thư viện, nên tôi thấy việc này có giá trị. Rốt cuộc cũng không khác nhiều so với việc dùng dll/so viết bằng C, vốn có thể không an toàn trong một số tình huống