Anatomy of a dangling pointer in Exim: debugging the "transport not found" error in cPanel with smarthost and SRS
Back to blog

Anatomy of a dangling pointer in Exim: debugging the "transport not found" error in cPanel with smarthost and SRS

10/16/2026 · 5 min · Email

Working with email servers and custom Exim routing rules provides tremendous flexibility, but it comes with a trade-off: when combining external relay services (smarthosts), sender rewriting (SRS), and security filters like Imunify360, missing runtime references can quietly interrupt legitimate email delivery.

While inspecting delivery delays on a cPanel server recently, a subtle issue surfaced: inbound messages sent by major providers such as Google were intermittently rejected with a 451 temporary error during the sender verification phase (sender verify).

This guide walks through the exact delivery path, explains how Exim processes transport pointers during startup, shows why routine terminal tests hid the failure, and demonstrates how to apply a permanent fix in cPanel.


1. Error logs and impact on the SMTP transaction#

The investigation started with the main Exim transaction log (/var/log/exim_mainlog). Filtering for incoming connections from Google MX servers revealed the root cause behind the delivery delays:

2026-04-17 17:11:52 H=mail-oo1-f52.google.com [209.85.161.52]:60854 sender verify defer for <[email protected]>: transport "dkim_remote_forwarded_smtp" not found in smarthost_forwarded router
2026-04-17 17:11:52 H=mail-oo1-f52.google.com [209.85.161.52]:60854 X=TLS1.3:TLS_AES_128_GCM_SHA256:128 F=<[email protected]> temporarily rejected RCPT <[email protected]>: Could not complete sender verify

How sender verify triggers the 451 rejection#

During the SMTP conversation, immediately after the client issues the RCPT TO:<[email protected]> command, Exim evaluates the incoming Access Control List (acl_check_rcpt). When the verify = sender rule is active, Exim briefly suspends the transaction with the remote client.

During this pause, the MTA initiates an internal callback routing check to determine whether the sender address ([email protected]) can be routed back and accept bounces.

The 451 code (temporary rejection) occurs because this internal routing process failed due to a local configuration mistake. When Google receives a temporary failure code, it retains the message in its outbound queue and retries delivery according to exponential backoff timers, creating noticeable delays for recipients.


2. Under the hood: how Exim handles transports and pointers in c#

To understand why this happens, it helps to examine how the Exim binary processes directives from /etc/exim.conf.

Configuration parsing and the symbol table (readconf.c)#

When the Exim daemon starts or receives a SIGHUP signal to reload configuration tables, the readconf_main() function parses the file sequentially.

  1. Routers and transports are converted into internal C structs (router_instance and transport_instance).
  2. Exim stores transport names in an internal hash table.
  3. The tricky detail: Exim does not validate, at startup, whether transport names declared inside dynamic string expansions (such as ${if ...}) actually exist. It only stores the string for lazy evaluation at runtime.

Lazy evaluation and runtime transport resolution (expand.c and transport.c)#

During the sender verify check, route_address() walks down the linked list of routers until it hits a matching condition. On this server, the address matched the custom smarthost_forwarded router:

smarthost_forwarded:
  driver = manualroute
  domains = !+local_domains
  condition = ${if and {{def:original_domain}{!def:authenticated_id}}{yes}{no}}
  no_more
  transport = dkim_remote_forwarded_smtp
  route_list = * smtp-out.domain.com::587

This interacted with the SRS macro inside the authenticated smarthost router (smarthost_auth):

.ifdef SRSENABLED
    transport = ${if eq {$local_part@$domain} \
                        {$original_local_part@$original_domain} \
                     {dkim_remote_smtp} {dkim_remote_forwarded_smtp}}
.endif

When expand_string() evaluated that condition, it returned "dkim_remote_forwarded_smtp". Control was then handed over to the transport subsystem (transport.c), where transport_find(uschar *name) ran a lookup in the symbol hash table.

Because dkim_remote_forwarded_smtp had never been declared in the transports section of the configuration file, transport_find() returned a NULL pointer. Unable to assign a valid delivery transport to the router, Exim aborted the check and wrote the error to the log: transport "dkim_remote_forwarded_smtp" not found.


3. Investigating behavior with command-line tools#

To confirm the state of memory without disrupting active mail flow, we checked Exim using native CLI options directly from the terminal.

Verifying loaded transports in memory#

The -bP flag (Print Configuration) displays the active compiled runtime state:

exim -bP transports | grep "_smtp"

The output showed existing transports:

remote_smtp_smart_regular transport:
remote_smtp transport:
dkim_remote_smtp transport:

Checking specifically for the transport requested by the router:

exim -bP transport dkim_remote_forwarded_smtp

Returned:

transport dkim_remote_forwarded_smtp not found

The symbol was absent from the active process memory map.

Tracing router evaluations with debug mode#

To see how Exim walked through routers, we tested address resolution with route and transport debugging enabled:

exim -d+route+transport -bt [email protected]

In the resulting process trace, we saw file lookups taking place:

search_open: lsearch "/etc/localdomains"
internal_search_find: file="/etc/localdomains" type=lsearch key="domain.com"
lookup failed

Further down the log, Imunify360's filter router intercepted the flow:

--------> imunifyemail_spamfilter_router router <--------
set transport 'imunifyemail_spamfilter_transport'
queued for imunifyemail_spamfilter_transport transport: local_part = sender
routed by imunifyemail_spamfilter_router router

Why regular -bt testing concealed the bug#

This explains why manual testing did not immediately expose the issue: when running a standard exim -bt test, Imunify360 routes the message into its own local scan queue early in the chain, before Exim ever evaluates the smarthost routers positioned below it.

However, when an inbound connection from Google triggers a sender verify callback, Exim evaluates the complete routing logic to confirm return path viability. That is when the smarthost router runs, tries to bind the missing transport, and triggers the deferral.


4. Ruling out edge cases and operating system anomalies#

Before updating configuration templates, it is good practice to verify that external system factors are not causing false positives.

File descriptor exhaustion and DKIM private keys#

If Exim ran out of open file descriptors, attempts to access private keys in /var/cpanel/domain_keys/private/ could fail, preventing transport initialization.

We traced file-related system calls during verification:

strace -f -s 128 -e trace=open,openat,stat exim -bt [email protected] 2>&1 | grep domain_keys

No EMFILE (Too many open files) or ENOENT (No such file or directory) errors were returned, confirming normal file access.

Library compatibility and TLS handshakes with the smarthost#

Another question was whether OpenSSL library updates interfered with TLS handshakes on port 587 of the external smarthost.

We tested the connection directly:

openssl s_client -connect smtp-out.domain.com:587 -starttls smtp

The TLS handshake completed cleanly, confirming network encryption functioned as expected.


5. Applying a permanent fix in cPanel#

In cPanel environments, /etc/exim.conf is dynamically built from templates. Manual changes written directly to that file are overwritten during automated system updates (upcp). The new transport must be added through WHM's advanced configuration interface.

Adding the transport in WHM advanced editor#

  1. Open WHM and navigate to Service Configuration -> Exim Configuration Manager.
  2. Select the Advanced Editor tab.
  3. Scroll down to Section: TRANSPORTSTART.
  4. Add the complete transport block, including dynamic outbound IP selection, HELO data alignment, and DKIM signing:
dkim_remote_forwarded_smtp:
  driver = smtp
  hosts_require_tls = *
  interface = <; ${if > {${extract {size} {${stat:/etc/mailips}} }} {0} {${lookup {${lc:${perl{get_message_sender_domain}}}} lsearch{/etc/mailips} {$value} {${lookup {${if match_domain {$original_domain} {+relay_domains} {${lc:$original_domain}} {} }} lsearch{/etc/mailips} {$value} {${lookup {${perl{get_sender_from_uid}}} lsearch*{/etc/mailips} {$value} {} }} }} }} }
  helo_data = ${if > {${extract{size}{${stat:/etc/mailhelo}}}} {0} {${lookup {${lc:${perl{get_message_sender_domain}}}} lsearch{/etc/mailhelo} {$value} {${lookup {${if match_domain {$original_domain} {+relay_domains} {${lc:$original_domain}} {} }} lsearch{/etc/mailhelo} {$value} {${lookup {${perl{get_sender_from_uid}}} lsearch*{/etc/mailhelo} {$value} {$primary_hostname} }} }} }} {$primary_hostname} }
  dkim_domain = ${perl{get_dkim_domain}}
  dkim_selector = default
  dkim_private_key = /var/cpanel/domain_keys/private/${dkim_domain}
  dkim_canon = relaxed
  dkim_hash = sha256

This configuration mirrors cPanel's built-in mechanism for mapping outbound IPs from /etc/mailips and HELO identifiers from /etc/mailhelo, while preserving valid DKIM signatures on forwarded traffic.

Rebuilding the configuration and restarting the service#

Save the template in WHM or execute the following maintenance commands in the terminal:

# Rebuild the final /etc/exim.conf file and test syntax
/scripts/buildeximconf

# Restart the Exim service under Systemd
/scripts/restartsrv_exim

Verify that the symbol is now registered in the running daemon:

exim -bP transport dkim_remote_forwarded_smtp

The output should confirm the loaded parameters:

driver = smtp
dkim_canon = relaxed
dkim_domain = ${perl{get_dkim_domain}}
dkim_hash = sha256
dkim_private_key = /var/cpanel/domain_keys/private/${dkim_domain}
dkim_selector = default
hosts_require_tls = *

Best practices for maintaining router and transport parity in Exim#

The transport not found error during sender verification highlights an essential architectural trait of Exim: lazy string evaluation allows configuration mismatches to linger undetected until specific external conditions trigger them.

When maintaining custom routing logic:

  1. Verify symmetry between routers and transports: if a router dynamically selects transports through string expansions (such as when combining SRS and relay smarthosts), ensure every possible output string has a corresponding definition in TRANSPORTSTART.
  2. Account for scanner interception in testing: local spam filters and antivirus layers can handle messages before smarthost routers run, hiding issues that only full callback checks will trigger.
  3. Persist changes through control panel templates: in managed hosting environments like cPanel, always commit edits through official configuration interfaces so that automatic updates do not discard your custom transports.

Was this article helpful?

Leave a quick reaction to help prioritize future technical guides:

CC BY-NC

This post is licensed under CC BY-NC.

Comments

Join the discussion below.

0 comments