Cloudflare inženieri atrada kļūdu hyper bibliotēkā — Rust valodā rakstītā HTTP bibliotēkā, uz kuras balstās liela daļa Rust tīmekļa koda. Kļūda reizēm nogrieza lielas HTTP atbildes uz pusi: 3,3 MB attēls klientam pienāca kā aptuveni 200 KB, taču ar statusu 200 OK un pareizu faila garuma galveni. Serveris uzskatīja, ka viss izdevies.
Problēma slēpjas hyper HTTP/1 apstrādes ciklā un skar versijas no 0.14.x līdz jaunajām 1.7.x un 1.8.x. Cloudflare uz to uzdūrās 2025. gada decembrī, pārbūvējot Workers Images sasaisti. Diagnostika prasīja aptuveni sešas nedēļas, pats labojums — četras koda rindas.
Kā izpaudās
Decembrī Cloudflare Images pārstrādāja Workers Images sasaisti un starp servisiem ielika jaunu starpnieku, kas datus padod pa Unix ligzdām. Dažas dienas vēlāk pienāca pirmie ziņojumi: daži klienti sāka saņemt nepilnus attēlus.
Mazie attēli nāca veseli, lielie — apgriezti. Apgriešanas vieta sakrita ar ligzdas (socket) buferi, aptuveni 200 KB. Tas bija pirmais pavediens: dati pazuda tieši tur, kur pildījās raidīšanas buferis. Attēlu pārveidošana Cloudflare tīklā notiek uz malas serveriem, tāpēc kļūda skāra reālus klientu pieprasījumus, ne testa vidi.
Kļūda parādījās neregulāri. Viens un tas pats pieprasījums vienreiz nostrādāja pareizi, citreiz atdeva apgrieztu failu. Tāda uzvedība norāda uz sacensību apstākli (race condition) — rezultāts ir atkarīgs no tā, kura no divām darbībām paspēj notikt pirmā. Šādu kļūdu ir grūti noķert, jo to nevar palaist pēc pieprasījuma; tā parādās tikai tad, kad laika logs sakrīt.
Kāpēc parastie rīki klusēja
Lietojuma žurnāli neko neuzrādīja. Izkliedētā izsekošana starp servisiem arī bija tīra — katrs posms ziņoja par veiksmi. Klients redzēja statusu 200 un derīgu Content-Length, tikai trūka daļa datu. Kļūda bija zemāk, tīkla slānī.
Sākumā aizdomas krita uz jauno starpnieku, kas pievienots decembrī, taču tā koda pārbaude neko neatrada. Atbilde atklājās ar strace, kas pieraksta operētājsistēmas sistēmizsaukumus. Tas parādīja, ka hyper izsauc ligzdas slēgšanu, pirms visi baiti ir aizrakstīti kodolam. Salīdzinot sekmīga un neizdevušos pieprasījuma izsaukumu virknes, atšķirība bija skaidra. Programma domāja, ka atbilde nosūtīta; kodols vēl turēja daļu datu buferī, un tie tika nomesti līdz ar savienojuma slēgšanu.
Cēlonis: viena ignorēta atgriešanas vērtība
Vaina bija failā dispatch.rs, HTTP/1 savienojuma apstrādes ciklā. Kods izsauca poll_flush(), bet tā rezultātu izmeta ar let _ = self.poll_flush(cx)?;. Ja buferis vēl nebija iztukšots, poll_flush atgriež Poll::Pending — signālu «vēl neesmu pabeidzis». Šo signālu cikls ignorēja un devās slēgt savienojumu.
Mazām atbildēm tas nekļuva par problēmu, jo tās ietilpst buferī uzreiz. Lielai atbildei kodola raidīšanas buferis piepildās, poll_flush paziņo, ka jāgaida, bet hyper tik un tā turpina uz slēgšanu. Neizsūtītie baiti pazūd, un savienojums aizveras it kā normāli.
Kļūda pastāvēja gandrīz visās hyper versijās, ko Cloudflare pārbaudīja, un tik ilgi palika nepamanīta tāpēc, ka gala rezultāts izskatījās veiksmīgs — statuss 200, pareizs faila garums galvenē, tikai trūkstoši dati.
Kā reproducēja un salaboja
Lai kļūdu pieķertu droši, Cloudflare uzrakstīja testu, kas mākslīgi samazina ligzdas buferi līdz 8 KB, un pašrocīgu TCP straumes apvalku, kas kontrolēti bloķē rakstīšanu. Ar to sacensību apstākli varēja izsaukt katrā palaišanā, nevis gaidīt nejaušu sakritību.
Labojums ir PR #4018 — dažas rindas funkcijā poll_shutdown(). Pirms savienojuma slēgšanas kods tagad nogaida, līdz poll_flush patiešām pabeidz darbu:
ready!(self.poll_flush(cx)?);
Cloudflare labojumu iesniedza hyper augšposmā, kur tas apstiprināts, un savā atzarā to jau izmanto. Nākamajā hyper laidienā tas nonāks pie visiem.
Ko no tā paņemt līdzi
hyper uztur Sean McArthur kopš 2014. gada, un uz tās balstās reqwest, tīmekļa ietvars axum, daudzi citi Rust servera un klienta rīki, kā arī pati Cloudflare infrastruktūra. Viena zema līmeņa kļūda tā potenciāli skar tūkstošiem projektu, kas hyper nemaz tieši neredz. Viena ignorēta atgriešanas vērtība klusi bojāja datus gadiem, un neviena augšējā līmeņa pārbaude to nepamanīja, jo kļūme nekad neizraisīja kļūdas statusu. Par notikumu rakstīja arī InfoQ, atsaucoties uz sponsorēšanas saikni starp Cloudflare un hyper uzturētājiem un jautājot, kāpēc datu nogriešana netika pieķerta agrāk ar izlašu pārbaudi.
Praktiskā mācība tiem, kas raksta tīklošanas kodu: poll_flush vai flush atgriešanas vērtību nedrīkst izmest. Ja funkcija saka «vēl neesmu pabeidzis», savienojumu slēgt nedrīkst. Un, ja kaut kas lūst tikai virs noteikta datu apjoma, ir vērts skatīties tīkla slānī ar strace, nevis tikai lietojuma žurnālos.
Komentāri
Šim rakstam vēl nav komentāru. Esi pirmais, kurš dalās ar savu viedokli.