Kun bugi ei ollutkaan omassa koodissa – harvinainen havainto koodikirjastosta

16.12.2025

Lukuaika 5-8 minuuttia.

Keväällä 2025 asiakkaan sulautettua Linuxia käyttävässä projektissa koin harvinaisen tilanteen, jossa bugit eivät olleetkaan kirjoittamassani koodissa vaan kolmannen osapuolen kirjastossa, libwebsocketsissa. Versiossa 4.3.3, joka on otettu projektin v4.3-stable -haarasta löytyi ensin yksi, mutta pian myös toinen bugi, jotka aiheuttivat päänvaivaa. Myöhemmin kyseiseen haaraan on tehty useita muutoksia. Lyhyehköissä koodinpätkissä näkyy jo aina niin ihastuttavia yhden kirjaimen muuttujia, joista osan merkitys vaihtuu kesken kaiken.  Tämä kokemus antoi arvokkaan oppitunnin siitä, että on hyvä pysyä hereillä ja tutkia haasteita avoimella mielellä.

Mikä on libwebsockets? 

Libwebsockets on avoimen lähdekoodin kirjasto, joka on alun alkaen nimensä mukaisesti ajateltu käytettäväksi websockettien palvelinpuolen toteuttamiseen. Kirjaston laajuuden ansioista sillä voidaan kuitenkin toteuttaa myös koko http-palvelin. Projektissa, jonka parissa tuolloin työskentelin oli tehty juuri niin. 

Libwebsocketsin avulla hoidettiin staattiset web-sivut ja rajapinta AJAX:lle (REST API). Tällä kertaa olimme toteuttamassa CGI-skriptejä ensimmäistä kertaa kirjaston avulla. CGI (Common Gateway Interface) -skriptejä käytetään muun muassa dynaamisten web-sivujen luomiseen. Ne toimivat hyvin yksinkertaisesti: kun selaimelta tulee pyyntö, palvelin käynnistää uuden prosessin, jossa skripti (esim. bash- tai Python-skripti) suoritetaan ja tuon prosessin tulosteet välitetään takaisin selaimelle. Skriptille voidaan kirjoittaa esimerkiksi, selaimelta POST -pyynnöstä  tulevaa dataa. 

Libwebsockets on ikään kuin framework. Siinä ohjelma tarjoaa libwebsocketsille tapahtumakäsittelijän ja toteuttaa callback-funktion, jossa halutut toiminnot voidaan suorittaa sekä tarvittaessa kutsua jälleen libwebsocketsin funktioita. Oman koodin callback-funktio saa myös tiedon siitä, miksi sitä on kutsuttu. Mahdollisia syitä on kymmeniä ja syyn perusteella valitaan oikea toiminta. Syyt on listattu lws_callback_reasons -enumeraatiossa. Kokonaisuutena libwebsocketsin oppimiskynnys on korkea ja sen dokumentaatio on niukka, mutta myös hyviä esimerkkejä on saatavilla. 

Ensimmäinen bugi: CGI-skriptin datan käsittely

Seikkailu tosiaan alkoi sillä, että CGI-skriptit, jotka saivat vain vähän dataa selaimelta toimivat, mutta ne, joille lähetettiin enemmän dataa, eivät toimineet lainkaan. Skriptin suoritus keskeytyi, ja selain sai virhekoodin. 

Luonnollisesti aluksi keskityin etsimään vikaa omasta koodista, sillä siellähän ne yleensä ovat. Ensin asetin tulosteen, jolla näin millä syyllä libwebsockets kutsuu callback-funktiotani. Eräs callback-funktiolle annettu kutsun syy oli LWS_CALLBACK_CGI_TERMINATED. 

Näytti siltä, että datan lähettäminen keskeytyy ja päättyy jonkinlaiseen virhetilanteeseen ja selaimelle vastataan virhekoodilla. Lisäkokeet osoittivat, että skripti kuoli ennenaikaisesti ennen kuin kaikki lähetetty data oli vastaanotettu.

Debuggaus ja ongelman paikantaminen

Seuraavaksi asetin breakpointin käsittelemään kutsusyytä LWS_CALLBACK_CGI_STDIN_DATA (jota käytetään datan kirjoittamiseen skriptin prosessille). Tarkoitukseni oli vain jatkaa suoritusta joka pysähdyksen jälkeen, kunnes jotain menisi eri tavalla. Hämmästyksekseni skripti menikin loppuun asti saaden kaiken datan. Tämä viittasi ajoitusongelmaan.

Tuossa haarassa, kuten monessa muussakin haarassa, kutsutaan, hieman hölmösti nimettyä, libwebsocketsin funktiota: lws_callback_http_dummy(). Funktion pitäisi tilanteesta riippuen suorittaa varsinaisia toimenpiteitä. Testasin vielä niin, että hidastin toimintaa lisäämällä sleepin ennen funktion kutsumista. Skripti toimi edelleen.

Nyt alkoi vaikuttaa siltä, että vika ei ollutkaan tällä kertaa omassa koodissa. Tein debug-käännöksen libwebsocketsista ja aloin tarkastella, mitä tapahtuu ennen kuin omaa callback -funktiota kutsutaan syyllä: LWS_CALLBACK_CGI_TERMINATED. 

Ongelma paikallistui libwebsocketsin koodiin tarkemmin lws_callback_http_dummy()-funktioon ja seuraavaan riviin:

n = (int)write(n, args->data, (unsigned int)args->len);

Virhetilanteessa POSIX write() -funktio palauttaa -1:n ja asettaa errno -muuttujan vastaamaan tapahtunutta virhettä. Debuggerilla havaitsin, että paluuarvo oli -1 ja errno oli asetettu arvoon EAGAIN. Tässä vaiheessa oli jo ilmeistä missä vika piilee. 

EAGAIN tarkoittaa, että asynkronisessa kommunikaatiossa vastaanottopää ei ole valmis vastaanottamaan ja tulisi yrittää myöhemmin uudelleen. Kirjaston koodi ei tarkistanut errno-muuttujaa, jättäen huomiotta tämän varsin odotettavissa olevan asynkronissessa kommunikaatiossa sattuvan tilanteen.

Pikainen korjaustoimenpide

Myönnettäköön, ettei korjaukseni tähän kuitenkaan ollut kovin kaunis. Koska en halunnut käyttää enempää aikaa libwebsocketsin rakenteeseen, en tehnyt korjausta oikealla tavalla, tarkastamalla errno -muuttujaa ja järjestämällä niin, että olisi tarvittaessa voitu yrittää myöhemmin uudelleen. Sen sijaan päädyin ratkaisuun, jossa odotetaan, että prosessi on valmis vastaanottamaan dataa. Tämä osaltaan vesittää asynkronisen kommunikoinnin idean, mutta tässä tapauksessa se ei ollut kovin vakavaa, sillä aikakriittisempi toiminta on joka tapauksessa eri säikeessä. 

Lisäsin ennen ongelmariviä seuraavan koodinpätkän, käyttäen select()-funktiota odottamaan korkeintaan 100 millisekuntia:

fd_set fds;
FD_ZERO(&fds);
FD_SET(n, &fds);

struct timeval timeout = {.tv_sec = 0, .tv_usec = 1e5};

if(select(n + 1, NULL, &fds, NULL, &timeout) <= 0)

return -1;

Jos prosessi ei ole valmiina määräajan puitteissa, palautetaan -1, kuten siinäkin tapauksessa, jossa tapahtuisi sama virhe kuin aiemmin. Kaiketi tuota select() -funktiotakaan ei pitäisi enää käyttää, mutta käytin silti.

Pian ilmeni toinen bugi

Korjauksen kanssa pystyimme elämään jonkin aikaa onnellisena, kunnes alkoi tulla segfaulteja. Kun libwebsockets oli jo valmiiksi käännetty debug-symboleilla, segfault oli helppo paikallistaa sen sisään debuggerilla. 

Segfault tuli tällä kerralla cgi-server.c -tiedostossa rivillä:

memmove(start + m, start, (unsigned int)n);

missä esiintyvä m saa arvonsa näin:

m = lws_snprintf(chdr, LWS_HTTP_CHUNK_HDR_SIZE - 3,

                     "%X\x0d\x0a", n);

n saa arvonsa näin:

n = (int)read(n, start, sizeof(buf) - LWS_PRE);

ja funktion alussa on asetettu näin:

unsigned char buf\[LWS_PRE + 4096], *start = &buf\[LWS_PRE]

Segfault johtui siitä, että memmove()-funktiolla yritettiin kopioida dataa buf-taulukon määritellyn rajan yli. On nähtävissä, että näin tapahtuu, jos

n > sizeof(buf) – m – (start - buf)

Nähdään, että m ≤ LWS_HTTP_CHUNK_HDR_SIZE – 3 ja start = buf + LWS_PRE.

Toisin sanoen ongelmia tulee, jos

n > sizeof(buf) – LWS_HTTP_CHUNK_HDR_SIZE – 3 - LWS_PRE

Korjaus: 

Nythän n voi olla sizeof(buf) - LWS_PRE, eli korjaus on muuttaa rivi

n = (int)read(n, start, sizeof(buf) - LWS_PRE);

riviksi:

n = (int)read(n, start, sizeof(buf) - LWS_PRE - LWS_HTTP_CHUNK_HDR_SIZE - 3);

Ratkaisuni tuntui hieman rumalta lähinnä tuon yhden literaalivakion vuoksi. Myöhemmin tämä bugi korjattiin myös libwebsocketsin kehittäjien toimesta muuttamalla rivi seuraavaksi.

n = (int)read(n, start, sizeof(buf) - LWS_PRE - 16);

Ensimmäistä bugia ei kuitenkaan ole vielä tämän blogin kirjoitushetkellä korjattu.

Joskus on sukellettava syvälle omaan ja muiden koodiin, jotta syy löytyy

Tarinan opetus lienee se, että vaikka useimmiten bugin syy löytyy omasta koodista, niin näin ei aina ole. Tämä kokemus muistuttaa, että joissakin harvinaisissa tapauksissa myös kirjastokoodissa voi olla vikaa. Kehittäjän on oltava valmis sukeltamaan syvälle myös kirjastokoodiin, kun tavanomaiset debuggausmenetelmät eivät tuota tulosta.

Marko Saarinen

SHARE

MORE POSTS

Tickingbot Oy, Visiokatu 4, 33720 Tampereinfo@tickingbot.fiLinkedIn

Privacy policy