Z pewnym moim przyjacielem portujemy pewną aplikację m.in. na system z rodziny Windows. Aplikacja była pisana już jakiś czas temu, i korzysta z w/w funkcji - gettimeofday - do odczytu czasu. Ponieważ jest to nowa funkcja ze stosunkowo nowego standardu IEEE Std 1003.1:2001 (POSIX), to jej implementacja nie znalazła się w Microsoft C Runtime Library aka msvcrt.dll, ani w żadnej innej bibliotece systemu Windows która wpadła w moje ręce podczas poszukiwań. W związku z czym powstała potrzeba jej zaimplementowania (skorzystanie z gotowej implementacji odrzuciliśmy - czytanie licencji trwało by dłużej niż pisanie nowej funkcji).
Kilka słów na temat funkcji gettimeofday. Zacznijmy od prototypu:
int gettimeofday(struct timeval *tp, struct timezone *tpz);
Jak widać wyżej, gettimeofday przyjmuje dwie struktury które wypełnia (nie ma const przy pointerach, stąd wniosek że struktury są wypełniane). Zacznijmy od drugiej struktury - jak można przeczytać w man'ie, jest to struktura nieużywana (noo przynajmniej pod Linuxem, ale w portowanym projekcie zakładamy że również jest nieużywana) ;>
The use of the timezone struct is obsolete: the tz_dsttime field has
never been used under Linux; it has not been and will not be supported
by libc or glibc. Each and every occurrence of this field in the ker-
nel source (other than the declaration) is a bug. Thus, the following
is purely of historic interest.
Jeden struct z głowy, został struct timeval, który jest standardowym (w bardzo wąskim znaczeniu tego słowa) structem definiowanym na kilka sposobów:
Linux Programmer's Manual:
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
MinGW sys/time.h:
struct timeval {
long tv_sec;
long tv_usec;
};
Komentarze w pierwszym strukcie wyjaśniają sporo, ale żeby było to jasne i oczywiste dla wszystkich, ponownie to napiszę. Struct ma dwa pola, pierwsze - tv_sec - zawiera Unix Time Stamp, a drugie - tv_usec - zawiera mikrosekundy (10E-6), czyli ma na celu zwiększenie precyzji struktury opisującej moment na osi czasu (wartości przyjmuje od 0 do 999999).
By the way...
On 22nd Nov'24 we're running a webinar called "CVEs of SSH" – it's free, but requires sign up: https://hexarcana.ch/workshops/cves-of-ssh (Dan from HexArcana is the speaker).
Wracając do funkcji, wystarczy więc wypełnić jakoś oba pola. Żeby jeszcze ułatwić, zakładam że wystarczy nam dokładność na poziomie milisekund (10E-3). Pole tv_sec można oczywiście wypełnić wywołując funkcję time(NULL) która zwraca dokładnie to co potrzeba, czyli Unix Time Stamp. Co do tv_usec, to do wyboru jest kilka funkcji. Osobiście wybrałem GetLocalTime z WinAPI, która wypełnia strukturę SYSTEMTIME, w której między innymi są pola wMilliseconds zawierające milisekundy (od rozpoczęcia ostatniej pełnej sekundy lokalnego czasu ;>), oraz wSeconds (ilość sekund w danej minucie). Przykładowy kod wygląda więc tak:
int
gettimeofday (struct timeval *tp, struct timezone *tzp)
{
SYSTEMTIME st;
/* timezone is obsolute according to UNIX man pages */
(void) tzp;
/* Set time */
tp->tv_sec = (long) time (NULL);
/* Get time info from the system */
GetLocalTime (&st);
tp->tv_usec = (long) st.wMilliseconds * 1000;
/* return success, nothing there to fail */
return 0;
}
Nyom. I implementacja gotowa. To tyle na dzisiaj, do zobaczenia ;>
Hee? ^_-; A co z tym race condition o który pisałeś w tytule ???
Hyhy żartuje, jeszcze nie koniec. Dopiero dotarliśmy do najciekawszego.
Z implementacją na pierwszy rzut jest wszystko OK. Pobierane są sekundy, następnie są wrzucane w strukta, potem pobierane są milisekundy i wrzucane w strukta, i tyle. Niemniej jednak jest pewien złośliwy przypadek który sprawia że gdy pobierzemy czas w dwóch momentach, A oraz B, to różnica B - A będzie ujemna (mimo że B następuje wyraźnie PO A).
Przypadek ten występuje w momencie gdy wywołanie time(NULL) następuje w ostatniej "chwili" danej sekundy, np. gdy pozostaje jedna nanosekunda do końca danej sekundy (np. obecna sekunda to 10.999999999999999), a wywołanie do GetLocalTime następuje kilka nanosekund później, gdy sekundy już się "przekręcą" na następne, a miliseundy wyzerują (czyli 11.0, niemniej jednak sekundy już są wypełnione, więc w strukcie zostanie 10.0 zamiast 11.0 - następuje powrót do przeszłości ;>). Możemy to sprawdzić uruchamiając następujący kod:
int
main (void)
{
struct timezone tzp;
static struct timeval tp[2];
int i = 0, j;
while (1)
{
gettimeofday (&tp[i], &tzp);
j = !i;
/* until the future is in the past */
if(tp[i].tv_sec == tp[j].tv_sec && tp[i].tv_usec < tp[j].tv_usec)
break;
i = j;
}
printf ("%u.%u\n", tp[j].tv_sec, tp[j].tv_usec);
printf ("%u.%u\n", tp[i].tv_sec, tp[i].tv_usec);
return 0;
}
Kompilacja i uruchomienie:
22:38:04 gynvael >gcc gettime.c -DRACE_CONDITION_TEST
22:38:41 gynvael >a
1223930327.999000
1223930327.0
22:38:48 gynvael >
Jak widać po 8 sekundach działania zdarzył się przypadek "wyzerowania" milisekund między wywołaniami dwóch funkcji do pobierania czasu.
Należy zatem jakoś rozwiązać ten problem. Ignorując na chwilę sekundy przestępne możemy to zrobić w bardzo prosty sposób - sprawdzić czy ostatnia cyfra dziesiętna sekund tv_sec oraz wSecond się zgadza. Jeżeli nie, to nastąpiło "wyzerowanie", i należy dodać do tv_sec jedną sekundę.
Ostateczny kod funkcji wygląda tak:
int
gettimeofday (struct timeval *tp, struct timezone *tzp)
{
SYSTEMTIME st;
/* timezone is obsolute according to UNIX man pages */
(void) tzp;
/* Set time */
tp->tv_sec = (long) time (NULL);
/* Get time info from the system */
GetLocalTime (&st);
tp->tv_usec = (long) st.wMilliseconds * 1000;
/* Anti race condition sec fix
* When the milliseconds are at 999 at the time of call to time(), and at
* 999+1 = 0 at the time of the GetLocalTime call, then the tv_sec and
* tv_usec would be set to one second in the past. To correct this, just
* check if the last decimal digit of the seconds match, and if not, add
* a second to the tv_sec.
*/
if (tp->tv_sec % 10 != st.wSecond % 10)
tp->tv_sec++;
/* return success, nothing there to fail */
return 0;
}
Nie jest to oczywiście najlepsze z możliwych rozwiązań, jednak jest stosunkowo szybkie (najlepsze było by załatwienie obu pól strukta jednym wywołaniem, ale wtedy nie było by o czym napisać posta ;>).
Zapraszam do wrzucania swoich propozycji rozwiązania problemu do komentarzy =^^=
OK, tym razem na prawdę koniec posta. Dziękuje za uwagę ;>
PS. gratz dla IceWalla za obejście mojego systemu "captcha" =^^= jak będę miał chwilę zrobię level drugi =^^=
Comments:
http://www.securityfocus.com/archive/1/447649
Co do bezpieczeństw w przypadku tematu posta, to akurat żaden "bypass" mi nie przychodzi do głowy. Natomiast stabilność aplikacji to inna sprawa. Rozważmy przypadek błędnej implementacji funkcji w aplikacji która co 100 milisekund musi jakąś akcję wykonywać - np. chłodzić reaktor atomowy (przykład z bajki wzięty), i jeżeli powiedzmy przez 150 milisekund nie będzie chłodzenia, to będzie Чернобыль 2. No i sobie program chodzący chodzi w pętli, sprawdza czy od ostatniego momentu upłynęło >= 100 mili sekund, jeśli tak to chłodzi, i zapisuje obecny czas. A tu nagle dochodzimy do przypadku że od ostatniego razu upłynęło -999 milisekund. I teraz może się albo coś stać (np. program może zasnąć na 50 milisekund bo stwierdzi że ma sporo czasu i nie ma co CPU marnować, albo może zamiast long być użyty unsigned long, więc z -999 by się zrobiło 4294966297 milisekund, więc program zaczął by panikować że przez 49 dni nie chłodził, i zużył by cały zbiornik chłodziwa od razu po czym by nie miał czy chłodzić, i bum ;D). Oczywiście to przykład kapkę na siłę, niemniej jednak może komuś coś zilustrować ;>
Add a comment: