Mới đây một lập trình viên phát hiện BUG 24 năm tuổi trong Linux


Bối cảnh vấn đề

Tại Skroutz, mỗi lập trình viên được cung cấp một bản sao cơ sở dữ liệu để phát triển, được cập nhật hàng ngày bằng pipeline sử dụng LVM snapshots, rsync, và ZFS snapshots. Hệ thống này hỗ trợ MariaDB (600GB), MongoDB (200GB) và Elasticsearch, giúp đồng bộ dữ liệu nhanh chóng.


Tuy nhiên, họ gặp một lỗi kỳ lạ khi truyền dữ liệu qua rsync: một số phiên truyền bị treo vô thời hạn dù mạng vẫn hoạt động bình thường. Lỗi này xuất hiện không thường xuyên, nhưng có xu hướng xảy ra nhiều hơn khi giới hạn tốc độ rsync. Ban đầu, họ nghĩ đây là lỗi "race condition" và giải quyết tạm thời bằng cách giảm tốc độ truyền. Nhưng khi lỗi này xảy ra hàng ngày, họ phải điều tra sâu hơn.

Phân tích kỹ thuật

1. Cách rsync hoạt động và dấu hiệu lỗi

rsync hoạt động theo mô hình pipeline:

Client (máy nhận) và Server (máy gửi) trao đổi dữ liệu qua TCP.

Client tạo một tiến trình generator để xác định dữ liệu cần tải về.

Server gửi dữ liệu theo yêu cầu của generator → dữ liệu đi qua pipeline generator → sender → receiver.

Quá trình này bị treo khi generator bị kẹt ở send(), còn sender và receiver bị kẹt ở recv(), nhưng không có dữ liệu nào được truyền.

Khi kiểm tra trạng thái socket bằng ss -mito, họ thấy:

Máy khách có 3.5MB dữ liệu trong hàng đợi gửi (Send-Q), sẵn sàng truyền.

Máy chủ có bộ đệm nhận (Recv-Q) trống, sẵn sàng nhận dữ liệu.

Điều này bất thường, vì TCP đáng lẽ phải tiếp tục truyền dữ liệu. Họ kiểm tra thông tin về bộ đệm và thấy cờ persist đang hoạt động, nghĩa là kết nối đang chờ tín hiệu mở cửa sổ nhận.

2. Hiện tượng "Zero Window" và vấn đề

TCP sử dụng cửa sổ nhận (Receive Window) để kiểm soát luồng dữ liệu. Khi buffer đầy, hệ thống sẽ gửi tín hiệu Zero Window để tạm ngừng nhận dữ liệu. Khi có chỗ trống, nó gửi tín hiệu cập nhật để tiếp tục truyền.

Nhưng trong trường hợp này:

Máy chủ đã báo có thể nhận dữ liệu (gửi tín hiệu mở cửa sổ).

Máy khách vẫn không gửi dữ liệu, khiến rsync bị treo.

3. Phân tích mã nguồn nhân Linux

Họ kiểm tra file tcp_input.c trong nhân Linux và phát hiện lỗi trong hàm tcp_ack_update_window(), chịu trách nhiệm cập nhật cửa sổ nhận.

Lệnh kiểm tra quan trọng là:

static inline bool tcp_may_update_window(const struct tcp_sock *tp,

const u32 ack, const u32 ack_seq,

const u32 nwin)

{

return after(ack, tp->snd_una) ||

after(ack_seq, tp->snd_wl1) ||

(ack_seq == tp->snd_wl1 && nwin > tp->snd_wnd);

}

Trong đó:

snd_wl1: số thứ tự gói tin cuối cùng cập nhật cửa sổ nhận.

ack_seq: số thứ tự gói tin xác nhận.

Họ phát hiện rằng:

Biến snd_wl1 không được cập nhật khi nhận dữ liệu lớn qua bulk receiver fast-path (một cơ chế tối ưu tốc độ).

Khi ack_seq tăng lên quá cao (qua 2GB), nhưng snd_wl1 không đổi, Linux bỏ qua tín hiệu cập nhật cửa sổ nhận → kết nối bị treo.

Lỗi này tồn tại từ Linux 2.1.8 (năm 1996).

4. Khắc phục lỗi

Skroutz báo cáo lỗi lên nhóm phát triển Linux. Chỉ sau 2 giờ, Neal Cardwell cung cấp bản vá, cập nhật trong Linux 5.10-rc1, và cả các phiên bản 4.9.241, 4.19.153. Sau khi thử nghiệm, họ xác nhận lỗi đã được khắc phục.

☄️Vì sao lỗi này tồn tại suốt 24 năm?

👉 Hiếm khi xảy ra:

Các giao thức như HTTP thường đọc dữ liệu ngay sau khi nhận, tránh lỗi này.

rsync sử dụng pipeline dữ liệu lớn, khiến lỗi xuất hiện.

Nếu dùng rsync qua SSH, lỗi sẽ không xuất hiện do SSH kiểm soát dữ liệu tốt hơn.

👉Điều kiện rất đặc biệt:

Kết nối phải giữ trạng thái Zero Window rất lâu (trên 2GB dữ liệu) mà không có lỗi mất gói tin.

Nếu có mất gói tin, TCP sẽ tự điều chỉnh, tránh lỗi này.

👉Ứng dụng thường tự động timeout:

Nhiều ứng dụng đặt timeout, nếu lỗi xảy ra, nó sẽ tự động thử lại hoặc ngắt kết nối.

👉Không dễ debug:

Phải có đủ kiên nhẫn để điều tra tận mã nguồn nhân Linux.

Theo:  Group WhiteHat dịch

Share on Google Plus
    Blogger Comment
    Facebook Comment

0 nhận xét:

Đăng nhận xét