TLS_READ(3)

πŸ“¦ version 4.4.0, in alpine 3.24, arch rolling

πŸ’– TLS_READ(3) β€” the libtls connection handbook, bestie edition πŸ’–

okay so this is the libtls era β€” the library that lives in libtls, libssl and libcrypto and lets you actually use a TLS connection. think of this page as your group chat with the handshake. pull up a chair, grab a boba, and let's get into it πŸ§‹βœ¨


✨ NAME ✨

tls_read, tls_write, tls_handshake, tls_error, tls_close β€” literally the whole "use a TLS connection" starter pack πŸ’…


🌸 SYNOPSIS 🌸

first up, the include. you're pulling in libtls, libssl, libcrypto, and yes bestie you need #include <tls.h>:

library "libtls" libssl libcrypto #include <tls.h>

ssize_t
tls_read(struct tls *ctx, void *buf, size_t buflen);

ssize_t
tls_write(struct tls *ctx, const void *buf, size_t buflen);

int
tls_handshake(struct tls *ctx);

const char *
tls_error(struct tls *ctx);

int
tls_close(struct tls *ctx);

no cap, that's the entire cast of characters πŸ‘‘ keep ctx close, she's the main character in every single one.


πŸ¦‹ DESCRIPTION πŸ¦‹

  • tls_read() slurps buflen bytes of data from the socket into buf, and hands you back how much data it actually read. simple as that πŸŽ€
  • tls_write() pushes buflen bytes from buf out to the socket, and returns how much got written. iconic, no notes ✨
  • tls_handshake() does the TLS handshake on purpose, explicitly. and honestly? you only need to call this if you genuinely require the handshake to be finished before you move on β€” because tls_read() and tls_write() already do the handshake automatically whenever it's needed. yes she does the work for you 🫢
  • tls_error() is your gossip source. call it to grab a string with more deets about the most recent error relating to a context πŸ’€
  • tls_close() closes the connection when you're done. key detail, don't skip: only the TLS layer gets shut down, so you are responsible for closing the file descriptors β€” unless the connection was made with tls_connect(3) or tls_connect_servername(3). once it's closed, ctx can go to tls_free(3) 🌈

πŸ’… RETURN VALUES πŸ’…

tls_read() and tls_write() return a size on success, or -1 on error. tls_handshake() and tls_close() return 0 on success, or -1 on error. that part's chill πŸ“

but here's the part you actually need to internalize β€” tls_read(), tls_write(), tls_handshake(), and tls_close() all have two special return values:

  • TLS_WANT_POLLIN β€” the underlying read file descriptor needs to be readable to keep going 🌸
  • TLS_WANT_POLLOUT β€” the underlying write file descriptor needs to be writeable to keep going 🌸

so what do you do with that energy?

  • blocking file descriptors β†’ just repeat the same function call immediately.
  • non-blocking file descriptors β†’ repeat the same function call once the required condition has actually been met.
⚠️ no cap, this one's important: callers of these functions cannot rely on the value of the global errno. to stop you from mishandling error conditions, tls_read(), tls_write(), tls_handshake(), and tls_close() all explicitly clear errno. don't trust it, bestie πŸ’€

and tls_error()? it returns NULL if no error happened with ctx during or since the last call to tls_handshake(), tls_read(), tls_write(), tls_close(), or tls_reset(3) involving ctx β€” or if memory allocation failed while it was trying to assemble the string describing the most recent error related to ctx. so NULL is either "we're so fine" or "we couldn't even build the complaint" 😭


🧚 EXAMPLES 🧚

✨ TLS writes on a blocking file descriptor ✨

this example is giving "just keep going" energy:

...
while (len > 0) {
        ssize_t ret;

        ret = tls_write(ctx, buf, len);
        if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT)
                continue;
        if (ret == -1)
                errx(1, "tls_write: %s", tls_error(ctx));
        buf += ret;
        len -= ret;
}
...

see how TLS_WANT_POLLIN and TLS_WANT_POLLOUT just get continued? blocking mode said "we try again immediately" πŸ’–

🌈 TLS writes on a non-blocking file descriptor using poll(2) 🌈

and this one is the full glow-up β€” actually paying attention to what the fd wants:

...
pfd[0].fd = fd;
pfd[0].events = POLLIN|POLLOUT;
while (len > 0) {
        nready = poll(pfd, 1, 0);
        if (nready == -1)
                err(1, "poll");
        if ((pfd[0].revents & (POLLERR|POLLNVAL)))
                errx(1, "bad fd %d", pfd[0].fd);
        if ((pfd[0].revents & (pfd[0].events|POLLHUP))) {
                ssize_t ret;

                ret = tls_write(ctx, buf, len);
                if (ret == TLS_WANT_POLLIN)
                        pfd[0].events = POLLIN;
                else if (ret == TLS_WANT_POLLOUT)
                        pfd[0].events = POLLOUT;
                else if (ret == -1)
                        errx(1, "tls_write: %s", tls_error(ctx));
                else {
                        buf += ret;
                        len -= ret;
                }
        }
}
...

notice how the TLS_WANT_POLLIN / TLS_WANT_POLLOUT results literally rewrite pfd[0].events? that's the whole trick, iykyk πŸŽ€


πŸ”₯ SEE ALSO πŸ”₯

the rest of the friend group, go say hi:


⭐ HISTORY ⭐

tls_read(), tls_write(), tls_error(), and tls_close() appeared in OpenBSD 5.6 and got their final names in OpenBSD 5.7 β€” glow-up era fr fr πŸ’…

tls_handshake() appeared in OpenBSD 5.9. late to the party but she showed up gorgeous ✨


πŸ‘‘ AUTHORS πŸ‘‘

Joel Sing <jsing@openbsd.org> with contributions from Bob Beck <beck@openbsd.org> β€” absolute icons, we love them 🫢


πŸ’€ CAVEATS πŸ’€

okay so this is the do not do this section and i need you locked in:

the function tls_error() returns an internal pointer. it must NOT be freed by the application β€” or you get a double free error πŸ’€ and that pointer will become invalid when the next error occurs with ctx.

consequently, if your application might need that message at a later time, it has to copy the string before calling the next libtls function involving ctx. because if you don't? a segmentation fault, or reading through to unintended data, is the likely result. and we are not doing that to ourselves πŸ˜­πŸ™ˆ


that's the whole page bestie β€” go forth and handle your TLS_WANT_POLLIN properly 🌟

Debian 🌸 July 7, 2025 β€” yeah that's the version and date, dropping it here in the cutest possible sign-off, mwah πŸ’‹πŸŽ€

πŸ“¦ where this page comes from
  • alpine 3.24: libressl-doc 4.3.1-r0 Β· custom
  • arch rolling: libressl 4.4.0-1 Β· ISC AND custom:OpenSSL
  • πŸ’– girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms