package Devel::PatchPerl::Plugin::Asan;
use base 'Devel::PatchPerl';

=head1 DESCRIPTION

Plugin for Devel::PatchPerl to fix several buffer overflows and use-after-free
bugs in production perls which prevent compilations with C<clang AddressSanitizer>,
aka I<asan>.

Note that F<buildperl.pl> from L<Devel::PPPerl> and L<Devel::PatchPerl> do
not provide such security patches, only configure and make patches.

Most fixes have very low security impact. No known exploits do exist.

You need to run C<perlall build --allpatches> or C<perlall build --patches=Asan>
to apply these.

=head1 PATCHES

The list is complete for non-threaded perls. 
For threaded perls some more patches need to be added.

    5.8.2-5.16.2: CVE-2013-1667 prevent hsplit DOS attacks
    5.10-5.15.9:  RT#111586 sdbm.c off-by-one access to global .dir
    5.12-5.16.0:  RT#72700 List::Util boot Fix off-by-two on string literal length
    5.15.4-9, 5.17.0-6: RT#115702 overlapping memcpy in to_utf8_case
    5.6-5.16.0:   RT#111594 Socket::unpack_sockaddr_un heap-buffer-overflow
    5.8-5.14.3:   RT#115992 PL_eval_start use-after-free
    5.10-5.14.3:  RT#115994 S_join_exact global-buffer-overflow
    5.17.7-8:     RT#82119 Socket::inet_ntop heap-buffer-overflow
    5.14.0-3:     RT#91678 S_anonymise_cv_maybe UTF8 cleanup
    5.17-19:      RT#118525 B::CV::GV crash on lexsubs

=head2 Devel::PatchPerl::Plugin::Asan::patchperl($class, {version,source,patchexe})

Apply patches in Devel::PatchPerl::Plugin::Asan depending on the
perl version. See L<Devel::PatchPerl::Plugin>.

Every patch is recorded in patchlevel.h, visible in myconfig.
If a patch fails the script dies.

=cut

sub patchperl {
  my $class = shift;
  my %args = @_;
  my ($vers, $source, $patch_exe) = @args{qw(version source patchexe)};
  for my $p ( grep { Devel::PatchPerl::_is( $_->{perl}, $vers ) } @Devel::PatchPerl::patch ) {
    for my $s (@{$p->{subs}}) {
      my ($sub, @args) = @$s;
      push @args, $vers unless scalar @args;
      $sub->(@args);
    }
  }
}


package
  Devel::PatchPerl;

use File::Copy;
use vars '@patch';

@patch = (
  {
    perl => [ qr/^5\.1[01]\.\d$/ ],
    # fixed in 5.16.0
    subs => [ [ \&_patch_sdbm] ],
  },
  {
    perl => [ qr/^5\.12\.[0-5]$/,
              qr/^5\.1[35]\.\d$/,
              qr/^5\.14\.[0-3]$/,
            ],
    subs => [ [ \&_patch_listutil_boot ], [ \&_patch_sdbm] ],
  },
  {
    perl => [ qr/^5\.16\.0$/ ],
    # fixed in 5.16.1
    subs => [ [ \&_patch_listutil_boot ] ],
  },
  {
    perl => [ qr/^5\.15\.[4-9]$/,
              qr/^5\.17\.[0-6]$/ ],
    # fixed in 5.17.6
    subs => [ [ \&_patch_to_utf8_case_memcpy ] ],
  },
  {
    perl => [ qr/^5\.[6-9].\d$/,
	      qr/^5\.1[0-5].\d$/,
              qr/^5\.16\.0$/ ],
    # fixed in 5.16.1
    subs => [ [ \&_patch_socket_un ] ],
  },
  {
    perl => [ qr/^5\.1[0123]\.\d$/,
              qr/^5\.15\.[012]$/,    # fixed in 5.15.3
	      qr/^5\.14\.[0123]$/ ], # to be fixed in 5.14.4
    subs => [ [ \&_patch_eval_start_510] ],
  },
  {
    perl => [ qr/^5\.8\.\d$/ ],
    subs => [ [ \&_patch_eval_start_58] ],
  },
  {
    perl => [ qr/^5\.1[0123]\.\d$/,  # broken since 5.10 (at least)
              qr/^5\.15\.0$/,        # fixed in 5.15.1
	      qr/^5\.14\.[0123]$/ ], # to be fixed in 5.14.4
    subs => [ [ \&_patch_join_exact] ],
  },
  {
    perl => [ qr/^5\.17\.[78]$/ ],
    # broken in 5.17.8, 2.006-2.007
    subs => [ [ \&_patch_socket_inet_ntop ] ],
  },
  {
    perl => [ qr/^5\.14\.[0123]$/,
              qr/^5\.15\.[012]$/ ],
    # regression in 5.14, fixed in v5.15.3-232-g1bac5ec
    subs => [ [ \&_patch_anonymise_cv_maybe ] ],
  },
  {
    perl => [ 
      qr/^5\.16\.[012]$/,  # to be fixed in 5.16.3
      qr/^5\.14\.[0123]$/, # to be fixed in 5.14.4
      qr/^5\.1[01235]\./,  # fixes not backported
      qr/^5\.8[23456789]\./,
      ],
    # d59e31fc729d8a39a774f03bc6bc457029a7aef2 CVE-2013-1667
    subs => [ [ \&_patch_hsplit_rehash ] ],
  },
  {
    perl => [
      qr/^5\.1[789]/,  # RT #118525
      ],
    subs => [ [ \&_patch_cvgv_lexsub ] ],
  },
);

sub _add_patchlevel {
  my $vers = shift;
  my $line = shift;
  my $success;
  File::Copy::cp("patchlevel.h", "patchlevel.h.orig");
  open my $in, "<", "patchlevel.h.orig" or return;
  open my $out, ">", "patchlevel.h" or return;
  $line =~ s/"/\"/g;
  my $qr = $] > 5.010 ? /^\s+PERL_GIT_UNPUSHED_COMMITS/
                      : /^\tNULL$/;
  while (my $s = <$in>) {
    print $out $s;
    if ($s =~ $qr) {
      $success++;
      print $out "\t,\"".$line."\"\n";
    }
  }
  close $in;
  close $out;
  print STDERR "patched: $line\n";
  return $success;
}

sub _patch_listutil_boot
{
  # RT#72700 Fix off-by-two on string literal length
  _patch(<<'END');
--- cpan/List-Util/ListUtil.xs.orig	2012-11-12 10:41:07.000000000 -0600
+++ cpan/List-Util/ListUtil.xs	2012-11-12 10:47:52.943198199 -0600
@@ -600,7 +600,7 @@
     varav = GvAVn(vargv);
 #endif
     if (SvTYPE(rmcgv) != SVt_PVGV)
-	gv_init(rmcgv, lu_stash, "List::Util", 12, TRUE);
+	gv_init(rmcgv, lu_stash, "List::Util", 10, TRUE);
     rmcsv = GvSVn(rmcgv);
 #ifndef SvWEAKREF
     av_push(varav, newSVpv("weaken",6));
END

  _add_patchlevel(@_, "RT#72700 List::Util boot Fix off-by-two on string literal length");
}

sub _patch_sdbm
{
  # acdbe25bd91bf897e0cf373b9
  # RT#111586 sdbm.c off-by-one access to global .dir
  _patch(<<'END');
--- ext/SDBM_File/sdbm/sdbm.c.orig	2012-11-12 10:53:26.000000000 -0600
+++ ext/SDBM_File/sdbm/sdbm.c		2012-11-12 10:56:02.790350262 -0600
@@ -78,8 +78,8 @@ sdbm_open(register char *file, register int flags, register int mode)
 	register char *dirname;
 	register char *pagname;
 	size_t filelen;
-	const size_t dirfext_len = sizeof(DIRFEXT "");
-	const size_t pagfext_len = sizeof(PAGFEXT "");
+	const size_t dirfext_size = sizeof(DIRFEXT "");
+	const size_t pagfext_size = sizeof(PAGFEXT "");
 
 	if (file == NULL || !*file)
 		return errno = EINVAL, (DBM *) NULL;
@@ -88,17 +88,17 @@ sdbm_open(register char *file, register int flags, register int mode)
  */
 	filelen = strlen(file);
 
-	if ((dirname = (char *) malloc(filelen + dirfext_len + 1
-				       + filelen + pagfext_len + 1)) == NULL)
+	if ((dirname = (char *) malloc(filelen + dirfext_size
+				       + filelen + pagfext_size)) == NULL)
 		return errno = ENOMEM, (DBM *) NULL;
 /*
  * build the file names
  */
 	memcpy(dirname, file, filelen);
-	memcpy(dirname + filelen, DIRFEXT, dirfext_len + 1);
-	pagname = dirname + filelen + dirfext_len + 1;
+	memcpy(dirname + filelen, DIRFEXT, dirfext_size);
+	pagname = dirname + filelen + dirfext_size;
 	memcpy(pagname, file, filelen);
-	memcpy(pagname + filelen, PAGFEXT, pagfext_len + 1);
+	memcpy(pagname + filelen, PAGFEXT, pagfext_size);
 
 	db = sdbm_prep(dirname, pagname, flags, mode);
 	free((char *) dirname);
END

  _add_patchlevel(@_, "RT#111586 sdbm.c off-by-one access to global .dir");
}

sub _patch_to_utf8_case_memcpy
{
  _patch(<<'END');
--- utf8.c~
+++ utf8.c
@@ -2366,7 +2366,9 @@ Perl_to_utf8_case(pTHX_ const U8 *p, U8* ustrp, STRLEN *lenp,
     /* Here, there was no mapping defined, which means that the code point maps
      * to itself.  Return the inputs */
     len = UTF8SKIP(p);
-    Copy(p, ustrp, len, U8);
+    if (p != ustrp) {   /* RT#115702 Don't copy onto itself */
+        Copy(p, ustrp, len, U8);
+    }
 
     if (lenp)
         *lenp = len;
END

  _add_patchlevel(@_, "RT#115702 overlapping memcpy in to_utf8_case");
}

sub _patch_socket_un
{
  my $vers = shift;
  my $patch = <<'END';
commit e5086424505dcbfc5e26aeb984b769ecf5ffed01
Author: David Mitchell <davem@iabyn.com>
Date:   Sun Feb 24 16:46:19 2013 +0000
   
On Linux sockaddrlen on sockets returned by accept, recvfrom,
getpeername and getsockname is not equal to sizeof(addr).
A (fairly harmless) read buffer overflow can occur when copying sockaddr
buffers on linux. Cherry-pick the fix from Socket 2.009 to keep ASAN happy.

--- ext/Socket/Socket.xs~
+++ ext/Socket/Socket.xs
@@ -565,10 +565,16 @@ unpack_sockaddr_un(sun_sv)
 			"Socket::unpack_sockaddr_un",
 			sockaddrlen, sizeof(addr));
 	}
+#   else
+	if (sockaddrlen < sizeof(addr)) { /* RT #111594 */
+           Copy(sun_ad, &addr, sockaddrlen, char);
+           Zero(((char*)&addr) + sockaddrlen, sizeof(addr) - sockaddrlen, char);
+       }
+       else {
+           Copy(sun_ad, &addr, sizeof(addr), char);
+       }
 #   endif
 
-	Copy( sun_ad, &addr, sizeof addr, char );
-
 	if ( addr.sun_family != AF_UNIX ) {
 	    croak("Bad address family for %s, got %d, should be %d",
 			"Socket::unpack_sockaddr_un",
END

  #; )
  if ($vers =~ /^5\.6\./) {
    $patch =~ s/@@ -565,10 +565,16 @@/@@ -1016,10 +1016,16 @@/;
  }
  if ($vers =~ /^5\.[89]\./ or $vers =~ /^5\.1[0-2]\./) {
    $patch =~ s/@@ -565,10 +565,16 @@/@@ -363,10 +363,16 @@/;
  }
  if ($vers =~ /^5\.16\./ or $vers =~ /^5\.15\.[5-9]\./) {
    $patch =~ s|ext/Socket/Socket.xs|cpan/Socket/Socket.xs|g;
  }
  _patch($patch);

  _add_patchlevel($vers, "RT#111594 Socket::unpack_sockaddr_un heap-buffer-overflow");
}

sub _patch_eval_start_510
{
  _patch(<<'END');
--- pp_ctl.c~
+++ pp_ctl.c
@@ -3088,6 +3088,7 @@ Perl_sv_compile_2op_is_broken(pTHX_ SV *sv, OP **startop, const char *code,
     CV* runcv = NULL;	/* initialise to avoid compiler warnings */
     STRLEN len;
     bool need_catch;
+    OP* ret;
 
     PERL_ARGS_ASSERT_SV_COMPILE_2OP_IS_BROKEN;
 
@@ -3182,7 +3183,9 @@ Perl_sv_compile_2op_is_broken(pTHX_ SV *sv, OP **startop, const char *code,
     PERL_UNUSED_VAR(newsp);
     PERL_UNUSED_VAR(optype);
 
-    return PL_eval_start;
+    ret = PL_eval_start;
+    PL_eval_start = NULL;
+    return ret;
 }
 
 
@@ -3903,8 +3906,10 @@ PP(pp_require)
     encoding = PL_encoding;
     PL_encoding = NULL;
 
-    if (doeval(gimme, NULL, NULL, PL_curcop->cop_seq))
+    if (doeval(gimme, NULL, NULL, PL_curcop->cop_seq)) {
 	op = DOCATCH(PL_eval_start);
+	PL_eval_start = NULL;
+    }
     else
 	op = PL_op->op_next;
 
@@ -4029,6 +4034,7 @@ PP(pp_entereval)
     PUTBACK;
 
     if (doeval(gimme, NULL, runcv, seq)) {
+	OP *ret;
 	if (was != PL_breakable_sub_gen /* Some subs defined here. */
 	    ? (PERLDB_LINE || PERLDB_SAVESRC)
 	    :  PERLDB_SAVESRC_NOSUBS) {
@@ -4037,7 +4043,9 @@ PP(pp_entereval)
 	    char *const safestr = savepvn(tmpbuf, len);
 	    SAVEDELETE(PL_defstash, safestr, len);
 	}
-	return DOCATCH(PL_eval_start);
+	ret = DOCATCH(PL_eval_start);
+	PL_eval_start = NULL;
+	return ret;
     } else {
 	/* We have already left the scope set up earlier thanks to the LEAVE
 	   in doeval().  */
END

  _add_patchlevel(@_, "RT#115992 PL_eval_start use-after-free");
}

sub _patch_eval_start_58
{
  _patch(<<'END');
diff -bu ./pp_ctl.c~ ./pp_ctl.c
--- ./pp_ctl.c~	2013-03-04 18:45:25.823223519 -0600
+++ ./pp_ctl.c	2013-03-04 18:52:26.691549451 -0600
@@ -2839,7 +2839,7 @@ STATIC OP *
 S_doeval(pTHX_ int gimme, OP** startop, CV* outside, U32 seq)
 {
     dSP;
-    OP * const saveop = PL_op;
+    OP * saveop = PL_op;
 
     PL_in_eval = ((saveop && saveop->op_type == OP_REQUIRE)
 		  ? (EVAL_INREQUIRE | (PL_in_eval & EVAL_INEVAL))
@@ -2985,7 +2985,9 @@ S_doeval(pTHX_ int gimme, OP** startop, CV* outside, U32 seq)
     MUTEX_UNLOCK(&PL_eval_mutex);
 #endif /* USE_5005THREADS */
 
-    RETURNOP(PL_eval_start);
+    saveop = PL_eval_start;
+    PL_eval_start = NULL;
+    RETURNOP(saveop);
 }
 
 STATIC PerlIO *
@@ -3426,7 +3428,12 @@ PP(pp_require)
     encoding = PL_encoding;
     PL_encoding = Nullsv;
 
-    op = DOCATCH(doeval(gimme, NULL, Nullcv, PL_curcop->cop_seq));
+   if (doeval(gimme, NULL, Nullcv, PL_curcop->cop_seq)) {
+ 	op = DOCATCH(PL_eval_start);
+	PL_eval_start = NULL;
+   }
+   else
+       op = PL_op->op_next;
 
     /* Restore encoding. */
     PL_encoding = encoding;
END

  _add_patchlevel(@_, "RT#115992 PL_eval_start use-after-free");
}

sub _patch_join_exact
{
# commit bb789b09de07edfb74477eb1603949c96d60927d
# Author:     David Mitchell <davem@iabyn.com>
# AuthorDate: Tue Jul 5 11:35:08 2011 +0100
# 
#     fix segv in regcomp.c:S_join_exact()
#     
#     This function joins multiple EXACT* nodes into a single node.
#     At the end, under DEBUGGING, it marks the optimised-out nodes as being
#     type OPTIMIZED. However, some of the 'nodes' aren't actually nodes;
#     they're random bits of string at the tail of those nodes. So you
#     can't peek that the 'node's OP field to decide what type it was.
#     
#     Instead, just unconditionally overwrite all the slots with fake
#     OPTIMIZED nodes.
  _patch(<<'END');
--- regcomp.c~
+++ regcomp.c
@@ -2647,13 +2647,13 @@ S_join_exact(pTHX_ RExC_state_t *pRExC_state, regnode *scan, I32 *min, U32 flags
     }
     
 #ifdef DEBUGGING
-    /* Allow dumping */
+    /* Allow dumping but overwriting the collection of skipped
+     * ops and/or strings with fake optimized ops */
     n = scan + NODE_SZ_STR(scan);
     while (n <= stop) {
-        if (PL_regkind[OP(n)] != NOTHING || OP(n) == NOTHING) {
-            OP(n) = OPTIMIZED;
-            NEXT_OFF(n) = 0;
-        }
+	OP(n) = OPTIMIZED;
+	FLAGS(n) = 0;
+	NEXT_OFF(n) = 0;
         n++;
     }
 #endif
END

  _add_patchlevel(@_, "RT#115994 S_join_exact global-buffer-overflow");
}

sub _patch_socket_inet_ntop
{
  my $vers = shift;
  my $patch = <<'END';
--- cpan/Socket/Socket.xs~
+++ cpan/Socket/Socket.xs
@@ -934,8 +934,13 @@ inet_ntop(af, ip_address_sv)
 #endif
 		      "Socket::inet_ntop", af);
 	}
-
-	Copy(ip_address, &addr, sizeof addr, char);
+	if (addrlen < sizeof(addr)) {
+	   Copy(ip_address, &addr, addrlen, char);
+           Zero(((char*)&addr)+addrlen, sizeof(addr) - addrlen, char);
+	}
+	else {
+	  Copy(ip_address, &addr, sizeof addr, char);
+	}
 	inet_ntop(af, &addr, str, sizeof str);
 
 	ST(0) = sv_2mortal(newSVpvn(str, strlen(str)));
END

  #; )
  _patch($patch);
  _add_patchlevel($vers, "RT#82119 Socket::inet_ntop heap-buffer-overflow");
}

sub _patch_anonymise_cv_maybe
{
  my $vers = shift;
  my $patch = <<'END';
commit 1bac5ecc108e6bb05752e5aef66c6890163aff39
Author: Brian Fraser <fraserbn@gmail.com>
Date:   Mon Sep 26 13:48:52 2011 -0700

    sv.c: S_anonymise_cv_maybe UTF8 cleanup.

diff --git a/sv.c b/sv.c
index d71f901..a3a2c74 100644
--- sv.c.orig
+++ sv.c
@@ -5893,7 +5893,6 @@ Perl_sv_replace(pTHX_ register SV *const sv, register SV *const nsv)
 STATIC void
 S_anonymise_cv_maybe(pTHX_ GV *gv, CV* cv)
 {
-    char *stash;
     SV *gvname;
     GV *anongv;
 
@@ -5913,10 +5912,10 @@ S_anonymise_cv_maybe(pTHX_ GV *gv, CV* cv)
     }
 
     /* if not, anonymise: */
-    stash  = GvSTASH(gv) && HvNAME(GvSTASH(gv))
-              ? HvENAME(GvSTASH(gv)) : NULL;
-    gvname = Perl_newSVpvf(aTHX_ "%s::__ANON__",
-					stash ? stash : "__ANON__");
+    gvname = (GvSTASH(gv) && HvNAME(GvSTASH(gv)) && HvENAME(GvSTASH(gv)))
+                    ? newSVhek(HvENAME_HEK(GvSTASH(gv)))
+                    : newSVpvn_flags( "__ANON__", 8, 0 );
+    sv_catpvs(gvname, "::__ANON__");
     anongv = gv_fetchsv(gvname, GV_ADDMULTI, SVt_PVCV);
     SvREFCNT_dec(gvname);
 
END

  #; )
  _patch($patch);
  _add_patchlevel($vers, "RT#91678 S_anonymise_cv_maybe UTF8 cleanup");
}

sub _patch_cvgv_lexsub
{
  my $vers = shift;
  my $patch = <<'END';
From d9601932ab176efaf28dd3d4bfe59b19dcc55106 Mon Sep 17 00:00:00 2001
From: Reini Urban <rurban@x-ray.at>
Date: Thu, 11 Jul 2013 12:09:15 -0500
Subject: [PATCH] [perl #118525] Do not access a GV of a lexical sub in
 B::CV::GV

A lexsub has a hek instead of a gv, return a fresh NULL instead.
Avoid accessing SvFLAGS of the HEK.
---
 ext/B/B.xs |    4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff ext/B/B.xs~ ext/B/B.xs
index fbe6be6..a6b9ee1 100644
--- ext/B/B.xs~
+++ ext/B/B.xs
@@ -1390,7 +1390,9 @@ IVX(sv)
 	ptr = (ix & 0xFFFF) + (char *)SvANY(sv);
 	switch ((U8)(ix >> 16)) {
 	case (U8)(sv_SVp >> 16):
-	    ret = make_sv_object(aTHX_ *((SV **)ptr));
+	    ret = (ix == PVCV_gv_ix && SvPADMY(sv)) /* lexsub has a HEK instead of GV */
+	        ? sv_setref_iv(sv_newmortal(), "B::NULL", PTR2IV(sv))
+	        : make_sv_object(aTHX_ *((SV **)ptr));
 	    break;
 	case (U8)(sv_IVp >> 16):
 	    ret = sv_2mortal(newSViv(*((IV *)ptr)));
-- 
END

  #; )
  _patch($patch);
  _add_patchlevel($vers, "RT#118525 B::CV::GV crash on lexsubs");
}

sub patch_hsplit_rehash
{
  my $vers = shift;
  my $patch = <<'END';
commit d59e31fc729d8a39a774f03bc6bc457029a7aef2
Author: Yves Orton <demerphq@gmail.com>
Date:   Tue Feb 12 10:53:05 2013 +0100

    Prevent premature hsplit() calls, and only trigger REHASH after hsplit()
    
    Triggering a hsplit due to long chain length allows an attacker
    to create a carefully chosen set of keys which can cause the hash
    to use 2 * (2**32) * sizeof(void *) bytes ram. AKA a DOS via memory
    exhaustion. Doing so also takes non trivial time.
    
    Eliminating this check, and only inspecting chain length after a
    normal hsplit() (triggered when keys>buckets) prevents the attack
    entirely, and makes such attacks relatively benign.
    
    (cherry picked from commit f1220d61455253b170e81427c9d0357831ca0fac)

diff --git a/ext/Hash-Util-FieldHash/t/10_hash.t b/ext/Hash-Util-FieldHash/t/10_hash.t
index 2cfb4e8..d58f053 100644
--- ext/Hash-Util-FieldHash/t/10_hash.t~
+++ ext/Hash-Util-FieldHash/t/10_hash.t
@@ -38,15 +38,29 @@ use constant START     => "a";
 
 # some initial hash data
 fieldhash my %h2;
-%h2 = map {$_ => 1} 'a'..'cc';
+my $counter= "a";
+$h2{$counter++}++ while $counter ne 'cd';
 
 ok (!Internals::HvREHASH(%h2), 
     "starting with pre-populated non-pathological hash (rehash flag if off)");
 
 my @keys = get_keys(\%h2);
+my $buckets= buckets(\%h2);
 $h2{$_}++ for @keys;
+$h2{$counter++}++ while buckets(\%h2) == $buckets; # force a split
 ok (Internals::HvREHASH(%h2), 
-    scalar(@keys) . " colliding into the same bucket keys are triggering rehash");
+    scalar(@keys) . " colliding into the same bucket keys are triggering rehash after split");
+
+# returns the number of buckets in a hash
+sub buckets {
+    my $hr = shift;
+    my $keys_buckets= scalar(%$hr);
+    if ($keys_buckets=~m!/([0-9]+)\z!) {
+        return 0+$1;
+    } else {
+        return 8;
+    }
+}
 
 sub get_keys {
     my $hr = shift;
diff --git a/hv.c b/hv.c
index 2be1feb..abb9d76 100644
--- hv.c~
+++ hv.c
@@ -35,7 +35,8 @@ holds the key and hash value.
 #define PERL_HASH_INTERNAL_ACCESS
 #include "perl.h"
 
-#define HV_MAX_LENGTH_BEFORE_SPLIT 14
+#define HV_MAX_LENGTH_BEFORE_REHASH 14
+#define SHOULD_DO_HSPLIT(xhv) ((xhv)->xhv_keys > (xhv)->xhv_max) /* HvTOTALKEYS(hv) > HvMAX(hv) */
 
 static const char S_strtab_error[]
     = "Cannot modify shared string table in hv_%s";
@@ -794,29 +795,9 @@ Perl_hv_common(pTHX_ HV *hv, SV *keysv, const char *key, STRLEN klen,
     if (masked_flags & HVhek_ENABLEHVKFLAGS)
 	HvHASKFLAGS_on(hv);
 
-    {
-	const HE *counter = HeNEXT(entry);
-
-	xhv->xhv_keys++; /* HvTOTALKEYS(hv)++ */
-	if (!counter) {				/* initial entry? */
-	} else if (xhv->xhv_keys > xhv->xhv_max) {
-		/* Use only the old HvKEYS(hv) > HvMAX(hv) condition to limit
-		   bucket splits on a rehashed hash, as we're not going to
-		   split it again, and if someone is lucky (evil) enough to
-		   get all the keys in one list they could exhaust our memory
-		   as we repeatedly double the number of buckets on every
-		   entry. Linear search feels a less worse thing to do.  */
-	    hsplit(hv);
-	} else if(!HvREHASH(hv)) {
-	    U32 n_links = 1;
-
-	    while ((counter = HeNEXT(counter)))
-		n_links++;
-
-	    if (n_links > HV_MAX_LENGTH_BEFORE_SPLIT) {
-		hsplit(hv);
-	    }
-	}
+    xhv->xhv_keys++; /* HvTOTALKEYS(hv)++ */
+    if ( SHOULD_DO_HSPLIT(xhv) ) {
+        hsplit(hv);
     }
 
     if (return_svp) {
@@ -1192,7 +1173,7 @@ S_hsplit(pTHX_ HV *hv)
 
 
     /* Pick your policy for "hashing isn't working" here:  */
-    if (longest_chain <= HV_MAX_LENGTH_BEFORE_SPLIT /* split worked?  */
+    if (longest_chain <= HV_MAX_LENGTH_BEFORE_REHASH /* split worked?  */
 	|| HvREHASH(hv)) {
 	return;
     }
@@ -2831,8 +2812,8 @@ S_share_hek_flags(pTHX_ const char *str, I32 len, register U32 hash, int flags)
 
 	xhv->xhv_keys++; /* HvTOTALKEYS(hv)++ */
 	if (!next) {			/* initial entry? */
-	} else if (xhv->xhv_keys > xhv->xhv_max /* HvKEYS(hv) > HvMAX(hv) */) {
-		hsplit(PL_strtab);
+	} else if ( SHOULD_DO_HSPLIT(xhv) ) {
+            hsplit(PL_strtab);
 	}
     }
 
diff --git a/t/op/hash.t b/t/op/hash.t
index 278bea7..201260a 100644
--- t/op/hash.t~
+++ t/op/hash.t
@@ -39,22 +39,36 @@ use constant THRESHOLD => 14;
 use constant START     => "a";
 
 # some initial hash data
-my %h2 = map {$_ => 1} 'a'..'cc';
+my %h2;
+my $counter= "a";
+$h2{$counter++}++ while $counter ne 'cd';
 
 ok (!Internals::HvREHASH(%h2), 
     "starting with pre-populated non-pathological hash (rehash flag if off)");
 
 my @keys = get_keys(\%h2);
+my $buckets= buckets(\%h2);
 $h2{$_}++ for @keys;
+$h2{$counter++}++ while buckets(\%h2) == $buckets; # force a split
 ok (Internals::HvREHASH(%h2), 
-    scalar(@keys) . " colliding into the same bucket keys are triggering rehash");
+    scalar(@keys) . " colliding into the same bucket keys are triggering rehash after split");
+
+# returns the number of buckets in a hash
+sub buckets {
+    my $hr = shift;
+    my $keys_buckets= scalar(%$hr);
+    if ($keys_buckets=~m!/([0-9]+)\z!) {
+        return 0+$1;
+    } else {
+        return 8;
+    }
+}
 
 sub get_keys {
     my $hr = shift;
 
     # the minimum of bits required to mount the attack on a hash
     my $min_bits = log(THRESHOLD)/log(2);
-
     # if the hash has already been populated with a significant amount
     # of entries the number of mask bits can be higher
     my $keys = scalar keys %$hr;
END

  # "
  if ($vers =~ /^5\.8\.[2345678]$/) {
    $patch =~ s{diff --git a/ext/Hash-Util-FieldHash.+diff --git a/hv.c b/hv.c}
               {diff --git a/hv.c b/hv.c}gm;
  }
  _patch($patch);
  _add_patchlevel($vers, "CVE-2013-1667 hsplit rehash");
}

1;
