adafruit / circuitpython

CircuitPython - a Python implementation for teaching coding with microcontrollers
https://circuitpython.org
MIT License
3.97k stars 1.16k forks source link

use of STARTTLS (wrap already-connected socket) leads to hard fault #7314

Open jepler opened 1 year ago

jepler commented 1 year ago

CircuitPython version

Adafruit CircuitPython 8.0.0-beta.4-68-g2326b49b24-dirty on 2022-12-07; Adafruit Feather ESP32-S3 Reverse TFT with ESP32S3

Code/REPL

import wifi, socketpool, ssl, time
wifi.radio.connect(<omitted>)
import socketpool
socket = socketpool.SocketPool(wifi.radio)
ctx = ssl.create_default_context()

for i in range(3):
    log(i)
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(('example.com', 443))
    sss = ctx.wrap_socket(s)
    print(sss.send(b"GET / HTTP/1.0\r\n\r\n"))
    print(s.recv(8)) # this line is incorrect but it does not matter as it is never reached
    s.close()
    sss.close()

Behavior

When the send call is reached, the following happens:

[tio 15:35:18] Disconnected
[tio 15:35:21] Connected
Auto-reload is off.
Running in safe mode! Not running saved code.

You are in safe mode because:
CircuitPython core code crashed hard. Whoops!
Crash into the HardFault_Handler.
Please file an issue with the contents of your CIRCUITPY drive at 
https://github.com/adafruit/circuitpython/issues

Press any key to enter the REPL. Use CTRL-D to reload.

Description

No response

Additional information

The correct sequence is apparently correct, and does work:

sss = ctx.wrap_socket(s)
sss.connect(('example.com', 443))
vladak commented 1 year ago

I've seen SSL related hardfault on QtPy with ESP32-S2, trying to connect to a MQTT broker on port 8883 with TLS. When I Ctrl-C'd the program, it sometimes resulted in hard fault. Even though I had no time to isolate this further, the problem could be related to this issue.

scogswell commented 1 year ago

I've been trying to do SMTP with STARTTLS in which you start with a non-ssl socket and upgrade to an ssl socket during the transactions, and I think it triggers this same problem. If I wrap_socket(), the next recv will make the ESP32S3 (and S2) crash to safe mode. With 8.0.4 and 8.1.0-beta0. The code works perfectly on the pico-w though, 8.0.4.

I made a basic program that illustrates this, I think. If I try the fix mentioned above of doing a connect() after the wrap_socket() I'll get OSError: Failed SSL handshake

# STARTTLS example that crashes ESP32S3 into safe mode but works on pico-w
import wifi
import ssl
import socketpool
import time

STARTUP_WAIT = 5
HOST = "smtp.gmail.com"
PORT = 587
MAXBUF = 4096
buf = bytearray(MAXBUF)

def send_smtp_command(cmd: bytearray):
    """Quick function to send a command"""
    print("Sending",cmd)
    sock.send(cmd)

def receive_smtp_response(expect: bytearray):
    """Quick and inefficient function to read an smtp response"""
    more = True
    response = b""

    while more:
        sock.recv_into(buf,3)
        code = buf[:3]
        if code != expect:
            print("Expected code ",expect," got ",code)
            raise ConnectionError
        sock.recv_into(buf,1)
        if buf[:1] != b'-':
            more = False
        while True:
            sock.recv_into(buf,1)
            bin = buf[:1]
            if bin == b"\n":
                break
            response += bin
    return code,response

print("short wait for startup")
time.sleep(STARTUP_WAIT) 

if wifi.radio.ipv4_address is None:
    print("connecting to wifi")
    wifi.radio.connect("my ssid","my password")
print(f"local address {wifi.radio.ipv4_address}")

pool = socketpool.SocketPool(wifi.radio)
ssl_context = ssl.create_default_context()

sock = pool.socket(pool.AF_INET, pool.SOCK_STREAM)

print("Connecting to socket")
sock.connect((HOST,PORT))
c,r = receive_smtp_response(b"220")
print("Got code:",c,"\nresponse:",r)

send_smtp_command(b"EHLO 127.0.0.1\r\n")
c,r = receive_smtp_response(b"250")
print("Got code:",c,"\nresponse:",r)

if b"STARTTLS" not in r:
    print("Couldn't find STARTTLS in response")
    raise ConnectionError

send_smtp_command(b"STARTTLS\r\n")
c,r = receive_smtp_response(b"220")
print("Got code:",c,"\nresponse:",r)

print("Wrapping Socket")
sock = ssl_context.wrap_socket(sock=sock)

# This command works on pico-w but not ESP32s2/s3
send_smtp_command(b"EHLO 127.0.0.1\r\n")
c,r = receive_smtp_response(b"250")
print("Got code:",c,"\nresponse:",r)

send_smtp_command(b"QUIT\r\n")
sock.close()

On pico-w:

...
Sending b'STARTTLS\r\n'
Got code: bytearray(b'220') 
response: b'2.0.0 Ready to start TLS\r'
Wrapping Socket
Sending b'EHLO 127.0.0.1\r\n'
Got code: bytearray(b'250') 
response: b'smtp.gmail.com at your service, [174.118.230.138]\rSIZE 35882577\r8BITMIME\rAUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH\rENHANCEDSTATUSCODES\rPIPELINING\rCHUNKING\rSMTPUTF8\r'
Sending b'QUIT\r\n'

On ESP32S3:

...
Sending b'STARTTLS\r\n'
Got code: bytearray(b'220') 
response: b'2.0.0 Ready to start TLS\r'
Wrapping Socket
Sending b'EHLO 127.0.0.1\r\n'
(Crash to safe mode)
jepler commented 1 year ago

Thanks for that info -- it's a good point that this functionality is required for protocols that use STARTTLS. On a practical note you might check whether you can use port 465 instead, which depends on what the server accepts. https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers#cite_note-tcp465-86

scogswell commented 1 year ago

I was able to make my smtp work using port 465 and doing ssl right from the initial connection no problem. I was trying to cover the starttls case when I noticed this crash come up.

jepler commented 1 year ago

It wouldn't hurt to re-test this after idf5 but it's probably our bug.