develooper Front page | perl.perl5.porters | Postings from August 2011

[perl #97860] IO::Socket::INET does not appear to be thread safe

From:
Wayne Kohler
Date:
August 27, 2011 18:47
Subject:
[perl #97860] IO::Socket::INET does not appear to be thread safe
Message ID:
rt-3.6.HEAD-31297-1314391224-1238.97860-75-0@perl.org
# New Ticket Created by  Wayne Kohler 
# Please include the string:  [perl #97860]
# in the subject line of all future correspondence about this issue. 
# <URL: https://rt.perl.org:443/rt3/Ticket/Display.html?id=97860 >



This is a bug report for perl from wayne.kohler@gmail.com,
generated with the help of perlbug 1.39 running under perl 5.14.0.


-----------------------------------------------------------------

I have an piece of code where I need to open a socket connection to an xinetd based service
running on several remote hosts.  Each socket connection is opened in a separate thread.  

The gist of the code that creates the threads is basically as follows:

foreach my $host (@{$self->{hosts}}) {
    $threads{$host} = threads->new(\&get_data, $host);
    # etc
}

The thread entry function is as follows:

sub get_data {
  my $host = shift;
  $SIG{'KILL'} = sub { threads->exit(); };
  my $remote = IO::Socket::INET->new(
    Proto => "tcp",
    PeerAddr => "$host",
    PeerPort => "10515",
  ) or die "cannot connect to sysmond port at $host";
  my @results = <$remote>;
  shutdown($remote,2);
  eval join("", @results);
}

The problem I was seeing is that the code intermittently was not returning results
for random hosts.  So, I added some debug statements, and ran the application in a
loop until I saw a failure:

sub get_data {
  my $host = shift;
  $SIG{'KILL'} = sub { threads->exit(); };
  my $remote = IO::Socket::INET->new(
    Proto => "tcp",
    PeerAddr => "$host",
    PeerPort => "10515",
  ) or die "cannot connect to sysmond port at $host";
  my @results = <$remote>;
  if ($DEBUG) {
    my $sockhost = $remote->sockhost();
    my $sockport = $remote->sockport();
    my $peerport = $remote->peerport();
    my $peerhost = $remote->peerhost();
    open(FILE,">/tmp/results_$host");
    print FILE "#### Socket handle = $remote\n";
    print FILE "#### Socket host = $sockhost\n";
    print FILE "#### Socket port = $sockport\n";
    print FILE "#### Peer port = $peerport\n";
    print FILE "#### Peer host = $peerhost\n";
    print FILE "#### host = $host\n";
    print FILE join("", @results);
    close(FILE);
  }
  shutdown($remote,2);
  eval join("", @results);
}

When I examined the header of the debug file for the failing host, I saw the following:

#### Socket handle = IO::Socket::INET=GLOB(0x6e29b20)
#### Socket host = 192.168.170.1
#### Socket port = 36888
#### Peer port = 10515
#### Peer host = 192.168.170.180
#### host = dist0 

I noticed that the Peer host IP address was incorrect, it was the address of a different
host.  When I examined the header of the debug file for that host, I saw the following:

#### Socket handle = IO::Socket::INET=GLOB(0x6b03190)
#### Socket host = 192.168.170.1
#### Socket port = 36887
#### Peer port = 10515
#### Peer host = 192.168.170.180
#### host = ds0 

So, even though I called IO::Socket::INET in two separate threads with different host
names, it returned the SAME IP address for the two different hosts.  

I started digging into the INET.pm code, and found that it is using the following code
to perform the name to IP translation:

sub _get_addr {
    my($sock,$addr_str, $multi) = @_;
    my @addr;
    if ($multi && $addr_str !~ /^\d+(?:\.\d+){3}$/) {
        (undef, undef, undef, undef, @addr) = gethostbyname($addr_str);
    } else {
        my $h = inet_aton($addr_str);
        push(@addr, $h) if defined $h;
    }
    @addr;
}

Since $multi is not set, the else clause gets exectued.  So, looking deeper, I found
the following XS template for inet_aton in Socket.xs:

void
inet_aton(host)
        char *  host
        CODE:
        {
        struct in_addr ip_address;
        struct hostent * phe;

        if ((*host != '\0') && inet_aton(host, &ip_address)) {
                ST(0) = newSVpvn_flags((char *)&ip_address, sizeof ip_address, SVs_TEMP);
                XSRETURN(1);
        }

        phe = gethostbyname(host);
        if (phe && phe->h_addrtype == AF_INET && phe->h_length == 4) {
                ST(0) = newSVpvn_flags((char *)phe->h_addr, phe->h_length, SVs_TEMP);
                XSRETURN(1);
        }

        XSRETURN_UNDEF;
        }

This is using the standard gethostbyname function, which is apparantly NOT thread safe based
on some quick Google searches:

(Excerpted from: http://compute.cnr.berkeley.edu/cgi-bin/man-cgi?gethostbyname+3)

Reentrant Interfaces
     The gethostbyname(), gethostbyaddr(), and gethostent() func-
     tions use static storage that is reused in each call, making
     these functions unsafe for  use  in  multithreaded  applica-
     tions.

(Excerpted from: http://pubs.opengroup.org/onlinepubs/009695399/functions/gethostbyname.html)

The gethostbyaddr() function need not be reentrant. A function that is not required to be reentrant is not required to be thread-safe.

(Excerpted from: https://bugzilla.redhat.com/show_bug.cgi?id=485682)

This turns out to be much worse than it sounds:
gethostbyname() isn't a thread safe API and could cause (and may have been
observed to cause) errors when multiple threads do gethostbyname() at the same
time.

NOTE:

An interesting point to note is that I am running on virtual machines, hosted on 
HP Blade servers with a SAN datastore.  When I run this test on a system comprised
of separate physical hosts, I do not see the failure.  I can make the symptom go
away on the virtual system by adding a 1 second sleep after the creation of each
thread.  Perhaps on the physical system, there is enough of an iherenet delay for
whatever reason between the creation of each thread such that the problem does not
manifest itself.

The workaround I'm going to pursue is to use the IP address instead of the host
name as the PeerAddr argument to the IO::Socket::INET constructor.

[Please do not change anything below this line]
-----------------------------------------------------------------
---
Flags:
    category=library
    severity=high
---
Site configuration information for perl 5.14.0:

Configured by e3 at Tue Aug  9 15:32:56 UTC 2011.

Summary of my perl5 (revision 5 version 14 subversion 0) configuration:
   
  Platform:
    osname=linux, osvers=2.6.18-238.12.1.el5, archname=x86_64-linux-thread-multi
    uname='linux rhel5-740-slave01.vm.skarven.net 2.6.18-238.12.1.el5 #1 smp sat may 7 20:18:50 edt 2011 x86_64 x86_64 x86_64 gnulinux '
    config_args='-des -Dprefix=/opt/skarven/perl5.14.0 -Dusethreads=define -Duseithreads=define -Dusemultiplicity=define'
    hint=recommended, useposix=true, d_sigaction=define
    useithreads=define, usemultiplicity=define
    useperlio=define, d_sfio=undef, uselargefiles=define, usesocks=undef
    use64bitint=define, use64bitall=define, uselongdouble=undef
    usemymalloc=n, bincompat5005=undef
  Compiler:
    cc='cc', ccflags ='-D_REENTRANT -D_GNU_SOURCE -fno-strict-aliasing -pipe -fstack-protector -I/usr/local/include -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64',
    optimize='-O2',
    cppflags='-D_REENTRANT -D_GNU_SOURCE -fno-strict-aliasing -pipe -fstack-protector -I/usr/local/include'
    ccversion='', gccversion='4.1.2 20080704 (Red Hat 4.1.2-51)', gccosandvers=''
    intsize=4, longsize=8, ptrsize=8, doublesize=8, byteorder=12345678
    d_longlong=define, longlongsize=8, d_longdbl=define, longdblsize=16
    ivtype='long', ivsize=8, nvtype='double', nvsize=8, Off_t='off_t', lseeksize=8
    alignbytes=8, prototype=define
  Linker and Libraries:
    ld='cc', ldflags =' -fstack-protector -L/usr/local/lib'
    libpth=/usr/local/lib /lib /usr/lib /lib64 /usr/lib64 /usr/local/lib64
    libs=-lnsl -lgdbm -ldb -ldl -lm -lcrypt -lutil -lpthread -lc
    perllibs=-lnsl -ldl -lm -lcrypt -lutil -lpthread -lc
    libc=/lib/libc-2.5.so, so=so, useshrplib=false, libperl=libperl.a
    gnulibc_version='2.5'
  Dynamic Linking:
    dlsrc=dl_dlopen.xs, dlext=so, d_dlsymun=undef, ccdlflags='-Wl,-E'
    cccdlflags='-fPIC', lddlflags='-shared -O2 -L/usr/local/lib -fstack-protector'

Locally applied patches:
    

---
@INC for perl 5.14.0:
    /opt/skarven/perl5.14.0/lib/site_perl/5.14.0/x86_64-linux-thread-multi
    /opt/skarven/perl5.14.0/lib/site_perl/5.14.0
    /opt/skarven/perl5.14.0/lib/5.14.0/x86_64-linux-thread-multi
    /opt/skarven/perl5.14.0/lib/5.14.0
    .

---
Environment for perl 5.14.0:
    HOME=/home/e3
    LANG=en_US.UTF-8
    LANGUAGE (unset)
    LC_ALL=POSIX
    LD_LIBRARY_PATH (unset)
    LOGDIR (unset)
    PATH=/opt/skarven/perl5.14.0/bin:/usr/pgsql-9.0/bin:/opt/skarven/sbin:/home/e3/sbin:/home/e3/bin:/opt/skarven/bin:/usr/kerberos/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/home/e3/bin:/home/e3/sbin:/home/e3/bin:/home/e3/sbin
    PERL_BADLANG (unset)
    SHELL=/bin/bash




nntp.perl.org: Perl Programming lists via nntp and http.
Comments to Ask Bjørn Hansen at ask@perl.org | Group listing | About