#!/usr/bin/env perl #*************************************************************************** # _ _ ____ _ # Project ___| | | | _ \| | # / __| | | | |_) | | # | (__| |_| | _ <| |___ # \___|\___/|_| \_\_____| # # Copyright (C) Daniel Stenberg, , et al. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at https://curl.se/docs/copyright.html. # # You may opt to use, copy, modify, merge, publish, distribute and/or sell # copies of the Software, and permit persons to whom the Software is # furnished to do so, under the terms of the COPYING file. # # This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY # KIND, either express or implied. # # SPDX-License-Identifier: curl # ########################################################################### # # Example input: # # MEM mprintf.c:1094 malloc(32) = e5718 # MEM mprintf.c:1103 realloc(e5718, 64) = e6118 # MEM sendf.c:232 free(f6520) package memanalyzer; use strict; use warnings; BEGIN { use base qw(Exporter); our @EXPORT = qw( memanalyze ); } my $memsum; my $maxmem; sub newtotal { my ($newtot)=@_; # count a max here if($newtot > $maxmem) { $maxmem = $newtot; } } sub memanalyze { my ($file, $verbose, $trace, $showlimit) = @_; my @res; my $mallocs = 0; my $callocs = 0; my $reallocs = 0; my $strdups = 0; my $wcsdups = 0; my $sockets = 0; $memsum = 0; # the total number of memory allocated over the lifetime $maxmem = 0; # the high water mark open(my $fileh, "<", "$file") or return (); if($showlimit) { while(<$fileh>) { if(/^LIMIT.*memlimit$/) { push @res, $_; last; } } close($fileh); return @res; } my %sizeataddr; my %getmem; my $totalmem = 0; my $frees = 0; my $dup; my $size; my $addr; my %filedes; my %getfile; my %fopen; my %fopenfile; my $openfile = 0; my $fopens = 0; my %addrinfo; my %addrinfofile; my $addrinfos = 0; my $source; my $linenum; my $function; my $lnum = 0; while(<$fileh>) { chomp $_; my $line = $_; $lnum++; if($line =~ /^BT/) { # back-trace, ignore } elsif($line =~ /^LIMIT ([^ ]*):(\d*) (.*)/) { # new memory limit test prefix my $i = $3; my ($source, $linenum) = ($1, $2); if($trace && ($i =~ /([^ ]*) reached memlimit/)) { push @res, "LIMIT: $1 returned error at $source:$linenum\n"; } } elsif($line =~ /^MEM ([^ ]*):(\d*) (.*)/) { # generic match for the filename+linenumber $source = $1; $linenum = $2; $function = $3; if($function =~ /free\((\(nil\)|0x([0-9a-f]*))/) { $addr = $2; if($1 eq "(nil)") { ; # do nothing when free(NULL) } elsif(!exists $sizeataddr{$addr}) { push @res, "FREE ERROR: No memory allocated: $line\n"; } elsif(-1 == $sizeataddr{$addr}) { push @res, "FREE ERROR: Memory freed twice: $line\n"; push @res, "FREE ERROR: Previously freed at: $getmem{$addr}\n"; } else { $totalmem -= $sizeataddr{$addr}; if($trace) { push @res, "FREE: malloc at $getmem{$addr} is freed again at $source:$linenum\n"; push @res, "FREE: $sizeataddr{$addr} bytes freed, left allocated: $totalmem bytes\n"; } newtotal($totalmem); $frees++; $sizeataddr{$addr}=-1; # set -1 to mark as freed $getmem{$addr}="$source:$linenum"; } } elsif($function =~ /malloc\((\d*)\) = 0x([0-9a-f]*)/) { $size = $1; $addr = $2; if($sizeataddr{$addr} && $sizeataddr{$addr}>0) { # this means weeeeeirdo push @res, "Mixed debug compile ($source:$linenum at line $lnum), rebuild curl now\n"; push @res, "We think $sizeataddr{$addr} bytes are already allocated at that memory address: $addr!\n"; } $sizeataddr{$addr} = $size; $totalmem += $size; $memsum += $size; if($trace) { push @res, "MALLOC: malloc($size) at $source:$linenum makes totally $totalmem bytes\n"; } newtotal($totalmem); $mallocs++; $getmem{$addr}="$source:$linenum"; } elsif($function =~ /calloc\((\d*),(\d*)\) = 0x([0-9a-f]*)/) { $size = $1 * $2; $addr = $3; my $arg1 = $1; my $arg2 = $2; if($sizeataddr{$addr} && $sizeataddr{$addr}>0) { # this means weeeeeirdo push @res, "Mixed debug compile ($source:$linenum at line $lnum), rebuild curl now\n"; push @res, "We think $sizeataddr{$addr} bytes are already allocated at that memory address: $addr!\n"; } $sizeataddr{$addr} = $size; $totalmem += $size; $memsum += $size; if($trace) { push @res, "CALLOC: calloc($arg1,$arg2) at $source:$linenum makes totally $totalmem bytes\n"; } newtotal($totalmem); $callocs++; $getmem{$addr}="$source:$linenum"; } elsif($function =~ /realloc\((\(nil\)|0x([0-9a-f]*)), (\d*)\) = 0x([0-9a-f]*)/) { my ($oldaddr, $newsize, $newaddr) = ($2, $3, $4); my $oldsize = '-'; if($oldaddr) { $oldsize = $sizeataddr{$oldaddr} ? $sizeataddr{$oldaddr} : 0; $totalmem -= $oldsize; if($trace) { } $sizeataddr{$oldaddr} = 0; $getmem{$oldaddr} = ""; } $totalmem += $newsize; $memsum += $newsize; $sizeataddr{$newaddr} = $newsize; if($trace) { push @res, "REALLOC: $oldsize less bytes and $newsize more bytes ($source:$linenum)\n"; } newtotal($totalmem); $reallocs++; $getmem{$newaddr}="$source:$linenum"; } elsif($function =~ /strdup\(0x([0-9a-f]*)\) \((\d*)\) = 0x([0-9a-f]*)/) { # strdup(a5b50) (8) = df7c0 $dup = $1; $size = $2; $addr = $3; $getmem{$addr} = "$source:$linenum"; $sizeataddr{$addr} = $size; $totalmem += $size; $memsum += $size; if($trace) { push @res, "STRDUP: $size bytes at $getmem{$addr}, makes totally: $totalmem bytes\n"; } newtotal($totalmem); $strdups++; } elsif($function =~ /wcsdup\(0x([0-9a-f]*)\) \((\d*)\) = 0x([0-9a-f]*)/) { # wcsdup(a5b50) (8) = df7c0 $dup = $1; $size = $2; $addr = $3; $getmem{$addr}="$source:$linenum"; $sizeataddr{$addr}=$size; $totalmem += $size; $memsum += $size; if($trace) { push @res, "WCSDUP: $size bytes at $getmem{$addr}, makes totally: $totalmem bytes\n"; } newtotal($totalmem); $wcsdups++; } else { push @res, "Not recognized input line: $function\n"; } } # FD url.c:1282 socket() = 5 elsif($_ =~ /^FD ([^ ]*):(\d*) (.*)/) { # generic match for the filename+linenumber $source = $1; $linenum = $2; $function = $3; if($function =~ /socket\(\) = (\d*)/) { $filedes{$1} = 1; $getfile{$1} = "$source:$linenum"; $openfile++; $sockets++; # number of socket() calls } elsif($function =~ /socketpair\(\) = (\d*) (\d*)/) { $filedes{$1} = 1; $getfile{$1} = "$source:$linenum"; $openfile++; $filedes{$2} = 1; $getfile{$2} = "$source:$linenum"; $openfile++; } elsif($function =~ /accept\(\) = (\d*)/) { $filedes{$1} = 1; $getfile{$1} = "$source:$linenum"; $openfile++; } elsif($function =~ /sclose\((\d*)\)/) { if($filedes{$1} != 1) { push @res, "Close without open: $line\n"; } else { $filedes{$1}=0; # closed now $openfile--; } } } # FILE url.c:1282 fopen("blabla") = 0x5ddd elsif($_ =~ /^FILE ([^ ]*):(\d*) (.*)/) { # generic match for the filename+linenumber $source = $1; $linenum = $2; $function = $3; if($function =~ /f[d]*open\(\"(.*)\",\"([^\"]*)\"\) = (\(nil\)|0x([0-9a-f]*))/) { if($3 eq "(nil)") { ; } else { $fopen{$4} = 1; $fopenfile{$4} = "$source:$linenum"; $fopens++; } } # fclose(0x1026c8) elsif($function =~ /fclose\(0x([0-9a-f]*)\)/) { if(!$fopen{$1}) { push @res, "fclose() without fopen(): $line\n"; } else { $fopen{$1} = 0; $fopens--; } } } # GETNAME url.c:1901 getnameinfo() elsif($_ =~ /^GETNAME ([^ ]*):(\d*) (.*)/) { # not much to do } # ADDR url.c:1282 getaddrinfo() = 0x5ddd elsif($_ =~ /^ADDR ([^ ]*):(\d*) (.*)/) { # generic match for the filename+linenumber $source = $1; $linenum = $2; $function = $3; if($function =~ /getaddrinfo\(\) = (\(nil\)|0x([0-9a-f]*))/) { my $add = $1; if($add eq "(nil)") { ; } else { $addrinfo{$add} = 1; $addrinfofile{$add} = "$source:$linenum"; $addrinfos++; } if($trace) { push @res, "GETADDRINFO ($source:$linenum)\n"; } } # fclose(0x1026c8) elsif($function =~ /freeaddrinfo\((0x[0-9a-f]*)\)/) { my $addr = $1; if(!$addrinfo{$addr}) { push @res, "freeaddrinfo() without getaddrinfo(): $line\n"; } else { $addrinfo{$addr} = 0; $addrinfos--; } if($trace) { push @res, "FREEADDRINFO ($source:$linenum)\n"; } } } else { push @res, "Not recognized prefix line: $line\n"; } } close($fileh); if($totalmem) { push @res, "Leak detected: memory still allocated: $totalmem bytes\n"; for(keys %sizeataddr) { $addr = $_; $size = $sizeataddr{$addr}; if($size > 0) { push @res, "At $addr, there is $size bytes.\n"; push @res, " allocated by $getmem{$addr}\n"; } } } if($openfile) { for(keys %filedes) { if($filedes{$_} == 1) { push @res, "Open file descriptor created at $getfile{$_}.\n"; } } } if($fopens) { push @res, "Open FILE handles left at:\n"; for(keys %fopen) { if($fopen{$_} == 1) { push @res, "fopen() called at $fopenfile{$_}.\n"; } } } if($addrinfos) { push @res, "IPv6-style name resolve data left at:\n"; for(keys %addrinfofile) { if($addrinfo{$_} == 1) { push @res, "getaddrinfo() called at $addrinfofile{$_}.\n"; } } } if($verbose) { push @res, "Mallocs: $mallocs\n", "Reallocs: $reallocs\n", "Callocs: $callocs\n", "Strdups: $strdups\n", "Wcsdups: $wcsdups\n", "Frees: $frees\n", "Sockets: $sockets\n", "Allocations: ".($mallocs + $callocs + $reallocs + $strdups + $wcsdups)."\n", "Operations: ".($mallocs + $callocs + $reallocs + $strdups + $wcsdups + $sockets)."\n", "Maximum allocated: $maxmem\n", "Total allocated: $memsum\n"; } return @res; } 1;