- Dự án cURL, sau khi trước đây đã loại bỏ
strncpy(), nay cũng cấm hoàn toàn strcpy() trong toàn bộ codebase
strcpy() có API đơn giản nhưng có nguy cơ tách rời việc kiểm tra kích thước bộ đệm, nên không an toàn về lâu dài khi bảo trì
- Để thay thế, một hàm mới
curlx_strcopy() đã được đưa vào, nhận cả kích thước bộ đệm đích và độ dài chuỗi làm đối số để kiểm tra khả năng sao chép rồi mới thực hiện
- Hàm này nội bộ dùng
memcpy() và đảm bảo cả việc xử lý ký tự kết thúc null
- Thay đổi này giúp tăng tính bảo mật và tính nhất quán của mã nguồn, đồng thời cũng có thể giảm các báo cáo lỗ hổng sai do AI tạo ra
Bối cảnh loại bỏ strcpy
- Trước đây cURL đã loại bỏ toàn bộ lời gọi
strncpy(), đồng thời chỉ ra các vấn đề như API khó hiểu, không đảm bảo kết thúc null và việc thêm 0 đệm không cần thiết của hàm này
- Với trường hợp cần sao chép chuỗi con, họ chuyển sang dùng
memcpy() và tự xử lý việc kết thúc null
strcpy() có API đơn giản, nhưng vì không chỉ rõ kích thước bộ đệm nên trong quá trình bảo trì có thể xảy ra rủi ro tách biệt giữa mã kiểm tra và lời gọi sao chép
- Khi mã nguồn bị nhiều lập trình viên chỉnh sửa suốt hàng chục năm, việc kiểm tra kích thước bộ đệm có thể bị vô hiệu hóa
Giới thiệu hàm sao chép chuỗi mới
- Để ngăn rủi ro này, một hàm thay thế mang tên
curlx_strcopy() đã được giới thiệu
- Nhận các đối số gồm bộ đệm đích, kích thước bộ đệm, bộ đệm nguồn, độ dài chuỗi nguồn
- Chỉ thực hiện bằng
memcpy() khi cả việc sao chép và kết thúc null đều khả thi
- Nếu thất bại, bộ đệm đích sẽ được khởi tạo thành chuỗi rỗng
- Hàm này cần nhiều đối số và nhiều mã hơn so với
strcpy(), nhưng đổi lại gắn chặt việc kiểm tra bộ đệm với thao tác sao chép để đảm bảo an toàn
- cURL đã cấm hoàn toàn việc dùng
strcpy() trong codebase và loại bỏ nó giống như với strncpy()
Chi tiết triển khai
- Ví dụ định nghĩa hàm như sau
void curlx_strcopy(char *dest, size_t dsize, const char *src, size_t slen)
{
DEBUGASSERT(slen < dsize);
if(slen < dsize) {
memcpy(dest, src, slen);
dest[slen] = 0;
}
else if(dsize)
dest[0] = 0;
}
- Thông qua
DEBUGASSERT, cURL phát hiện sớm lỗi trong quá trình phát triển, còn ở môi trường phát hành thực tế thì hàm được thiết kế để luôn thành công
- Giống
strcpy, hàm này không có giá trị trả về và chọn cách bắt lỗi ở giai đoạn kiểm thử và fuzzing
Phản ứng từ cộng đồng
- Một số nhà phát triển cho rằng nó tương tự
strcpy_s() (C11 Annex K), nhưng cURL hiện vẫn dùng chuẩn C89
- Những ý kiến khác đề xuất cần thêm giá trị trả về hoặc cải thiện cách xử lý khi bộ đệm không đủ
- Đáp lại, phía cURL giải thích rằng “vì đây là hàm được thiết kế để luôn thành công nên không cần giá trị trả về”
Tác dụng phụ liên quan đến AI
- Thay đổi lần này còn có thể ngăn các chatbot AI nhận diện sai việc dùng strcpy trong mã cURL rồi kết luận là ‘dễ tổn thương’
- Tuy vậy, tác giả cũng lưu ý rằng “AI vẫn có thể tạo ra các báo cáo sai khác”, qua đó nhắc đến giới hạn của phân tích mã bằng AI
5 bình luận
Nên dùng
snprintfthay chostrcpy. Nếu trong code cóstrcpythì phải tìm ra địa chỉ của lập trình viên đã tạo ra nó.Đây là cách tôi từng làm bằng mã debug hồi còn làm ở một công ty game cách đây 25 năm, chứ đâu chỉ riêng mỗi
strcpy. Khi phát hành thì lại mở hết ra để tăng tốc độ rồi đem đi phục vụ. Thực ra phía game là nơi nhạy cảm nhất với xung đột bộ nhớ, nên khi làm cũng phải cực kỳ tỉnh táo và cẩn trọng, đến mức còn tự làm cả memory debugger để dùng. Nhưng nhìn lại ngày nay thì hóa ra khi đó mình đang tạo ra garbage collection. Đúng là một ký ức mơ hồ.Lỗi C4996
'strcpy': Hàm hoặc biến này có thể không an toàn. Hãy cân nhắc dùngstrcpy_sthay vào đó. Để tắt cảnh báo ngừng hỗ trợ, hãy dùng_CRT_SECURE_NO_WARNINGS. Xem trợ giúp trực tuyến để biết chi tiết.Ý kiến trên Hacker News
strcpy()không chỉ tệ về mặt bảo mật mà còn kém cả về hiệu năngTrước đây người ta nghĩ
strcpy()hiệu quả khi không biết độ dài chuỗi, nhưng thực tế nó sao chép từng byte một nên CPU phải dự đoán nhánh, điều này không hiệu quảstrcpydùng scalar loop. Không rõ có phải chỉ như vậy trên kiến trúc ARM hay khôngCác routine xử lý chuỗi của C đều có những ràng buộc lớn nên tôi thấy chúng khá vô dụng
Vì vậy tôi nghĩ rất cần một thư viện ghi lại kích thước bộ nhớ đã cấp phát cùng với con trỏ chuỗi
Có thể tham khảo thư viện bstring làm ví dụ
strncpyra đời là để sao chép tên tệp có độ dài cố định. Xem giải thích chi tiết trong câu trả lời StackOverflow nàystrncpyban đầu là hàm để xử lý trường chuỗi có độ rộng cố định. Ví dụ nó dùng để đệm NUL vào trường nhưchar username[20]. Xem thêm trong manual string_copying.7Tôi thấy lạ là
curlx_strcopykhông trả về trạng thái thành công hay thất bạiCó thể kiểm tra
dest[0], nhưng cách này rất dễ gây lỗi và không trực quanDEBUGASSERT(slen < dsize);vượt qua, nhưng assert có thể bị loại bỏ trong bản build release. Tôi nghĩ mã lỗi tường minh sẽ tốt hơnstrncpy()ban đầu không dành cho chuỗi null-terminated mà cho các trường có độ dài cố địnhVấn đề bắt đầu khi công cụ phân tích tĩnh khuyến nghị dùng
strncpythay chostrcpy, trong khi lựa chọn thay thế thực sự làsnprintfhoặcstrlcpystrlcpylà hàm thuộc họ BSD nên không có trong POSIX. Khuyến nghị chính thức làstpecpy, nhưng gần như không có triển khai thực tế. Xem tài liệu liên quanstrncpyđệm phần sau ký tự null là để so sánh hiệu quả trong các trường tên có độ dài cố định như directory entry. Tài liệu cơ sở của chuẩn ANSI C cũng ghi rõ như vậyAPI này tạo cảm giác giống Annex-K. Kích thước buffer đích có tính cả chỗ cho NUL, còn kích thước nguồn thì không
Tôi nghĩ thà tự dùng
memcpycòn hơnTrong bài có câu “
strcpylà miếng mồi để AI tạo ra các báo cáo lỗ hổng sai” nghe rất ấn tượngstrcpylà vấn đề, mà nó còn tạo ra các lập luận phức tạp nhưng có lỗi logic, khiến người bảo trì phải vất vả kiểm chứngNguyên tắc “kiểm tra gần chỗ dùng” là tốt, nhưng sẽ mơ hồ khi cần kiểm tra từ sớm trong vòng đời của dữ liệu
Tôi nghĩ sẽ hay hơn nếu có thể phân biệt bằng kiểu dữ liệu rằng đây là “dữ liệu đã được kiểm chứng”, giống như kiểu
Resultcủa RustResultchỉ đơn thuần chứa thành công/thất bại, chứ không đảm bảo trạng thái đã được kiểm chứng. Thay vào đó nên có một kiểu riêng chỉ có thể tạo ra sau khi đi qua bước kiểm chứng. Đó là triết lý “parse, don’t validate”Sự khác biệt off-by-one giữa kích thước buffer và độ dài chuỗi là một vấn đề cực kỳ tệ về mặt tính dễ dùng. Sau này rất dễ tiếp tục gây lỗi
Hàm sao chép chuỗi mới được đề xuất sẽ xóa trống buffer đích và trả về
voidnếu không thể sao chépNhưng tôi nghĩ trong trường hợp như vậy nên xử lý như lỗi và không đụng vào buffer. Chỉ chặn bằng
DEBUGASSERTthì không yên tâmChúc mừng dự án đã hoàn thành. C/C++ nếu nỗ lực thì vẫn có thể đạt được an toàn bộ nhớ
Tuy vậy trong môi trường di động, cỡ chữ của đồ thị quá nhỏ nên rất khó đọc
strcpykhông có nghĩa là mã sẽ trở nên an toàn bộ nhớChuyển hẳn sang ngôn ngữ C3 cũng là một lựa chọn hay. Đây là một dự án giữ cú pháp của C với mức thay đổi tối thiểu, đồng thời bổ sung các tính năng hiện đại nên cũng dễ chuyển đổi.