Skip Menu |

This queue is for tickets about the Mail-DKIM CPAN distribution.

Report information
The Basics
Id: 77902
Status: resolved
Priority: 0/
Queue: Mail-DKIM

People
Owner: jason [...] long.name
Requestors: david.verdin [...] renater.fr
Cc:
AdminCc: Mark.Martinec [...] ijs.si

Bug Information
Severity: Important
Broken in: 0.39
Fixed in: (no value)



Subject: DKIM signature computation very long on large messages
I recently had Sympa choking on large messages while trying to compute a DKIM signature for large messages (with an attachment larger than 7 MB). Here is the environment: Perl 5.8.8 OS RHEL 5 The bug is due to the PRINT function in MessageParser.pm iterating over a linearilly growing string buffer. Hence, the larger the message is, the longer (exponentially) the signing process takes. I found that applying the following patch to MessageParser.pm brings the signing process back to a linear dimension, which, for a 7 MB attachment, means signing in one-two seconds instead of 40 minutes. I am under the impression that these changes won't break anything in the constitution of the signature, but I may have missed something important in the module. Please, let me know if the patch is wrong. Regards, David Here is the patch: 50,51c50 < my $buf = $self->{buf}; < $buf .= @_ == 1 ? $_[0] : join("", @_) if @_; --- Show quoted text
> my $buf = @_ == 1 ? $_[0] : join("", @_) if @_;
81c80 < $self->{buf} = $buf; --- Show quoted text
> $self->{buf} .= $buf;
From: Mark.Martinec [...] ijs.si
Show quoted text
> The bug is due to the PRINT function in MessageParser.pm iterating > over a linearilly growing string buffer. Hence, the larger the > message is, the longer (exponentially) the signing process takes.
This is not the case in a normal situation, the stored buffer should not be growing. Each call to PRINT should process whatever it can in the buffer and leave there only the last unterminated line - a leftover to be processed with the next call. Your situation can only arise if you are feeding chunks of a huge line, piece by piece. Seems the problem here is that the application forgot to convert newlines to CRLF pairs, so that a whole message looks to Mail::DKIM as one huge line, and calls to PRINT could not process anything (no full line so far) until a CLOSE, so could only keep collecting the text. Show quoted text
> Here is the patch: > > 50,51c50 > < my $buf = $self->{buf}; > < $buf .= @_ == 1 ? $_[0] : join("", @_) if @_; > ---
> > my $buf = @_ == 1 ? $_[0] : join("", @_) if @_;
> 81c80 > < $self->{buf} = $buf; > ---
> > $self->{buf} .= $buf;
This is also wrong: whatever leftover remains in $self->{buf} should stay there for the next call, which should concatenate the leftover with newly supplied arguments. Nevertheless I do agree that the PRINT subroutine performs some unnecessary copying. Avoiding this could save a little bit of resources. I did some benchmarking and I'll attach a replacement MessageParser.pm, which accesses the buffer indirectly through a ref, avoiding copying or accessing it through a hash. If an application passes a message in large chunks to PRINT the speedup is very small, while an application that does so inefficiently by calling PRINT line-by-line can see some improvement in speed.
Subject: MessageParser.pm
#!/usr/bin/perl # Copyright 2005 Messiah College. All rights reserved. # Jason Long <jlong@messiah.edu> # Copyright (c) 2004 Anthony D. Urso. All rights reserved. # This program is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. use strict; use warnings; package Mail::DKIM::MessageParser; use Carp; sub new_object { my $class = shift; return $class->TIEHANDLE(@_); } sub new_handle { my $class = shift; local *TMP; tie *TMP, $class, @_; return *TMP; } sub TIEHANDLE { my $class = shift; my %args = @_; my $self = bless \%args, $class; $self->init; return $self; } sub init { my $self = shift; my $buf = ""; $self->{buf_ref} = \$buf; $self->{in_header} = 1; } sub PRINT { my $self = shift; my $buf_ref = $self->{buf_ref}; $$buf_ref .= @_ == 1 ? $_[0] : join("", @_) if @_; if ($self->{in_header}) { local $1; # avoid polluting a global $1 while ($$buf_ref ne '') { if (substr($$buf_ref,0,2) eq "\015\012") { substr($$buf_ref, 0, 2) = ''; $self->finish_header(); $self->{in_header} = 0; last; } if ($$buf_ref !~ /^(.+?\015\012)[^\ \t]/s) { last; } my $header = $1; $self->add_header($header); substr($$buf_ref, 0, length($header)) = ''; } } if (!$self->{in_header}) { my $j = rindex($$buf_ref,"\015\012"); if ($j >= 0) { $self->add_body(substr($$buf_ref, 0, $j+2)); substr($$buf_ref, 0, $j+2) = ''; } } return 1; } sub CLOSE { my $self = shift; my $buf_ref = $self->{buf_ref}; if ($self->{in_header}) { if ($$buf_ref ne '') { # A line of header text ending CRLF would not have been # processed yet since before we couldn't tell if it was # the complete header. Now that we're in CLOSE, we can # finish the header... $$buf_ref =~ s/\015\012\z//s; $self->add_header("$$buf_ref\015\012"); } $self->finish_header; $self->{in_header} = 0; } else { if ($$buf_ref ne '') { $self->add_body($$buf_ref); } } $$buf_ref = ""; $self->finish_body; return 1; } sub add_header { die "add_header not implemented"; } sub finish_header { die "finish_header not implemented"; } sub add_body { die "add_body not implemented"; } sub finish_body { # do nothing by default } sub reset { carp "reset not implemented"; } 1;
From: Mark.Martinec [...] ijs.si
While at it... Here is a refinement of my previous attachment, this time in a form of a patch. In addition to what the previous code did, this one also avoids one more copying of a buffer, shaving off another millisecond or two (out of 50 ms) on larger mail (3 MB) with larger buffers (32 kB or 64 kB).
Subject: Mail-DKIM-PR77902.patch
--- lib/Mail/DKIM/MessageParser.pm~ 2008-08-26 00:10:56.000000000 +0200 +++ lib/Mail/DKIM/MessageParser.pm 2012-11-07 19:30:01.127433685 +0100 @@ -41,6 +41,7 @@ my $self = shift; + my $buf = ''; + $self->{buf_ref} = \$buf; $self->{in_header} = 1; - $self->{buf} = ""; } @@ -48,18 +49,19 @@ { my $self = shift; - my $buf = $self->{buf}; - $buf .= @_ == 1 ? $_[0] : join("", @_) if @_; + my $buf_ref = $self->{buf_ref}; + $$buf_ref .= @_ == 1 ? $_[0] : join("", @_) if @_; if ($self->{in_header}) { - while (length $buf) + local $1; # avoid polluting a global $1 + while ($$buf_ref ne '') { - if (substr($buf,0,2) eq "\015\012") + if (substr($$buf_ref,0,2) eq "\015\012") { - $buf = substr($buf, 2); + substr($$buf_ref, 0, 2) = ''; $self->finish_header(); $self->{in_header} = 0; last; } - if ($buf !~ /^(.+?\015\012)[^\ \t]/s) + if ($$buf_ref !~ /^(.+?\015\012)[^\ \t]/s) { last; @@ -67,17 +69,21 @@ my $header = $1; $self->add_header($header); - $buf = substr($buf, length($header)); + substr($$buf_ref, 0, length($header)) = ''; } } if (!$self->{in_header}) { - my $j = rindex($buf,"\015\012"); + my $j = rindex($$buf_ref,"\015\012"); if ($j >= 0) { - $self->add_body(substr($buf, 0, $j+2)); - substr($buf, 0, $j+2) = ''; + # avoid copying a large buffer: the unterminated + # last line is typically short compared to the rest + + my $carry = substr($$buf_ref, $j+2); + substr($$buf_ref, $j+2) = ''; # shrink to last CRLF + $self->add_body($$buf_ref); # must end on CRLF + $$buf_ref = $carry; # restore unterminated last line } } - $self->{buf} = $buf; return 1; } @@ -86,9 +92,9 @@ { my $self = shift; - my $buf = $self->{buf}; + my $buf_ref = $self->{buf_ref}; if ($self->{in_header}) { - if (length $buf) + if ($$buf_ref ne '') { # A line of header text ending CRLF would not have been @@ -96,6 +102,6 @@ # the complete header. Now that we're in CLOSE, we can # finish the header... - $buf =~ s/\015\012$//s; - $self->add_header("$buf\015\012"); + $$buf_ref =~ s/\015\012\z//s; + $self->add_header("$$buf_ref\015\012"); } $self->finish_header; @@ -104,10 +110,10 @@ else { - if (length $buf) + if ($$buf_ref ne '') { - $self->add_body($buf); + $self->add_body($$buf_ref); } } - $self->{buf} = ""; + $$buf_ref = ''; $self->finish_body; return 1;
Hi David, As Mark said, the buffer should not be growing with each successive call to PRINT. Every time it sees a line terminator, it should consume the line and remove that from the buffer. His theory, that you are not feeding in input with the proper line terminators, seems plausible and is likely the reason for the symptoms you are seeing. (The reason Mail::DKIM requires CRLF line termination was in an attempt to make it more compliant to the DKIM specifications, but in hindsight I think I should've taken a more pragmatic approach. Oh well...) Mark- I've taken your patch, thanks. I appreciate your help with this module and the performance analysis you've done on this and prior occasions :). If I'm not mistaken, the code is still re-iterating through the whole buffer to look for the CRLF, is that right? I wonder if it is worth redesigning the code to avoid that extra work, even though it's only really problem when used incorrectly... Jason
From: Mark.Martinec [...] ijs.si
Show quoted text
> If I'm not mistaken, the code is still re-iterating > through the whole buffer to look for the CRLF, is that right?
Not exactly, the rindex() searches for a CRLF in reverse, starting from the end of a buffer. Under normal circumstances that should amount to 30 or 40 characters scanned for every submitted buffer (perhaps a 16 kB .. 64 kB chunk with each call to PRINT for optimal results), which should be negligible. Show quoted text
> I wonder if it is worth redesigning the code to avoid that > extra work, even though it's only really problem when used > incorrectly...
Guaranteeing that multiline text chunks are terminated by a CRLF when they are passed downstream to canonicalization algorithms simplifies these algorithms hence making them faster, as they do not need to check and deal with a situation of a split CR LF across subsequent calls. So a small penalty in pre-processing yields a benefit in further processing. I guess there are other possible approaches, but they would need to be carefully benchmarked.
Version 0.39_6, containing Mark's patch for this bug, has been released to CPAN for testing. I intend to release this as version 0.40 later this week, if I do not discover any problems with the new version. Jason
Version 0.40, containing the fix for this bug, is now on its way to CPAN. Should show up there in a couple hours. I'm going to mark this ticket resolved.