- Trên Windows 11 24H2, đã tái hiện được hiện tượng thủy phi cơ Skimmer biến mất hoặc người chơi bị bắn vọt lên bầu trời ở độ cao bất thường ngay sau khi spawn; nguyên nhân không phải do hệ điều hành mà là một lỗi xử lý dữ liệu cũ bên trong trò chơi
- Dòng Skimmer trong
vehicles.ide bị thiếu 2 giá trị tỷ lệ bánh xe cần cho máy bay, nhưng CFileLoader::LoadVehicleObject không kiểm tra giá trị trả về của sscanf, nên đã dùng nguyên các biến cục bộ chưa được khởi tạo
- Trên các môi trường Windows cũ, giá trị tỷ lệ bánh xe
0.7 của xe TopFun ngay trước đó tình cờ còn sót lại trên stack nên Skimmer trông có vẻ hoạt động bình thường, nhưng trên Windows 11 24H2, mức sử dụng stack của LeaveCriticalSection thay đổi đã phá vỡ sự ngẫu nhiên đó
- Tỷ lệ bánh xe sai đã làm nhiễm bẩn phép tính hệ treo và tọa độ Z của hộp va chạm, rồi lan sang cả độ cao khi tạo spawn và phép tính tốc độ cánh quạt, dẫn đến vị trí camera bất thường, hiệu ứng burn-in, và vòng lặp treo trong môi trường SilentPatch
- Cách khắc phục là thêm
-1, 0.7, 0.7, -1 vào dòng Skimmer trong vehicles.ide hoặc áp dụng hotfix SilentPatch tiếp theo; kiểm tra dữ liệu đầu vào và quản lý cảnh báo biên dịch ảnh hưởng trực tiếp tới khả năng tương thích lâu dài
Triệu chứng của Skimmer lộ ra trên Windows 11 24H2
- Trên issue tracker của SilentPatch đã có báo cáo rằng sau khi cập nhật lên Windows 11 24H2, máy bay Skimmer biến mất hoàn toàn khỏi game
- Không thể spawn bằng trainer và cũng không tìm thấy ở vị trí spawn gốc
- Hiện tượng tái hiện được cả trên bản game có mod lẫn bản sao vanilla chỉ cài SilentPatch
- Trên GTAForums cũng đã có báo cáo về cùng vấn đề này từ tháng 11/2024, và một số người dùng nghi ngờ SilentPatch, nhưng hiện tượng tương tự cũng xuất hiện trên game hoàn toàn không mod
- Trên Windows 10 22H2 và Windows 11 23H2, Skimmer spawn bình thường, còn người dùng Windows 11 24H2 thì gặp cùng lỗi này
- Kết quả debug từ xa trên máy ảo 24H2 cho thấy các máy bay và thuyền khác đều bình thường, chỉ riêng Skimmer là biến mất
Độ cao bất thường và vòng lặp cánh quạt không bao giờ kết thúc
- Khi dùng script để ép tạo Skimmer và cho CJ lên ngồi, người chơi bị bắn lên độ cao
1.0287648030984853e+0031m, tức khoảng 10.3 nonillion mét
- Nếu cài SilentPatch, game sẽ rơi vào vòng lặp và treo ngay sau khi hất người chơi lên cao
- Nếu không có SilentPatch, game không treo nhưng xuất hiện hiệu ứng burn-in nổi tiếng khi camera di chuyển tới vị trí gần như vô cực
- Điểm treo nằm ở vòng lặp chuẩn hóa góc cánh quạt trong
CPlane::PreRender
- Giá trị
m_fBladeSpeed tăng tới 3.73340132e+29
- Dù liên tục trừ
6.2831855, giá trị vẫn không đổi theo biểu diễn số thực dấu phẩy động nên vòng lặp không thể kết thúc
- Vì tốc độ cánh quạt được suy ra từ một giá trị tỷ lệ với độ cao máy bay, đây là manh mối cho thấy Skimmer ngay từ đầu đã được tạo ở vị trí cao bất thường
Phép tính hệ treo làm nhiễm bẩn hộp va chạm
- Hàm tạo bằng script
CCarCtrl::CreateCarForScript cộng kết quả của GetDistanceFromCentreOfMassToBaseOfModel vào tọa độ Z được truyền vào
- Khi kiểm tra hộp va chạm của Skimmer,
bbox.sup.z đã bị nhiễm bẩn thành một giá trị vô lý như -4.30747210e+33
- Theo dõi bằng data breakpoint cho thấy giá trị hộp va chạm ở thời điểm load ban đầu là bình thường
bbox.sup.z ban đầu là -2.21952772
- Sau đó, khi xe được spawn lần đầu,
SetupSuspensionLines cập nhật tọa độ Z của hộp va chạm để phản ánh chiều cao hệ treo
- Vấn đề nằm ở một trong các giá trị đầu vào dùng cho phép tính đường hệ treo
- Phép tính sử dụng giới hạn trên/dưới của hệ treo trong
handling.cfg và tỷ lệ bánh xe trong vehicles.ide
- Các giá trị của Skimmer trong
handling.cfg không khác đáng kể so với các máy bay khác
Dòng vehicles.ide quá ngắn của Skimmer
- Định nghĩa Skimmer trong
vehicles.ide ngắn hơn các máy bay khác và bị thiếu 4 tham số cuối
- Trong các giá trị bị thiếu có 2 giá trị là tỷ lệ bánh trước và bánh sau
- Với thuyền thì không có các giá trị này cũng không sao, nhưng Skimmer là chiếc máy bay duy nhất bỏ qua các tham số đó
- Có vẻ Skimmer ban đầu được định nghĩa là thuyền trong Vice City rồi được đổi thành máy bay trong San Andreas, nhưng các tham số mới cần thiết đã không được thêm vào
- Nếu chèn lại các tham số bị thiếu, Skimmer sẽ hoạt động bình thường
Loader không kiểm tra giá trị trả về của sscanf
CFileLoader::LoadVehicleObject phân tích một dòng trong vehicles.ide bằng sscanf và giả định rằng mọi tham số luôn tồn tại
- Hàm này không kiểm tra giá trị trả về của
sscanf, và cũng không gán giá trị mặc định cho hầu hết các tham số cuối
wheelModelID không được khởi tạo
frontWheelScale, rearWheelScale cũng không được khởi tạo
- Chỉ có
wheelUpgradeClass được khởi tạo thành -1
- Với những dòng thiếu giá trị như Skimmer, các biến tỷ lệ bánh xe sẽ giữ nguyên trạng thái chưa khởi tạo và giá trị đó lan sang dữ liệu phương tiện
- Bản sửa của SilentPatch bọc lời gọi
sscanf và cung cấp giá trị mặc định cho 4 giá trị cuối
wheelModelID = -1
frontWheelSize = 0.7f
rearWheelSize = 0.7f
wheelUpgradeClass = -1
- Commit sửa lỗi đã được đưa vào kho lưu trữ SilentPatch
Vì sao lỗi này ẩn suốt 20 năm
- San Andreas sử dụng CRT được biên dịch tĩnh, nên không phải hotfix ở mức CRT của Windows đã làm thay đổi hành vi của
sscanf
- Trên Windows 10, tại vị trí biến cục bộ ngay trước lúc parse Skimmer vẫn còn sót lại giá trị
0.7
- Giá trị này trùng với tỷ lệ bánh xe của TopFun được định nghĩa ngay trước Skimmer
- Dòng TopFun chứa
-1, 0.7, 0.7, -1
vehicles.ide được đọc theo thứ tự và mỗi dòng sẽ gọi LoadVehicleObject
- Trên Windows 10, vị trí stack đó không bị ghi đè giữa các lần gọi
LoadVehicleObject, nên Skimmer đã tình cờ thừa hưởng tỷ lệ bánh xe của TopFun
- Trên Windows 11 24H2, trong quá trình đọc dòng kế tiếp,
LeaveCriticalSection bên trong fgets dùng nhiều không gian stack hơn, khiến giá trị còn sót lại bị ghi đè
Windows 11 24H2 chỉ là tác nhân kích hoạt
- Cách các hàm WinAPI nội bộ sử dụng stack không phải là hành vi được cam kết và có thể thay đổi mà không cần báo trước
- Windows 11 24H2 chỉ đơn giản là xóa đi giá trị stack còn sót lại mà game đã vô tình dựa vào; nguyên nhân thực sự là hành vi không xác định của chính game
- Ngay cả trên Windows 10, biến cục bộ ngay sau tỷ lệ bánh xe đã bị
LeaveCriticalSection ghi đè, và game thực ra đã ở trong trạng thái có thể gặp lỗi này từ nhiều năm trước
- Vì San Andreas còn hỗ trợ cả Windows 98, lỗi này đã vô tình không lộ ra trên ít nhất hơn 10 phiên bản Windows và nhiều bản phát hành Wine khác nhau
- Bản vá PC chính thức 1.01 không sửa lỗi này, nhưng bản phát hành Xbox gốc đã có chỉnh sửa gán giá trị mặc định
1.0
- Steam 3.0, newsteam, RGL đều dựa trên nhánh mã Xbox nên đã thừa hưởng bản sửa này
- Các bản Android của War Drum Studios, X360, PS3 và Definitive Edition cũng không bị ảnh hưởng
Vì sao SilentPatch chọn 0.7 làm giá trị mặc định
- SilentPatch dùng tỷ lệ bánh xe mặc định
0.7 thay vì 1.0 như bản sửa Xbox của Rockstar
- Có ba lý do cho lựa chọn này
- Trên bản PC, từ trước đến nay Skimmer thực tế vẫn luôn hoạt động với tỷ lệ bánh xe
0.7 của TopFun
- Sea Sparrow và Vortex, hai phương tiện không phải thuyền nhưng nổi trên mặt nước, cũng có tỷ lệ bánh xe
0.7
- Nhiều ô tô trong game cũng dùng tỷ lệ bánh xe
0.7
Cách tự sửa
- Bản sửa mã sẽ được đưa vào hotfix SilentPatch tiếp theo
- Nếu muốn sửa ngay, hãy mở
data\vehicles.ide trong thư mục San Andreas bằng Notepad và thay thế dòng bắt đầu bằng 460, skimmer
- Dòng cần thay thế là như sau
460, skimmer, skimmer, plane, SEAPLANE, SKIMMER, null, ignore, 5, 0, 0, -1, 0.7, 0.7, -1
Bài học từ khả năng tương thích của game cũ
- Đây là một lỗi đơn giản của San Andreas, và hàm liên quan vốn dĩ là đoạn mã không thể hoạt động đúng theo thiết kế ban đầu
- Ngay cả thay đổi về bố cục stack trong triển khai nội bộ cũng có thể dẫn đến vấn đề tương thích nếu ứng dụng có lỗi vô tình phụ thuộc vào một hành vi cụ thể
- Một ví dụ tương tự là Bully: Scholarship Edition từng bị lỗi trên Windows 10 vì dựa vào các giả định sai và chỉ lộ vấn đề khi hệ điều hành thay đổi
- Vấn đề gốc của San Andreas là thiếu kiểm tra dữ liệu đầu vào, nên không thể loại bỏ các dòng cấu hình không đầy đủ
- Đoạn mã này nhiều khả năng vốn đã phát sinh cảnh báo biên dịch, và việc bỏ qua hoặc tắt cảnh báo có thể khiến những lỗi ẩn suốt thời gian dài trở thành vấn đề thực tế với người dùng
1 bình luận
Ý kiến trên Hacker News
Bài viết kiểu này đạt mức mà người ta chỉ kỳ vọng ở Raymond Chen, và đó là một lời khen rất lớn.
Thật vui khi tác giả còn đào sâu để chỉ ra chính xác vì sao lại như vậy.
Cá nhân tôi nghĩ rằng nếu đó là hành vi không nằm trong hợp đồng thì nên ngẫu nhiên hóa nó.
Ví dụ, nếu một ngôn ngữ không bảo đảm thứ tự duyệt map, thì nên cố ý ngẫu nhiên hóa thứ tự đó.
Nếu không, sẽ sinh ra loại mã mong manh “chạy ổn cho đến một ngày nào đó thì vỡ”.
-ftrivial-auto-var-init, để khởi tạo biến chưa được khởi tạo bằng một giá trị cụ thể hoặc giá trị ngẫu nhiên.Nhưng nếu ngẫu nhiên hóa hoặc điền 0 vào toàn bộ nội dung stack ở mỗi lần gọi hàm thì hiệu năng sẽ tụt thảm hại, nên thường không làm vậy.
Có những công cụ làm việc này cho mục đích debug, nhưng ở chế độ đó chương trình chạy chậm hơn rất nhiều.
Có lẽ đây cũng là lý do các maintainer kernel Linux luôn khăng khăng không bao giờ được phá vỡ user space.
Khi có đủ nhiều người dùng API, việc hợp đồng hứa hẹn điều gì không còn quan trọng; sẽ có ai đó phụ thuộc vào mọi hành vi quan sát được của hệ thống.
Nếu bạn hứa ngẫu nhiên hóa, sẽ có người phụ thuộc cả vào sự ngẫu nhiên hóa đó.
Khi ấy bạn cũng sẽ không thể loại bỏ nó mãi mãi.
Bạn không bị buộc phải trả overhead không cần thiết như khởi tạo những biến không dùng đến.
Ở phần “đừng bỏ qua cảnh báo biên dịch”, tôi không rõ ở đây có thể kỳ vọng lỗi biên dịch nào.
Có phải chỉ là việc không kiểm tra xem giá trị trả về của
scanfcó khớp với số lượng đối số hay không? Ngoài ra thì trông giống lỗi file dữ liệu mà trình biên dịch không thể biết được.sscanf.Trong ví dụ nhỏ, thêm
g++ -Wall -Wextra -Wunused-resultcũng không thấy cảnh báo.Nhưng vì cả dòng được parse bằng một lần gọi
sscanfduy nhất, phân tích tĩnh của trình biên dịch buộc phải giả định rằng các giá trị giờ đã được khởi tạo.Có vẻ không có phương pháp phân tích tĩnh tổng quát nào để bắt bug này.
Tuy vậy, có thể tạo cảnh báo riêng cho
scanf, buộc phải truyền vào giá trị đã được khởi tạo trước hoặc phải kiểm tra giá trị trả về.Đọc những bài phân tích kỹ thuật sâu như thế này lúc nào cũng thú vị.
Tôi tò mò không biết trong thời đại AI, những bài như vậy sẽ trở nên hiếm hơn hay không.
AI sẽ không thay thế họ, cũng như hơn 50 năm đổi mới trong phát triển phần mềm vừa qua đã không làm được điều đó.
Hàng triệu, có lẽ hàng chục triệu lập trình viên ngôn ngữ bậc cao chỉ biết khác biệt giữa stack và heap như một lý thuyết mơ hồ từng học ở trường, và vì công việc hằng ngày không cần bận tâm nên họ cũng không quan tâm.
Tôi tò mò hơn là trong phiên bản Windows này, cách triển khai khóa/mở khóa critical section đã thay đổi điều gì.
Có phải chỉ mình tôi thấy đoạn mã này khó chịu không?
while (this->m_fBladeAngle > 6.2831855) { this->m_fBladeAngle = this->m_fBladeAngle - 6.2831855; }Cảm giác như họ dùng vòng lặp
whilecó thể thành vòng lặp vô hạn chỉ vì lười làm phép chia.Nhưng xét việc họ từng parse JSON bằng
sscanfkhiến thời gian tải GTA5 tăng thêm 5 phút, tôi không kỳ vọng nhiều.Trình biên dịch cũng có thể có kỹ thuật tối ưu hóa việc này tốt hơn.
Thực tế gần như không có cách nào để nó thành vòng lặp vô hạn. Underflow thì có thể, nhưng để vậy thì góc vốn đã phải nhỏ hơn
2*pi, nên vòng lặp sẽ thoát.fmod.Ai gặp vấn đề truy cập thì có thể dùng liên kết này.
https://web.archive.org/web/20250423144746/https://cookieplm...
Vì biết C/C++, ngay từ đầu blog tôi đã đoán đại khái chuyện gì đang xảy ra, tức là vấn đề biến chưa được khởi tạo.
Thật đáng kinh ngạc khi có một ngôn ngữ cho phép để biến ở trạng thái chưa khởi tạo. Điều này đã gây ra vô số bug, gồm cả những bug production tôi từng trực tiếp thấy, và để bắt chúng thường phải dựa vào cờ biên dịch bổ sung, công cụ phân tích tĩnh, Valgrind, v.v.
Dù các ngôn ngữ hiện đại hơn chọn các cách giải khác, như dùng giá trị 0 mặc định hoặc bắt buộc khởi tạo trước khi dùng, người ta vẫn tiếp tục quay lại C/C++.
Đoạn “Tất cả những phát hiện này chứng minh bug không phải là vấn đề của Windows 11 24H2. Những thứ như cách các hàm WinAPI nội bộ sử dụng stack không phải là hợp đồng và có thể thay đổi bất cứ lúc nào mà không cần báo trước” làm tôi nhớ đến một bài viết tuyệt vời từng đọc.
Đại ý là với một API đủ thành công thì không tồn tại thứ gọi là API không công khai.