Skip to content

Mail::Milter::Authentication::Resolver triggers deep recursion with Mail-DMARC 1.20260612's RFC 9989 tree_walk #180

Description

@pgnd

DESCRIPTION
upgraded Mail-DMARC 1.20260612's new RFC 9989 tree_walk implementation causes Perl deep recursion warnings in Mail::Milter::Authentication::Resolver (CPAN). The warnings appear for every email processed with DMARC checking enabled.
Risk of stack exhaustion on deeply nested subdomain structures.

in Fedora44 mock env + Perl v5.42.2, CPAN build of Mail::Milter::Authentication v4.20260521 prior to 6/12 picks up

Mail-DMARC: 1.20260325 (released 2026-02-26)

after, it grabs new release of Mail::DMARC

Mail-DMARC v1.20260612 (released 2026-06-12)

and now logs repeating deep recursion warnings; e.g.,

fm-auth-milter[3008]: Warning: Deep recursion on subroutine "Mail::Milter::Authentication::Resolver::_do" at /usr/local/fm-auth-milter/lib/perl5/Mail/Milter/Authentication/Resolver.pm line 124.<LF>
fm-auth-milter[3008]: Warning: Deep recursion on subroutine "Mail::DMARC::PurePerl::get_organizational_domain" at /usr/local/fm-auth-milter/lib/perl5/Mail/Milter/Authentication/Resolver.pm line 59.<LF>
fm-auth-milter[3008]: Warning: Deep recursion on subroutine "Mail::DMARC::PurePerl::tree_walk" at /usr/local/fm-auth-milter/lib/perl5/Mail/DMARC/PurePerl.pm line 476.<LF>
fm-auth-milter[3008]: Warning: Deep recursion on subroutine "Mail::Milter::Authentication::Resolver::send" at /usr/local/fm-auth-milter/lib/perl5/Mail/DMARC/PurePerl.pm line 404.<LF>

checking, every DNS operation (send, query, search) calls _do(), which unconditionally calls get_organizational_domain() on line 59 when DMARC is loaded

Mail::Milter::Authentication::Resolver::_do()
https://github.com/fastmail/authentication_milter/blob/master/lib/Mail/Milter/Authentication/Resolver.pm#L59
sub _do { ## no critic
    my $self = shift;
    my $what = shift;
    ...
    my $domain = $_[0];
    my $org_domain = $_[0];
    my $query = $_[1];
    if ( $handler->is_handler_loaded( 'DMARC' ) ) {
        my $dmarc_object = $handler->get_handler('DMARC')->get_dmarc_object();
!!      $org_domain = eval{ $dmarc_object->get_organizational_domain( $org_domain ) };
        $handler->handle_exception( $@ );
    }

Mail::DMARC v1.20260612 introduces RFC 9989 DMARC support,
cc: @msimerson

implement RFC 9989
 https://github.com/msimerson/mail-dmarc/pull/297

RFC 9989 tree_walk (Mail/DMARC/PurePerl.pm) now performs iterative DNS queries in a loop, up to ~ 8 queries per organizational domain lookup.

At line 404, it calls:

my $query = $self->get_resolver->send( "_dmarc.$target", 'TXT' );

and triggers a recursive call chain,

Mail/Milter/Authentication/Resolver.pm:122-125 (send method)
	-> Resolver.pm:45-110 (_do method)
	-> Resolver.pm:59 (get_organizational_domain call)
	-> Mail/DMARC/PurePerl.pm:470-486 (get_organizational_domain method)
	-> Mail/DMARC/PurePerl.pm:476 (tree_walk call)
	-> Mail/DMARC/PurePerl.pm:404 (get_resolver->send call)
	-> Resolver.pm:122 (send method) [recursion repeats]

where each tree_walk iteration (querying _dmarc.domain, _dmarc.subdomain, etc.) triggers get_organizational_domain() again, creating unmatched recursion.

that nested recursion scales, and approaches/exceeds Perl's default ~100-level limit; per perldoc perldiag

		...
1938    Deep recursion on anonymous subroutine
	    Deep recursion on subroutine "%s"
	        (W recursion) This subroutine has called itself (directly or
	        indirectly) 100 times more than it has returned. This probably
	        indicates an infinite recursion, unless you're writing strange
	        benchmark programs, in which case it indicates something else.
		...

ATM here, it's just WARNINGS logged; processing continues, and mail's checked/delivered.

As long as the recursion depth stays under the available stack.
IIUC, that's currently un-checked.

This triggers on every DMARC-checked message.
A domain structure with deep (dunno HOW deep ... yet) subdomain hierarchies could exceed that limit, exhaust the stack exhaustion and, i suspect, trigger a fatal overflow.

i think Resolver.pm needs to skip (limit?) the get_organizational_domain() call for the synthetic DNS domains that tree_walk creates for lookups -- i.e., the _dmarc.*

e.g.,

sub _do { ## no critic
    my $self = shift;
    my $what = shift;
    ...
    my $domain = $_[0];
    my $org_domain = $_[0];
    my $query = $_[1];
-   if ( $handler->is_handler_loaded( 'DMARC' ) ) {
+   if ( $handler->is_handler_loaded( 'DMARC' ) && $_[0] !~ /^_dmarc\./ ) {
        my $dmarc_object = $handler->get_handler('DMARC')->get_dmarc_object();
        $org_domain = eval{ $dmarc_object->get_organizational_domain( $org_domain ) };
        $handler->handle_exception( $@ );
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions