"Đừng dùng /dev/random, nó sẽ chặn" là một trong những lời khuyên được lặp lại nhiều nhất về Linux. Bài này đo lại nó.

Chi phí sinh số ngẫu nhiên

Cái bẫy entropy không còn tồn tại

Đọc 100 MB từ mỗi thiết bị, nhân 6.12:

Nguồn Thời gian Thông lượng
/dev/random 138 ms 782 MB/s
/dev/urandom 137 ms 771 MB/s
entropy_avail trước: 256
entropy_avail sau  : 256

Không chặn, không giảm entropy, và hai thiết bị cho cùng một tốc độ.

Trên nhân trước 5.6, cùng lệnh đó có thể treo hàng giờ trên một máy chủ không có bàn phím và không có chuột. Đó là hoàn cảnh sinh ra haveged, rng-tools, và hàng loạt bài viết khuyên theo dõi entropy_avail.

Từ nhân 5.6, /dev/random chỉ chặn đúng một lần — lúc khởi động, trước khi bộ sinh số được gieo mầm. Sau thời điểm đó nó không bao giờ chặn nữa, vì mô hình "entropy bị tiêu hao khi đọc" đã bị bỏ.

Lý do bỏ nó: một bộ sinh số giả ngẫu nhiên an toàn về mật mã, đã được gieo mầm bằng 256 bit thật, không yếu đi khi bạn đọc thêm. Việc "trừ entropy" mỗi lần đọc là mô hình sai từ đầu, và nó gây ra nhiều sự cố hơn là bảo vệ được gì.

Ba hệ quả cho hôm nay:

  • entropy_avail là 256 và luôn là 256. Cảnh báo dựa trên nó không bao giờ kêu, và nếu nó kêu thì đó là nhân rất cũ.
  • havegedrng-tools không còn cần thiết trên nhân hiện đại. Chúng vẫn có ích ở đúng một chỗ: rút ngắn thời gian gieo mầm lúc khởi động trên máy ảo mới tạo.
  • Chọn /dev/random hay /dev/urandom không còn là quyết định về hiệu năng.

Cách kiểm tra nhân của bạn có ở phía nào của mốc này:

uname -r
cat /proc/sys/kernel/random/poolsize        # 256 = nhan moi; 4096 = nhan cu

Chi phí lấy 16 byte

Kích thước điển hình cho UUID, token phiên, muối băm:

Nguồn µs mỗi lần An toàn mật mã
rand() của libc 0,02 không
getrandom(0) 0,20
/dev/random 0,21
/dev/urandom, giữ fd mở 0,23
/dev/urandom, mở lại mỗi lần 0,63

Ba nguồn an toàn chênh nhau chưa tới 15%. Chọn cái nào không quan trọng về tốc độ.

Điều quan trọng là dòng cuối: mở lại tệp mỗi lần tốn 2,7 lần, và toàn bộ phần chênh là open cộng close. Với một dịch vụ sinh một token mỗi yêu cầu, đó là 0,4 µs vứt đi trên mỗi yêu cầu.

getrandom() giải quyết chuyện này gọn nhất: nó là lời gọi hệ thống, không cần mô tả tệp nào:

#include <sys/random.h>
char buf[16];
getrandom(buf, sizeof buf, 0);

Nó cũng miễn nhiễm với hai vấn đề khác của /dev/urandom: hết mô tả tệp (phần 33) và chroot không có /dev.

rand() nhanh gấp mười lần và không dùng được

0,02 µs so với 0,20 µs. Cám dỗ rõ ràng, và câu trả lời cũng rõ ràng: không bao giờ dùng rand() cho bất cứ thứ gì liên quan tới bảo mật.

rand() của glibc là một bộ sinh số tuyến tính. Biết vài giá trị đầu ra là suy được toàn bộ dãy — cả quá khứ lẫn tương lai. Token phiên, mã đặt lại mật khẩu, khoá API sinh bằng rand() đều đoán được.

Nó dùng được cho: mô phỏng, sinh dữ liệu thử, chọn ngẫu nhiên trong thuật toán, jitter cho việc thử lại. Những chỗ đó thì 0,02 µs là món hời.

Một lưu ý: rand() không an toàn với đa luồng ở nhiều cài đặt. Dùng rand_r() hoặc random_r() khi có nhiều luồng, nếu không bạn sẽ có hai luồng nhận cùng một dãy.

Với khối lớn thì mọi thứ bằng nhau

Lấy 4.096 byte mỗi lần:

Nguồn µs mỗi lần MB/s
getrandom(0) 4,96 787,5
/dev/random 5,01 779,8
/dev/urandom 5,02 778,2
rand() 5,27 741,2

Cả năm hội tụ về khoảng 780 MB/s, và rand() chậm nhất.

Kết quả này ngược hẳn với bảng 16 byte, và lý do đơn giản: ở 16 byte, chi phí là mỗi lời gọi; ở 4 KB, chi phí là mỗi byte. Bộ sinh số của nhân dùng ChaCha20 và sinh cả khối một lượt, trong khi rand() phải chạy vòng lặp bốn byte một.

Nghĩa là bộ sinh số an toàn của nhân không còn chậm hơn bộ sinh số thường khi lấy lượng lớn. Lập luận "dùng bộ sinh số yếu cho nhanh" không còn cơ sở đo lường nào.

Trong các ngôn ngữ

Ngôn ngữ An toàn Không an toàn
C getrandom() rand()
Python secrets, os.urandom random
Go crypto/rand math/rand
Java SecureRandom Random
Node crypto.randomBytes Math.random
Rust rand::rngs::OsRng rand::thread_rng (tuỳ mục đích)

Một bẫy riêng của Java: SecureRandom.getInstance("NativeRandom") trên nhân cũ đọc /dev/randomchặn. Đây là nguồn của rất nhiều báo cáo "ứng dụng Java treo khi khởi động trong container". Cách chữa cũ là:

-Djava.security.egd=file:/dev/./urandom

Trên nhân từ 5.6 thì vấn đề tự biến mất, nhưng tham số đó vẫn còn trong vô số tệp cấu hình.

Thử ba mươi giây

Kiểm tra nhân của bạn có còn cái bẫy cũ không:

echo "nhan     : $(uname -r)"
echo "poolsize : $(cat /proc/sys/kernel/random/poolsize)     (256 = nhan moi, 4096 = cu)"
echo "avail    : $(cat /proc/sys/kernel/random/entropy_avail)"

echo "--- doc 10 MB tu /dev/random ---"
time dd if=/dev/random of=/dev/null bs=1M count=10 2>&1 | tail -1

echo "avail sau: $(cat /proc/sys/kernel/random/entropy_avail)"

Nếu lệnh dd xong trong vài chục mili giây và entropy_avail không đổi, mọi lời khuyên về entropy trong tài liệu vận hành của bạn đều có thể xoá đi.

Nếu nó treo, bạn đang chạy nhân trước 5.6 — và lúc đó vấn đề thật không phải entropy mà là phiên bản nhân.

Phần sau: tín hiệu và dừng êm — đo cách tiến trình nên phản ứng.