Linux C "free(): double free detected in tcache2

OP (Firma: JDCC) #7079638
Lesenswert?

Servus,

es klemmt mal wieder.

Ich bekomme die Fehlermeldung nach fork() und Ausführen des Threads
unmittelbar bei oder vor exit( 0);. Alle free() in allen Dateien sind
durch eine Abfrage ob der pointer != NULL ist abgesichert.

Im Web gibt es einige Seiten die auf ein Problem mit dem Thread-Cache
hinweisen - so ganz blicke ich da aber nicht durch (vor Allem was ich
da machen kann).

Hat vielleicht jemand eine Idee ?

Ubuntu, GCC
Gast #7079655
Lesenswert?

Joachim D. schrieb:
> Alle free() in allen Dateien sind
> durch eine Abfrage ob der pointer != NULL ist abgesichert.

unnötig.

> free(): double free detected

Das bedeutet, dass du free() zwei mal für den selben Pointer ausrufst.
Das kann auch jeweils einmal aus zwei Threads passieren.

Das ist ein schwerer Fehler und führt zu Undefined Behavior.
Gast #7079713
Lesenswert?

fork erzeugt einen neuen Prozess, keinen Thread - gemeinsam genutzter 
Speicher entfällt also.  exit in Kindprozessen ist meist unpassend - 
üblich ist _exit.  Der überflüssige NULL-check vor free ist bereits 
genannt worden.  free wird auch innerhalb der Libraries genutzt - ein 
doppelter fclose z.B. erzeugt auch so eine Meldung.
Gast #7079747
Lesenswert?

Ich kann deinen Sourcecode von hier aus nicht erkennen,
aber mein erster Gedanke:

malloc() vor dem fork()?
free() in den beiden Prozessen?

Dat geit net.

fork() klont dir zwar die Pointer, aber nicht den allozierten Speicher.
Gast #7079754
Lesenswert?

Joachim D. schrieb:
> Ein free() auf einen schon freigebenen ptr läßt den
> Prozess eigentlich nicht crashen.

Eben doch. Das ist ein Double-Free. Ein schwerer Speicherfehler. Das ist 
Undefined Behavior.

> Die free()'s habe ich pingelig kontrolliert, da ist eigentlich
> nichts.

Dann kontrolliere es noch einmal.

Oder entferne einfach mal alle free()-Aufrufe und lecke den Speicher.
Tritt es immer noch auf? Nein -> Dann wirds wohl einer deiner 
free()-Aufrufe gewesen sein. Ja -> Dann wirds wohl irgendeine andere 
Ressource über Bande sein, die du doppelt freigibst.
OP (Firma: JDCC) #7079757
Lesenswert?

1
        while( 1){
2
                size = sizeof( a1);
3
                fd = accept( s, (struct sockaddr*) &a1, &size);
4

5
                if ( ( pid = fork()) < 0) {
6
                        printf( "\nfork err %s", strerror( errno));
7
                } else {
8
                        if ( pid == 0) {
9
                                close( s);
10
                                DoCmd( fd);
11
                        } else {
12
                                close( fd);
13
                        }
14
                }
15
        }
1
       static  int
2
        DoCmd(
3
        int fd
4
        )
5
{
6
        int     i, st, size, pid;
7
        char    http_header[4096];
8
        char    *p, *q, ip[256], req[1044], str[2048];
9
        char    *tmp = NULL;
10

11
        JA( "****************************** INCOMING DATA %s / %s ************************************", sysdatel(), systime());
12

13
        jStartTime();
14
        LoadServicesConf();
15

16
        strcpy( ip, inet_ntoa( a1.sin_addr));
17

18
        // vorher mal Speicher resrvieren, bei GET wird die Länge nicht
19
        // übergeben. 16kB reichen für GET.
20

21
        if ( (tmp = calloc( 1, 16384)) == NULL)
22
                ExitError( "GetRecData", "A alloc err tmp");
23

24
        size = GetRecData( fd, &tmp, ip);
25
        memset( http_header, 0, sizeof( http_header));
26
        mopen( tmp, strlen( tmp));
27
        st = 0;
28
        while ( mgetstrim( str)){
29
                if ( ! *str)
30
                        break;
31
                appsprintf( http_header, "%s\n", str);
32
                st += strlen( str);
33
        }
34

35
        printf( "\n  %s %-16s %8d Byte ", systime(), ip, size);
36
        fflush( stdout);
37

38
        // Kram ist da, also ausführen
39

40
        if ( (sfp = fopen( tmpfilename( ip), "w")) == NULL){
41
                JA( "open error %s %s", tmpfilename( ip), strerror( errno));
42
                exit( 0);
43
        }
44

45
        GetInputData( fd, tmp, size, ip, http_header);
46

47
        fclose( sfp);
48

49
        if ( tmp != NULL)
50
                free( tmp);
51

52
        // Senden des Ergebnisses
53

54
//        SendFileToHTTP( fd, ip);
55
        close( fd);
56
        _exit( 0);
57
}
(Auszug) tmp bekommt in GetRecData() evtl. per realloc()
mehr Platz wenn's ein POST ist.
OP (Firma: JDCC) #7079758
Lesenswert?

MaWin schrieb:
> Oder entferne einfach mal alle free()-Aufrufe und lecke den Speicher.
> Tritt es immer noch auf? Nein -> Dann wirds wohl einer deiner
> free()-Aufrufe gewesen sein. Ja -> Dann wirds wohl irgendeine andere
> Ressource über Bande sein, die du doppelt freigibst.

Habe ich schon probiert (wenn der Prozess endelt sollte er ja
eh alles freigeben). Vielleicht ein überzähliges close() ?
Was ist dieser tcache 2 ?
OP (Firma: JDCC) #7079775
Lesenswert?

Auf stackoverflow gefunden:



The Thread Local Cache (tcache) is a performance optimization in glibc. 
Unfortunately, it comes at the expense of security and make some attacks 
much easier, as you have since discovered.

From 
https://sourceware.org/glibc/wiki/MallocInternals#Thread_Local_Cache_.28tcache.29

    While this malloc is aware of multiple threads, that's pretty much 
the extent of its awareness - it knows there are multiple threads. There 
is no code in this malloc to optimize it for NUMA architectures, 
coordinate thread locality, sort threads by core, etc. It is assumed 
that the kernel will handle those issues sufficiently well.

    Each thread has a thread-local variable that remembers which arena 
it last used. If that arena is in use when a thread needs to use it the 
thread will block to wait for the arena to become free. If the thread 
has never used an arena before then it may try to reuse an unused one, 
create a new one, or pick the next one on the global list.

    Each thread has a per-thread cache (called the tcache) containing a 
small collection of chunks which can be accessed without needing to lock 
an arena. These chunks are stored as an array of singly-linked lists, 
like fastbins, but with links pointing to the payload (user area) not 
the chunk header. Each bin contains one size chunk, so the array is 
indexed (indirectly) by chunk size. Unlike fastbins, the tcache is 
limited in how many chunks are allowed in each bin (tcache_count). If 
the tcache bin is empty for a given requested size, the next larger 
sized chunk is not used (could cause internal fragmentation), instead 
the fallback is to use the normal malloc routines i.e. locking the 
thread's arena and working from there.
OP (Firma: JDCC) #7079859
Lesenswert?

Ist ein Webserver. Keine Kommandozeile. Habe eine zum Test eingebaut.

==6731== Memcheck, a memory error detector
==6731== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==6731== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright 
info
==6731== Command: ../../bin/uquery.x --test
==6731==
==6736== Invalid read of size 4
==6736==    at 0xA3BECFB: fclose@@GLIBC_2.2.5 (iofclose.c:48)
==6736==    by 0x10F766: DoCmd (httpsrv.c:177)
==6736==    by 0x10F46F: HttpServer (httpsrv.c:115)
==6736==    by 0x12CD26: main (uquery.c:124)
==6736==  Address 0xa58cbb0 is 0 bytes inside a block of size 472 free'd
==6736==    at 0xA32127F: free (in 
/usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==6736==    by 0xA3BEDA6: _IO_deallocate_file (libioP.h:862)
==6736==    by 0xA3BEDA6: fclose@@GLIBC_2.2.5 (iofclose.c:74)
==6736==    by 0x112F73: GetInputData (input.c:201)
==6736==    by 0x10F757: DoCmd (httpsrv.c:175)
==6736==    by 0x10F46F: HttpServer (httpsrv.c:115)
==6736==    by 0x12CD26: main (uquery.c:124)
...
==6731== LEAK SUMMARY:
==6731==    definitely lost: 0 bytes in 0 blocks
==6731==    indirectly lost: 0 bytes in 0 blocks
==6731==      possibly lost: 0 bytes in 0 blocks
==6731==    still reachable: 2,525 bytes in 7 blocks
==6731==         suppressed: 0 bytes in 0 blocks
==6731== Reachable blocks (those to which a pointer was found) are not 
shown.
==6731== To see them, rerun with: --leak-check=full 
--show-leak-kinds=all
==6731==
==6731== For lists of detected and suppressed errors, rerun with: -s
==6731== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Schaut so aus als wäre das Problem, daß ich einen globalen Filehandle
FILE* verwende, ich vermute mal der sollte nur in dem angeschubsten
Prozess gültig sein. Ich baue das halt um ;(

Die Ausgabe habe ich gekürzt, sonst kommt formerror_text_long
#7089533
Lesenswert?

Marco H. schrieb:
> So was macht man nicht selbst sondern verwendet Bibliotheken. Dann kann
> man sich mit der Anwendung beschäftigen und nicht mit Elementaren Kram.
> Der dir so wie es ausschaut sehr früh auf die Füße fällt.

... und dann implodiert einem halt das Framework irgendwann und man hat 
keine Ahnung, wie man überhaupt rausfindet, wo und warum das sein 
könnte. Wenn man selber Unfug macht, muss das ja nicht unbedingt im 
eigenen Code auffallen.

Nene, so ein eigenes Gebastel und das Lernen von Grundlagen des 
Debuggings sind schon wichtig.
Gast #7089632
Lesenswert?

Georg A. schrieb:
> Nene, so ein eigenes Gebastel und das Lernen von Grundlagen des
> Debuggings sind schon wichtig.

Ja, ist schon wichtig die remote-exploitable Schwachstelle selbst 
einzubauen, wenn man sich seinen eigenen Webserver in C bastelt.
1
char    http_header[4096];
2
...
3
appsprintf( http_header, "%s\n", str);

Ob da ein Überlauf möglich ist? Keine Ahnung. Aber es riecht stark nach 
Fisch.
Gast #7089638
Lesenswert?

Joachim D. schrieb:
> Alle free() in allen Dateien sind
> durch eine Abfrage ob der pointer != NULL ist abgesichert.

Das allein reicht aber IMO nicht, weil es eine race condition gibt. 
Pointer ist ungleich null, Thread 1 kommt durch die if-Abfrage, macht 
free(), wird aber vor dem manuellen Nullen des Pointers wegscheduled. 
Thread 2 kommt deswegen auch durch die if-Abfrage, es kommt zum double 
free.
#7090092
Lesenswert?

Joachim D. schrieb:
> Georg,
>
> ich sehe das auch so.
>
> Ich bin mit diesen "Fertig-all-inclusive-Sachen" schon ziemlich auf
> die Schnauze geflogen. Nicht nochmal ;)

Zuerst schaut man sich den Vorschlag an, dann wird man Festellen das der 
Vorschlag gar nicht so dumm war.

HTTP ist mehr als man denkt. Sein Kram wird jenseits von GET an den 
modernen Browsers schon scheitern. Wenn die Daten größer werden und der 
Browser anfängt mit Chunks ist sein Latein sowie so zu ende. Ich sehe 
das es jetzt schon der Fall ist, wenn man nur mal so in das HTTP 
Protokoll schaut welche Anforderungen selbst bei simplen Anfragen GET 
Anfragen zu erfüllen sind.

Nur den Header auseinander zu nehmen, wird ich ohne Bibliotheken nicht 
mehr alleine aufsetzen wollen. Das mag vor 20 Jahren aufgrund von 
alternativen und der noch Einfachheit des Protokolls anderes gewesen 
sein.

Das läuft vom Ansatz her gegen den Baum, ohne die Endanwendung zu 
betrachten.

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren