1 điểm bởi GN⁺ 2024-04-05 | 1 bình luận | Chia sẻ qua WhatsApp
  • -fanalyzer của GCC 14 tập trung vào việc lần theo nhiều đường thực thi của mã C bằng thực thi ký hiệu, nhằm tìm các lỗi khó bắt trước thời gian chạy ngay tại thời điểm biên dịch
  • Các cải tiến lần này được chia thành phát hiện vòng lặp vô hạn, trực quan hóa tràn bộ đệm, theo dõi chuỗi C và bật mặc định phân tích dữ liệu nhiễm bẩn, với trọng tâm là làm cho kết quả chẩn đoán dễ hiểu hơn
  • -Wanalyzer-out-of-bounds không chỉ cảnh báo đơn giản mà còn dùng sơ đồ văn bản để thể hiện quan hệ giữa bộ đệm và vị trí ghi, đồng thời chỉ ra phần tràn xảy ra ở giữa ký tự nào trong chuỗi UTF-8
  • Thuộc tính mới null_terminated_string_arg(PARAM_IDX) cho phân tích viên và người đọc mã biết điều kiện API kỳ vọng chuỗi kết thúc bằng null, qua đó làm rõ hơn các đường truyền bộ đệm không kết thúc bằng null
  • Phân tích dữ liệu nhiễm bẩn được bật mặc định khi chọn -fanalyzer, giúp dễ kiểm tra hơn các luồng trong đó giá trị do kẻ tấn công kiểm soát được dùng làm kích thước, chỉ mục, độ lệch, v.v. mà không kiểm tra giới hạn trên

Phạm vi vấn đề mà -fanalyzer xử lý

  • -fanalyzer là một pass phân tích tĩnh của GCC, cố gắng tìm lỗi tại thời điểm biên dịch bằng cách thực thi ký hiệu nhiều đường thực thi trong mã nguồn C
  • Các cải tiến của GCC 14 được tổng hợp trước bản phát hành chính thức vào tháng 4 năm 2024; tại thời điểm bài viết, bản phát hành GCC 14.1 được kỳ vọng trong tháng 4 năm 2024
  • Bản tiền phát hành GCC 14.0 đã được dùng trong Fedora 40 Beta
  • Có thể thử các ví dụ với các tùy chọn trình biên dịch mới trên Compiler Explorer

Phát hiện vòng lặp vô hạn đơn giản

  • GCC 14 bổ sung cảnh báo mới -Wanalyzer-infinite-loop
  • Mã ví dụ có dạng trong các vòng for lồng nhau, điều kiện của vòng lặp thứ hai là j < n nhưng biểu thức tăng vẫn là i++
    • Đây là lỗi tương ứng với việc sao chép câu lệnh for đầu tiên rồi không đổi i trong biểu thức tăng thành j
    • Trình phân tích liên tục đi theo nhánh true của j < n trên đường này và cảnh báo về việc lặp vòng
  • Kết quả chẩn đoán hiện tại dễ hiểu hơn nếu đọc tuần tự từ số sự kiện (1) đến (5)
  • Trong GCC 15, việc cải thiện khả năng đọc bằng cách như ASCII art để nhấn mạnh đường luồng điều khiển vẫn là một mong muốn
  • Có thể chạy mã này trong ví dụ Compiler Explorer

Trực quan hóa tràn bộ đệm bằng văn bản

  • Từ GCC 13, trình phân tích bắt đầu hỗ trợ kiểm tra biên thông qua -Wanalyzer-out-of-bounds
  • Trong GCC 14, nó có thể xuất quan hệ không gian của lỗi tràn bộ đệm dự đoán dưới dạng sơ đồ dựa trên văn bản
  • Trong ví dụ thực hiện strcpy(buf, "hello") vào char buf[10], rồi gọi strcat(buf, " world!"), lỗi tràn bộ đệm dựa trên stack được phát hiện
    • Thông báo cũ cho biết dung lượng của buf là 10 byte và có thao tác ghi vượt phạm vi từ byte 10 đến byte 12
    • Sơ đồ của GCC 14 hiển thị cùng lúc bộ đệm đích mà strcpy đã điền và byte NUL kết thúc hiện có, vốn là điểm bắt đầu của strcat
  • Với ví dụ chuỗi không phải ASCII, nó cũng lần theo biểu diễn UTF-8 và chỉ ra vị trí tràn
    • Mã sao chép "サツキ" vào char buf[11] rồi nối thêm "メイ" được dùng làm ví dụ
    • Sơ đồ cho thấy lỗi tràn xảy ra ở giữa ký tự , tức U+30E1
  • Có thể xem mã liên quan trong ví dụ chuỗi ASCIIví dụ chuỗi không phải ASCII

Tăng cường theo dõi thao tác chuỗi C

  • Trình phân tích GCC 14 cải thiện việc theo dõi thao tác chuỗi C, mô phỏng các API quét bộ đệm để tìm byte kết thúc null
  • Nếu có đường mà con trỏ trỏ tới bộ đệm không kết thúc bằng null nhưng lại được truyền cho API đó, nó sẽ cảnh báo
  • Thuộc tính hàm mới null_terminated_string_arg(PARAM_IDX) cho phân tích viên và người đọc mã biết điều kiện rằng tham số cụ thể phải là chuỗi kết thúc bằng null
  • Trong ví dụ, example_fn(const char *p) được gắn các thuộc tính null_terminated_string_arg(1)nonnull
    • char str[3] = "abc"; không có chỗ để chứa byte kết thúc null
    • Khi gọi example_fn(str), trình phân tích cảnh báo về đọc quá mức bộ đệm dựa trên stack, đọc 1 byte sau cuối str
    • Chẩn đoán cũng xuất kèm một note rằng đối số số 1 của example_fn phải là con trỏ tới chuỗi kết thúc bằng null
  • Có thể chạy ví dụ này trên Compiler Explorer

Bật mặc định phân tích dữ liệu nhiễm bẩn

Trường hợp phân tích kernel qua CVE-2011-2210

  • Ví dụ trích từ CVE-2011-2210 của kernel Linux được dùng làm trường hợp phân tích dữ liệu nhiễm bẩn
  • Bằng cách thêm __attribute__((tainted_args)) vào macro __SYSCALL_DEFINEx, ta báo cho trình phân tích biết rằng các đối số của osf_getsysinfo là các giá trị đi qua ranh giới tin cậy và phải được coi là giá trị nhiễm bẩn
  • Trình phân tích GCC 14 cảnh báo rằng trong copy_to_user(buffer, hwrpb, nbytes), giá trị nbytes do kẻ tấn công kiểm soát được dùng làm kích thước mà không kiểm tra giới hạn trên
    • Chẩn đoán cho thấy nbytes chỉ được kiểm tra giới hạn dưới trong if (nbytes < sizeof(*hwrpb))
    • Tham số thứ ba của copy_to_user được thuộc tính access(write_only, 1, 3) thể hiện là tham số kích thước
  • Vấn đề nằm ở việc điều kiện làm sạch được viết là if (nbytes < sizeof(*hwrpb))
  • Công việc chạy trình phân tích trên kernel để tìm lỗ hổng và sửa các cảnh báo dương tính giả của trình phân tích vẫn đang tiếp tục

1 bình luận

 
GN⁺ 2024-04-05
Ý kiến trên Hacker News
  • Với tôi, fanalyzer là một trong những tính năng “killer” của GCC so với Clang. Vì nó giải thích lỗi nên việc lập trình C dễ hơn nhiều, và thông báo lỗi cũng bắt đầu cho cảm giác giống Rust ở khía cạnh thân thiện với lập trình viên

    • Tôi biết Rust được chú ý nhiều, nhất là trên HN, nhờ an toàn bộ nhớ và các abstraction tốt, nhưng tôi tự hỏi có bao nhiêu phần trong độ phổ biến của Rust là nhờ thông báo lỗi
      Lý do lớn nhất khiến người ta bỏ học một công nghệ thường là các lỗi gây bực bội hoặc không rõ ràng. Hơi lạc đề một chút, nhưng ý tôi là tôi thích C, và sẽ còn thích hơn nếu nó có thông báo lỗi ở mức của Rust
    • Clang cũng có một công cụ tương tự là Clang Static Analyzer: https://clang-analyzer.llvm.org/
    • Tôi lại có trải nghiệm hoàn toàn ngược lại. Clang thường cho thông báo lỗi tốt hơn GCC nhiều, một số triển khai cảnh báo hoặc lỗi bắt được nhiều trường hợp hơn, và clang-tidy cung cấp phân tích tĩnh tốt hơn hẳn
    • Nhớ lại một trong những lý do tôi từng ghét C++ đến vậy. Thay vì error: missing semicolon, nó thường tuôn ra hơn 1000 dòng thông báo lỗi liên quan đến template instantiation
    • Chuyện này nghe khá bất ngờ. Tôi tò mò GCC analyzer bắt thêm được những gì mà Clang Static Analyzer chưa báo
      Tôi đã thử GCC analyzer vài lần nhưng không tìm được frontend nào ổn để làm đầu ra dễ đọc. Với Clang thì có khá nhiều lựa chọn như đầu ra HTML khá ổn, CodeChecker, tích hợp Xcode, v.v.; tôi thắc mắc mọi người đọc đầu ra phía GCC như thế nào. Hơn nữa, GCC có vẻ tạo false positive nhiều hơn Clang rất nhiều
  • Có thêm 36 bình luận trong một luồng khác: https://news.ycombinator.com/item?id=39918278
    “GCC 14 Boasts Nice ASCII Art for Visualizing Buffer Overflows (phoronix.com)”, bài đăng cách đây 2 giờ

  • Vài tháng trước tôi có làm một tiện ích Linux nhỏ. Nó là một drop-in shim thay thế cho một file thực thi bất kỳ; khi được gọi, nó giả làm chương trình gốc, fork bản gốc và nối vào stdout/stderr
    Đầu ra lỗi được gửi tới một trợ lý GPT tùy biến biết ngữ cảnh của chương trình đó, rồi trợ lý này biến lỗi gốc thành dạng dễ đọc với con người và xuất ra stderr của shim. Tôi dùng nó vì đã chán nhìn các dump compiler lồng nhau liên quan đến concept/template của GCC/Clang, nhưng nếu muốn thì có thể dùng cho bất kỳ chương trình nào. Nó hoạt động tốt, nhưng tôi bị ốm nặng nên không làm tiếp được; tôi nghĩ đây sẽ là một dự án hay nếu ai đó làm lại cho tử tế và tổng quát hóa nó

  • Ước gì có định dạng đầu ra tốt hơn cho kết quả phân tích. Cách hiện tại là địa ngục với screen reader

    • Để tham khảo, tôi đã triển khai đầu ra SARIF trong GCC 13, và chẳng hạn có thể xem bằng plugin trong VS Code. Tuy nhiên nó không bao gồm ASCII art
      Có thể xem ví dụ đầu ra ở đây: https://godbolt.org/z/aan6Kfxds
      Đây là ví dụ đầu tiên trong bài, được thêm tùy chọn dòng lệnh -fdiagnostics-format=sarif-stderr. Tôi cũng đã thử nghiệm xuất sơ đồ thành SVG, nhưng chưa đủ hoàn thiện để đưa vào GCC 14
  • Mã ban đầu có dạng trả về -1 nếu nbytes < sizeof(*hwrpb), và trả về -2 nếu copy_to_user(buffer, hwrpb, nbytes) thất bại. Bản sửa được áp dụng là kiểm tra nbytes > sizeof(*hwrpb), nhưng tôi cho rằng cách sửa đúng là copy_to_user(buffer, hwrpb, sizeof(*hwrpb))
    Việc sao chép ra ngoài từ con trỏ hwrpb với kích thước khác sizeof(*hwrpb) là không hợp lý

    • Nếu bên gọi truyền nbytes = 4sizeof(hwrpb) là 16 byte, thì có thể chép thừa 12 byte từ buffer của bên gọi và đọc vùng nhớ không thuộc sở hữu của mình. Tôi nghĩ nên tránh điều đó
      Cách tốt hơn là chỉ sao chép số byte tối thiểu mà cả bên gọi lẫn bên được gọi đều hỗ trợ. Ví dụ như nbytes = MIN(nbytes, sizeof(hwrpb));. Với giả định thông tin phiên bản trong hwrpb->size được tuân thủ, điều này có thể bảo đảm tương thích ngược/xuôi ngay cả khi một phần struct chưa được khởi tạo
    • Đúng. Nhưng vì đã có kích thước buffer, việc ghi đè quá cuối buffer của bên gọi cũng vô lý, nên cũng không thể truyền một giá trị dài hơn nbytes
  • Thật sự tuyệt vời. Khối lượng công việc bỏ vào có vẻ khổng lồ. Độ khó trông ngang với việc đưa fat pointer/array view vào thư viện chuẩn và chuẩn C

  • -Wstringop-overflow có quá nhiều false positive nên là cảnh báo đầu tiên tôi tắt. Tôi nghi ngờ biến thể analyzer có khá hơn không

    • Chẳng phải hơi giống việc tháo pin máy dò carbon monoxide vì nó cứ kêu bíp bíp làm đau đầu và buồn ngủ sao
  • Rất hay. Dạo này tôi không phát triển C nhiều nên tò mò strcpystrcat còn được dùng thường xuyên đến mức nào. Lần cuối tôi kiểm tra thì chúng gần như bị xem là điều cấm kỵ ngang với goto
    Tất nhiên tôi biết trong phát triển kernel thì goto thường được ưa dùng. Tôi tò mò phân tích chuỗi C thực sự giúp ích đến mức nào

    • Trong một số ngữ cảnh, dùng goto rõ ràng là đúng và tao nhã. Cố tránh nó bằng mọi giá có thể dẫn đến mã xấu, rối rắm và khó bảo trì. Không phổ biến, nhưng vẫn có các cách dùng hợp lệ
      Các hàm như strcpy ít được khuyến nghị hơn, nhưng cũng có những tình huống mà chúng được bảo đảm là đúng miễn là các bất biến mạnh hơn, chẳng hạn bất biến ở cấp ngôn ngữ, không bị phá vỡ. Nếu những trường hợp đó bị phá vỡ thì vấn đề đã lớn hơn nhiều rồi. Dù hiếm, cũng có thể lập luận rằng một lựa chọn thay thế trên danh nghĩa an toàn hơn có thể kém hiệu quả hơn đôi chút mà không đem lại lợi ích
    • Miễn là được dùng tương đương về mặt logic với các cấu trúc lập trình có cấu trúc mà C không có, một số cách dùng goto vẫn mang tính idiom trong C. Cần cẩn thận, nhưng dù sao đó cũng là C
      Tuy nhiên tôi hoàn toàn không thích longjmp
    • Dùng goto đơn giản thì không có vấn đề. Ngược lại, họ hàm strxcpy là một mớ hỗn độn hoàn toàn và không nên dùng vì bất kỳ lý do gì. Việc chúng được dùng trong kernel thật kinh khủng
      Những hàm đó cùng mọi nỗ lực thất bại nhằm “sửa” chúng đáng lẽ nên bị thổi bay khỏi quỹ đạo
    • goto dùng cẩn trọng thì ổn. strcpystrcat cũng “ổn” theo nghĩa là nếu bạn biết mã đúng, và nếu sai thì sẽ là vấn đề lớn. Đáng tiếc là mô tả này áp dụng cho phần lớn C
    • Tôi không xem goto là điều cấm kỵ ngang với strcat, strcpy. goto thì ổn; còn strcatstrcpy được dùng trong cùng scope mà không có malloc đúng kích thước thì gần như là code smell
  • Rất tốt. Thật vui khi thấy có kèm đầy đủ báo cáo chi tiết giải thích điều gì đã sai