Phân Tích Mã Tĩnh: Hé Lộ 190+ Lỗi "Ngầm" Trong Code TDengine và Cách Phòng Chống!
Lê Lân
1
Phân Tích Mã Tĩnh: Bước Đột Phá Trong Phát Triển Phần Mềm An Toàn
Mở Đầu
Phân tích mã tĩnh đóng vai trò then chốt trong quy trình phát triển phần mềm an toàn, giúp phát hiện lỗi và lỗ hổng bảo mật ngay từ giai đoạn đầu khi chi phí sửa chữa còn thấp và dễ dàng hơn nhiều.
Trong bối cảnh phát triển phần mềm ngày càng phức tạp, việc đảm bảo chất lượng cũng như độ an toàn trở thành ưu tiên hàng đầu. Phân tích mã tĩnh là kỹ thuật phân tích mã nguồn mà không cần chạy chương trình, giúp phát hiện những lỗi tiềm ẩn, từ đó giúp nhà phát triển có thể kịp thời sửa chữa, cải thiện độ tin cậy và bảo mật của sản phẩm. Bài viết này sẽ đi sâu vào các ví dụ cụ thể về lỗi phát hiện được khi áp dụng phân tích mã tĩnh trên dự án TDengine – một cơ sở dữ liệu thời gian thực chuyên dụng cho IoT. Qua đó, chúng ta sẽ thấy được lợi ích thiết thực mà phương pháp này mang lại.
Tổng Quan Về Phân Tích Mã Tĩnh Và Dự Án TDengine
Giới Thiệu Về Phân Tích Mã Tĩnh
Phân tích mã tĩnh (Static code analysis) là quá trình kiểm tra mã nguồn mà không cần chạy ứng dụng. Người dùng thường sử dụng các công cụ tự động như PVS-Studio để quét mã, phát hiện các lỗi tiềm ẩn bao gồm lỗi logic, kiểu dữ liệu, lỗ hổng bảo mật, lỗi bộ nhớ,... Những lỗi này đôi khi rất khó phát hiện qua kiểm thử thông thường hoặc code review thủ công.
Giới Thiệu Dự Án TDengine
TDengine là một hệ quản trị cơ sở dữ liệu thời gian thực (time-series database) mã nguồn mở, được tối ưu dành cho các hệ thống Internet of Things (IoT) và công nghiệp. Dự án yêu cầu tính ổn định và an toàn rất cao để xử lý hàng tỷ cảm biến và thiết bị datastream với dung lượng lớn mỗi ngày.
Việc áp dụng phân tích mã tĩnh trên TDengine không chỉ phát hiện lỗi mà còn giúp nâng cao chất lượng tổng thể của sản phẩm, đặc biệt trong môi trường IoT với yêu cầu bảo mật khắt khe.
Những Lỗi Điển Hình Phát Hiện Qua Phân Tích Mã Tĩnh Trong TDengine
1. Đa Dạng Các Lỗi Null Pointer và Kiểm Tra Không Đầy Đủ
Lỗi N1: Dereferencing Null Pointer (Truy cập con trỏ NULL)
Đôi khi các hàm trả về con trỏ có thể trả về NULL khi gặp lỗi, nhưng đoạn mã sau đó không kiểm tra kỹ dẫn đến truy xuất con trỏ NULL, gây crash.
fileFirstVer = pRet->firstVer; // Lỗi: pRet có thể NULL mà vẫn bị truy xuất
Điều cần làm là sử dụng thêm else để chỉ truy xuất biến khi con trỏ không NULL:
if (pRet == NULL) {
fileFirstVer = pWal->vers.lastVer + 1;
} else {
fileFirstVer = pRet->firstVer;
}
Lỗi N2–N6: Lỗi trong xử lý trường hợp lỗi
Phân tích cho thấy rất nhiều đoạn mã xử lý lỗi có truy cập con trỏ NULL mà không kiểm tra, dẫn đến crash đoạn mã khi xảy ra lỗi.
2. Sai Lệch Trong Việc Sử Dụng Assert
Khi dùng dynamic_cast, kết quả trả về có thể là con trỏ NULL nếu ép kiểu thất bại, nhưng nhiều chỗ dùng assert chỉ kiểm tra trong debug, còn khi release sẽ bỏ qua, tiềm ẩn nguy hiểm.
Ví dụ:
DirectedEdgeStar* des = dynamic_cast<DirectedEdgeStar*>(ees);
assert(des); // Sai: assert chỉ xác nhận debug, release vẫn có thể gây lỗi
des->linkResultDirectedEdges();
Thay vì assert, nên sử dụng kiểm tra con trỏ NULL thực sự với if.
3. Không Kiểm Tra Bộ Nhớ Khi Cấp Phát
Trong dự án TDengine có nhiều ví dụ không kiểm tra kết quả trả về của hàm malloc hoặc realloc, dẫn đến dereferencing con trỏ NULL hoặc mất bộ nhớ.
Ví dụ cấp phát bộ nhớ bị bỏ qua kiểm tra:
unsignedchar* out = (unsignedchar*)malloc(dataLength * sizeof(double));
memcpy(out, data, dataLength * sizeof(double)); // Không kiểm tra out có NULL hay không
Việc bỏ qua bước kiểm tra rất nguy hiểm, đặc biệt trong môi trường IoT hạn chế bộ nhớ.
4. Sai Lệch Kiểu Dữ Liệu Trong Sử Dụng Kích Thước Bộ Đệm
Ở phiên bản 64-bit, size_t có 8 byte trong khi int32_t chỉ 4 byte, dẫn đến ghi bộ nhớ ngoài phạm vi, gây lỗi tràn bộ đệm.
5. Lỗi Tràn Bộ Đệm Do Kiểm Tra Độ Dài Không Chính Xác
Trong tính toán kích thước cần sao chép cho thao tác memcpy, dự án TDengine sử dụng biểu thức độ dài (như length[i]) khá lớn mà không tương thích với vùng nhớ được cấp phát.
Ví dụ lỗi:
if (length[i] < 0 || length[i] > 1 << 20) {
// ...
}
memcpy(value, row[i], length[i]);
value[length[i]] = 0; // Có thể tràn bộ đệm nếu length[i] = MAX_QUERY_VALUE_LEN
Phương án sửa: khai báo hằng số MAX_QUERY_VALUE_LEN và xử lý giới hạn chuỗi một cách chính xác, đồng thời tránh thừa null terminator.
6. Lỗi Tràn Bộ Đệm Và Không Kiểm Tra Độ Dài Chuỗi Đầu Vào
Khi đọc file, sử dụng fgets với một đoạn mã kiểm tra chiều dài dòng không chính xác dẫn đến khả năng array underrun nếu dòng trống.
Những Lỗi Khác Nổi Bật
7. Sai Lệch Khi Dùng Toán Tử Dịch Bit (Shift Operator)
Lỗi khi dịch các biến 8-bit sang 64-bit mà mặc định toán hạng dịch được mở rộng thành 32-bit có thể gây overflow.
n = n | (ch[i] << (8 * i)); // Sai, nên ép kiểu sang uint64_t trước
Sửa lại:
n = n | ((uint64_t)(ch[i]) << (8 * i));
8. Lỗi Về Quản Lý Bộ Nhớ Và Rò Rỉ (Memory Leak)
Nhiều hàm trên TDengine trả về sớm khi gặp lỗi mà không giải phóng bộ nhớ đã cấp phát, tiềm ẩn lỗi rò rỉ.
9. Lỗi Về Kiểu So Sánh Và Toán Tử Hợp Lệ
Những lỗi do sai dấu ngoặc hoặc so sánh sai khiến biến nhận giá trị không đúng.
Ví dụ sai:
if ((code = InitRegexCache() != 0)) return code; // Sai do sai dấu ()
Thay vì vậy, viết đúng:
if ((code = InitRegexCache()) != 0) return code;
10. Lỗi So Sánh Giá Trị Và Điều Kiện Vô Nghĩa
Một số điều kiện so sánh tự thân như cs.hasM() == cs.hasM() vô nghĩa, thường là do gõ nhầm.
Lợi Ích Của Việc Áp Dụng Phân Tích Mã Tĩnh Định Kỳ
Phát hiện sớm lỗi và dễ dàng sửa chữa: Chi phí sửa lỗi càng giảm khi được phát hiện sớm.
Phòng tránh các lỗ hổng bảo mật nghiêm trọng: Bao gồm lỗi null pointer, tràn bộ nhớ, và lỗi logic.
Nâng cao độ tin cậy và tính ổn định của phần mềm: Giảm thiểu crash và hành vi không xác định.
Cải thiện chất lượng code và thúc đẩy các thực hành tốt: Giúp cho codebase luôn sạch và dễ bảo trì.
Hỗ trợ đảm bảo chu trình phát triển phần mềm an toàn (SSDLC).
Như tác giả đã chỉ ra, việc sử dụng PVS-Studio thường xuyên là phương pháp đem lại hiệu quả cao nhất, so với các kiểm tra rải rác, một lần.
Kết Luận
Nội dung bài viết đã chỉ rõ việc áp dụng phân tích mã tĩnh trên dự án TDengine đã phát hiện hàng trăm lỗi tiềm ẩn nghiêm trọng. Các lỗi này không chỉ liên quan trực tiếp đến chất lượng chương trình mà còn tiềm ẩn nguy cơ mất an toàn và ảnh hưởng đến danh tiếng sản phẩm.
Phân tích mã tĩnh không chỉ là công cụ phát hiện lập tức các lỗi mà còn là biện pháp căn bản giúp nâng cao độ tin cậy và bảo đảm phát triển phần mềm an toàn và bền vững. Các nhóm phát triển phần mềm, đặc biệt trong lĩnh vực IoT, cần tích hợp công cụ này một cách định kỳ và nghiêm túc.
Bạn đọc được khuyến khích áp dụng phân tích mã tĩnh để xây dựng các dự án chất lượng và an toàn hơn.