Hello Exim Security Team,

We would like to report a security issue affecting Exim’s GnuTLS backend. The issue is a remotely reachable use-after-free in the BDAT body parsing path when a TLS `close_notify` is sent mid-body, followed by a final cleartext byte on the same TCP connection.

## **Summary**

We found a remotely reachable memory corruption issue in Exim’s GnuTLS backend. The bug is triggered during `BDAT` message body handling when a client sends a TLS `close_notify` before the body is fully finished, then sends one final byte in cleartext on the same connection.

At that point, Exim has already torn down the TLS layer and freed its transfer buffer, but part of the BDAT receive path can still call back into the old TLS `ungetc` handler. That handler then writes into the freed buffer.

In practice, this means a remote client can cause Exim to write into memory that was already freed during TLS teardown. The issue was reproduced on Exim 4.97 and also on current HEAD `4.99.1-242-371e52102` when built with `USE_GNUTLS=yes`.

The attacker only needs to reach STARTTLS, issue `MAIL FROM` / `RCPT TO`, and enter *BDAT* body parsing.

## **Technical details**

The root cause is that `tls_close()` in the GnuTLS backend frees `state->xfer_buffer` but does not fully invalidate the state associated with that buffer.

After `tls_close()` runs:

* `state->xfer_buffer` has been freed but is not reset to `NULL`  
* `state->xfer_buffer_lwm` and `state->xfer_buffer_hwm` are not reset  
* BDAT’s saved lower-layer receive callbacks, `lwr_receive_*`, can still point to TLS receive functions

During BDAT processing, `bdat_push_receive_functions()` snapshots the currently active receive callbacks. If BDAT is entered while TLS is active, this saved layer includes the TLS callbacks. Later, when `tls_close()` restores the outer receive callbacks back to the cleartext SMTP handlers, the saved BDAT layer can still call into the old TLS callbacks.

The problematic path is:

```
bdat_ungetc(...)
  -> lwr_receive_ungetc(...)
  -> tls_ungetc(...)
  -> ssl_xfer_buffer[--ssl_xfer_buffer_lwm] = ch;
```

At this point, `ssl_xfer_buffer` refers to memory that has already been freed.

## **Trigger**

The issue is reachable by sending `close_notify` in the middle of a `BDAT` body and then sending one final cleartext byte on the same TCP connection.

The observed wire shape is:

```
EHLO
STARTTLS
<TLS handshake>
EHLO
MAIL FROM:<...>
RCPT TO:<...>
BDAT 7 LAST\r\n
<TLS record: \r\n\r\nA>
<TLS record: B>
<TLS close_notify>
<cleartext byte: C>
```

A related variant uses `BDAT 8 LAST` and sends `BC` in the second TLS record.

The key point is that the remaining TLS transfer-buffer state at the time of `close_notify` determines where the later `tls_ungetc()` write lands in the freed transfer buffer. Since the buffer was freed but the pointer and low-water/high-water marks were left intact, the BDAT body fix-up path can still write through the stale TLS buffer pointer.

## **Proof of concept**

Start the ASAN-instrumented Exim daemon with the minimal configuration:

```
sudo ASAN_OPTIONS="log_path=stderr:abort_on_error=1" \
  LSAN_OPTIONS="log_path=stderr" \
  /home/ubuntu/research/exim-latest/exim/src/build-Linux-x86_64/exim \
  -oX 5556 \
  -C /home/ubuntu/research/tmp-pocs/pocs/configs/plain-minimal.conf \
  -bd -d 2>&1
```

Then run the PoC against the listening port:

```
python3 replay_gnutls_tls_ungetc_lowbytes.py \
  --port 5556 \
  --low-byte 1 \
  --host 127.0.0.1
```

The PoC demonstrates that the BDAT receive path can call back into `tls_ungetc()` after TLS teardown and cause a write through the freed `xfer_buffer` pointer.

## **Impact**

We believe this should be treated as a remotely reachable use-after-free in the GnuTLS build of Exim.

The immediate issue is that a remote client can drive Exim into writing into a transfer buffer that has already been freed by `tls_close()`. This can corrupt heap state or nearby allocations, depending on allocator behavior and runtime layout.

The server only needs to advertise STARTTLS and CHUNKING, and the attacker only needs to reach BDAT body parsing.

## **Relevant files**

* `src/tls-gnu.c`: `tls_close`, `tls_refill`, `tls_getc`  
* `src/tls.c`: `tls_ungetc`, GnuTLS aliases  
* `src/smtp_in.c`: `bdat_push_receive_functions`, `bdat_ungetc`  
* `src/receive.c`: body-loop `bdat_ungetc` callers  
* `src/store.c`: `internal_store_malloc`, `internal_store_free`

Please let us know if you need any additional details for reproduction.


Best regards,

