Những cải tiến về phân tích tĩnh trong GCC 14
(developers.redhat.com)-fanalyzercủ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-boundskhô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ý
-fanalyzerlà 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
forlồng nhau, điều kiện của vòng lặp thứ hai làj < nnhư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 đổiitrong biểu thức tăng thànhj - Trình phân tích liên tục đi theo nhánh true của
j < ntrên đường này và cảnh báo về việc lặp vòng
- Đây là lỗi tương ứng với việc sao chép câu lệnh
- 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àochar buf[10], rồi gọistrcat(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
buflà 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à byteNULkết thúc hiện có, vốn là điểm bắt đầu củastrcat
- Thông báo cũ cho biết dung lượng của
- 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àochar 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
- Mã sao chép
- Có thể xem mã liên quan trong ví dụ chuỗi ASCII và ví 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ínhnull_terminated_string_arg(1)vànonnullchar 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ốistr - Chẩn đoán cũng xuất kèm một note rằng đối số số 1 của
example_fnphả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
- Phân tích dữ liệu nhiễm bẩn của trình phân tích theo dõi đầu vào do kẻ tấn công kiểm soát, các điểm làm sạch và những điểm sử dụng khi chưa làm sạch
- Trong các bản phát hành GCC trước đây, do lỗi và nhiều cảnh báo dương tính giả, tính năng này không được bật mặc định và bị ẩn sau một đối số dòng lệnh riêng
- Trong GCC 14, sau nhiều bản sửa lỗi, khi chọn
-fanalyzer, phân tích dữ liệu nhiễm bẩn được bật mặc định - Thay đổi này cũng bật 6 cảnh báo dựa trên dữ liệu nhiễm bẩn sau
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ủaosf_getsysinfolà 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ịnbytesdo 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
nbyteschỉ được kiểm tra giới hạn dưới trongif (nbytes < sizeof(*hwrpb)) - Tham số thứ ba của
copy_to_userđược thuộc tínhaccess(write_only, 1, 3)thể hiện là tham số kích thước
- Chẩn đoán cho thấy
- Vấn đề nằm ở việc điều kiện làm sạch được viết là
if (nbytes < sizeof(*hwrpb))- Điều kiện dự định là dạng
if (nbytes > sizeof(*hwrpb)) - Trong phiên bản đã sửa điều kiện, trình phân tích không cảnh báo
- Điều kiện dự định là dạng
- 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
Ý 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
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
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 instantiationTô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
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 14Mã ban đầu có dạng trả về
-1nếunbytes < sizeof(*hwrpb), và trả về-2nếucopy_to_user(buffer, hwrpb, nbytes)thất bại. Bản sửa được áp dụng là kiểm tranbytes > 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ỏ
hwrpbvới kích thước khácsizeof(*hwrpb)là không hợp lýnbytes = 4vàsizeof(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 tronghwrpb->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ạonbytesThậ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-overflowcó 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ôngRất hay. Dạo này tôi không phát triển C nhiều nên tò mò
strcpyvàstrcatcò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ớigotoTất nhiên tôi biết trong phát triển kernel thì
gotothườ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àogotorõ 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 íchgotovẫn mang tính idiom trong C. Cần cẩn thận, nhưng dù sao đó cũng là CTuy nhiên tôi hoàn toàn không thích
longjmpgotođơ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ủngNhữ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
gotodùng cẩn trọng thì ổn.strcpyvàstrcatcũ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 Cgotolà điều cấm kỵ ngang vớistrcat,strcpy.gotothì ổn; cònstrcatvàstrcpyđược dùng trong cùng scope mà không cómallocđúng kích thước thì gần như là code smellRấ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