Skip to content

Commit e2a6761

Browse files
committed
Fix allow_tls_v1, allow_tls_v1_1 and allow_tls_v1_2 on 32-bit platforms
`SSL_CTX_set_options` and `SSL_CTX_clear_options` were declared with a `ULong` option mask. `ULong` is Pony's C `long`: 64 bits on 64-bit Unix, 32 bits everywhere else. OpenSSL 3.x declares both as taking a `uint64_t`. On arm32 a 64-bit argument starts on an even-numbered register, so the 32 bits the emitted call puts in r1 are never read, and the function ORs whatever sits in r2:r3 into the mask. `allow_tls_v1_2(false)` cannot disable TLS 1.2, its `(true)` form cannot re-enable it, and unrelated options get set from whatever was in those registers. I have not run this on 32-bit hardware. The emitted call and the calling convention are what say so. The mask is `U64` from the `_SslOpNo*` constants through to the call rather than only at the FFI boundary. OpenSSL 3.x has option flags at bits 32 through 36, and a `ULong` carrier would truncate every one of them to zero on 32-bit before the call. The other fourteen declarations corrected here are harmless on every platform ponyc builds. Four name a pointer whose element type never reaches the ABI. Seven name the wrong sign for a C `int` or `long`, which is the same width either way. Three are the wrong width somewhere nobody calls them, or somewhere the callee reads only the bits that arrive. They are corrected because a wrong declaration is what the next one gets copied from. `_alpn_select_cb` wrote the selected protocol's length by copying the first byte of a `USize`, which is the low byte on little-endian and the high byte on big-endian. ponyc builds no big-endian target, so nothing was broken. It writes the byte through a `U8` now, and takes the pointer width from `USize(0)` rather than from the length variable's type, which had coupled the two. `Digest.sha224()`'s docstring named SHA-256. It calls `EVP_sha224` and returns 28 bytes. Closes #78
1 parent 6b93e38 commit e2a6761

7 files changed

Lines changed: 224 additions & 62 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
## Fix allow_tls_v1, allow_tls_v1_1 and allow_tls_v1_2 on 32-bit platforms
2+
3+
`SSLContext.allow_tls_v1`, `SSLContext.allow_tls_v1_1` and `SSLContext.allow_tls_v1_2` did not change the protocol version they name, and left the context with TLS options nobody asked for. A call meant to forbid a version left it allowed, a call meant to allow one left it forbidden, and unrelated TLS features were switched on either way.
4+
5+
This affected 32-bit builds using OpenSSL 3.x or 4.x. 64-bit builds, LibreSSL builds, and OpenSSL 1.1.x builds were never affected.
6+
7+
These methods now change only the protocol version they name.

AGENTS.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,39 @@ else
8080
end
8181
```
8282

83+
**One symbol whose C signature changes by version** needs two `use` statements, and the second guard must exclude the first. ponyc resolves an FFI call by enumerating every combination of the defines named in the guards, not just the one the build passes, and nothing tells it the `ssl=` defines are mutually exclusive. Overlapping guards give "Multiple possible declarations for FFI call":
84+
```pony
85+
use @SSL_CTX_set_options[U64](ctx: Pointer[_SSLContext] tag, opts: U64) if "openssl_3.0.x" or "openssl_4.0.x"
86+
use @SSL_CTX_set_options[ULong](ctx: Pointer[_SSLContext] tag, opts: ULong) if "openssl_1.1.x" and not ("openssl_3.0.x" or "openssl_4.0.x")
87+
```
88+
`libressl` appears in neither guard because LibreSSL reaches these options through `SSL_CTX_ctrl` instead — see LibreSSL API Divergences above.
89+
90+
The call site needs an `ifdef` split too. Pony has no implicit numeric conversion, so one call expression cannot satisfy both parameter types.
91+
92+
Adding a new SSL define means adding it to the exclusion list. Forgetting is safe: the enumeration finds the overlap and fails the build on every backend, not just the new one.
93+
8394
Every ifdef chain must end with `compile_error` to catch missing defines at compile time.
8495

96+
## FFI Type Conventions
97+
98+
Declare the Pony type that corresponds to the C type in the header, not one that happens to be the same width on the platforms CI builds:
99+
100+
| C type | Pony type |
101+
|---|---|
102+
| `int` / `unsigned int` | `I32` / `U32` |
103+
| `long` / `unsigned long` | `ILong` / `ULong` |
104+
| `size_t` | `USize` |
105+
| `uint64_t` | `U64` |
106+
| pointer to an opaque C struct | `Pointer[_Name]`, declaring a phantom primitive for it |
107+
| `void *`, or a pointer only ever passed as null | `Pointer[None]` |
108+
| pointer to bytes | `Pointer[U8]` |
109+
110+
`ILong`/`ULong` track C's `long`: 32 bits on Windows and on 32-bit targets such as `arm32`, 64 bits on 64-bit Unix-like targets. `USize` tracks `size_t`, which is pointer-width. Neither is a stand-in for `uint64_t`. Reaching for `ULong` because it is 64 bits on the platform in front of you turns a correct call into a wrong one everywhere else — `SSL_CTX_set_options` takes a `uint64_t` in OpenSSL 3.x, and a `ULong` declaration passes 32 bits on every 32-bit build.
111+
112+
A `Pointer[X]`'s element type never reaches the ABI, so `Pointer[USize]` where C says `unsigned int *` cannot break a call that passes null. It breaks the caller who later passes `addressof` a real `USize`: C writes four of the eight bytes and the rest keep whatever was in the slot, so the caller reads a garbage value.
113+
114+
Public Pony signatures do not have to match the C types. Convert at the FFI call instead — `SSLContext.set_verify_depth` takes a `U32` and passes `depth.i32()`. Where the public type admits values the C type cannot hold, converting silently wraps: a depth above `2^31` arrives as a negative `int`. Validate at the public boundary or say so in the docstring; do not let the conversion be the whole answer.
115+
85116
## Key Files
86117

87118
| File | Role |

ssl/crypto/digest.pony

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ use "lib:crypto"
44
use "lib:bcrypt" if windows
55

66
use @EVP_MD_CTX_new[Pointer[_EVPCTX]]() if "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl"
7-
use @EVP_DigestInit_ex[I32](ctx: Pointer[_EVPCTX] tag, t: Pointer[_EVPMD], impl: USize)
7+
use @EVP_DigestInit_ex[I32](ctx: Pointer[_EVPCTX] tag, t: Pointer[_EVPMD], impl: Pointer[None])
88
use @EVP_DigestUpdate[I32](ctx: Pointer[_EVPCTX] tag, d: Pointer[U8] tag, cnt: USize)
9-
use @EVP_DigestFinal_ex[I32](ctx: Pointer[_EVPCTX] tag, md: Pointer[U8] tag, s: Pointer[USize])
9+
use @EVP_DigestFinal_ex[I32](ctx: Pointer[_EVPCTX] tag, md: Pointer[U8] tag, s: Pointer[U32])
1010
use @EVP_DigestFinalXOF[I32](ctx: Pointer[_EVPCTX] tag, md: Pointer[U8] tag, len: USize) if "openssl_3.0.x" or "openssl_4.0.x"
1111
use @EVP_MD_CTX_free[None](ctx: Pointer[_EVPCTX]) if "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl"
1212

@@ -44,7 +44,7 @@ class Digest
4444
else
4545
compile_error "You must select an SSL version to use."
4646
end
47-
@EVP_DigestInit_ex(_ctx, @EVP_md5(), USize(0))
47+
@EVP_DigestInit_ex(_ctx, @EVP_md5(), Pointer[None])
4848

4949
new ripemd160() =>
5050
"""
@@ -57,7 +57,7 @@ class Digest
5757
else
5858
compile_error "You must select an SSL version to use."
5959
end
60-
@EVP_DigestInit_ex(_ctx, @EVP_ripemd160(), USize(0))
60+
@EVP_DigestInit_ex(_ctx, @EVP_ripemd160(), Pointer[None])
6161

6262
new sha1() =>
6363
"""
@@ -70,11 +70,11 @@ class Digest
7070
else
7171
compile_error "You must select an SSL version to use."
7272
end
73-
@EVP_DigestInit_ex(_ctx, @EVP_sha1(), USize(0))
73+
@EVP_DigestInit_ex(_ctx, @EVP_sha1(), Pointer[None])
7474

7575
new sha224() =>
7676
"""
77-
Use the SHA256 algorithm to calculate the hash.
77+
Use the SHA224 algorithm to calculate the hash.
7878
"""
7979
_variable_length = false
8080
_digest_size = 28
@@ -83,7 +83,7 @@ class Digest
8383
else
8484
compile_error "You must select an SSL version to use."
8585
end
86-
@EVP_DigestInit_ex(_ctx, @EVP_sha224(), USize(0))
86+
@EVP_DigestInit_ex(_ctx, @EVP_sha224(), Pointer[None])
8787

8888
new sha256() =>
8989
"""
@@ -96,7 +96,7 @@ class Digest
9696
else
9797
compile_error "You must select an SSL version to use."
9898
end
99-
@EVP_DigestInit_ex(_ctx, @EVP_sha256(), USize(0))
99+
@EVP_DigestInit_ex(_ctx, @EVP_sha256(), Pointer[None])
100100

101101
new sha384() =>
102102
"""
@@ -109,7 +109,7 @@ class Digest
109109
else
110110
compile_error "You must select an SSL version to use."
111111
end
112-
@EVP_DigestInit_ex(_ctx, @EVP_sha384(), USize(0))
112+
@EVP_DigestInit_ex(_ctx, @EVP_sha384(), Pointer[None])
113113

114114
new sha512() =>
115115
"""
@@ -122,7 +122,7 @@ class Digest
122122
else
123123
compile_error "You must select an SSL version to use."
124124
end
125-
@EVP_DigestInit_ex(_ctx, @EVP_sha512(), USize(0))
125+
@EVP_DigestInit_ex(_ctx, @EVP_sha512(), Pointer[None])
126126

127127
new shake128(size': USize = 16) =>
128128
"""
@@ -142,7 +142,7 @@ class Digest
142142
_digest_size = 16
143143
end
144144
_ctx = @EVP_MD_CTX_new()
145-
@EVP_DigestInit_ex(_ctx, @EVP_shake128(), USize(0))
145+
@EVP_DigestInit_ex(_ctx, @EVP_shake128(), Pointer[None])
146146
else
147147
compile_error "shake128 is only supported with OpenSSL 1.1.x, 3.0.x, or 4.0.x"
148148
end
@@ -165,7 +165,7 @@ class Digest
165165
_digest_size = 32
166166
end
167167
_ctx = @EVP_MD_CTX_new()
168-
@EVP_DigestInit_ex(_ctx, @EVP_shake256(), USize(0))
168+
@EVP_DigestInit_ex(_ctx, @EVP_shake256(), Pointer[None])
169169
else
170170
compile_error "shake256 is only supported with OpenSSL 1.1.x, 3.0.x, or 4.0.x"
171171
end
@@ -194,10 +194,10 @@ class Digest
194194
if _variable_length then
195195
@EVP_DigestFinalXOF(_ctx, digest.cpointer(), size)
196196
else
197-
@EVP_DigestFinal_ex(_ctx, digest.cpointer(), Pointer[USize])
197+
@EVP_DigestFinal_ex(_ctx, digest.cpointer(), Pointer[U32])
198198
end
199199
elseif "openssl_1.1.x" or "libressl" then
200-
@EVP_DigestFinal_ex(_ctx, digest.cpointer(), Pointer[USize])
200+
@EVP_DigestFinal_ex(_ctx, digest.cpointer(), Pointer[U32])
201201
else
202202
compile_error "You must select an SSL version to use."
203203
end

ssl/net/_test.pony

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ actor \nodoc\ Main is TestList
3535
test(_TestSSLContextSetCiphersAfterDispose)
3636
test(_TestSSLContextSetVerifyDepthAfterDispose)
3737
test(_TestSSLContextAllowTlsAfterDispose)
38+
test(_TestSSLContextAllowTlsV1u2)
3839
test(_TestSSLContextClientAfterDispose)
3940
test(_TestSSLContextServerAfterDispose)
4041
test(_TestTCPSSLWritev)
@@ -1793,13 +1794,16 @@ class \nodoc\ iso _TestSSLContextSetVerifyDepthAfterDispose is UnitTest
17931794
dereferences the null context on every backend.
17941795
17951796
There is nothing to observe beyond the call returning, so the context is
1796-
checked for inertness afterwards.
1797+
checked for inertness afterwards. The call before the dispose is the only one
1798+
in the suite that carries a depth into `SSL_CTX_set_verify_depth`.
17971799
"""
17981800
fun name(): String => "net/ssl/SSLContext.set_verify_depth/after_dispose"
17991801

18001802
fun apply(h: TestHelper) =>
18011803
let ctx = SSLContext
18021804

1805+
ctx.set_verify_depth(4)
1806+
18031807
ctx.dispose()
18041808
ctx.set_verify_depth(4)
18051809

@@ -1842,6 +1846,112 @@ class \nodoc\ iso _TestSSLContextAllowTlsAfterDispose is UnitTest
18421846
ctx.alpn_set_client_protocols(["h2"]),
18431847
"the context should still be disposed")
18441848

1849+
class \nodoc\ iso _TestSSLContextAllowTlsV1u2 is UnitTest
1850+
"""
1851+
`allow_tls_v1_2` takes effect on a live context.
1852+
1853+
The context permits TLS 1.2 and nothing else, so disabling TLS 1.2 leaves the
1854+
handshake no version to negotiate. Re-enabling it clears the option and the
1855+
handshake completes again.
1856+
1857+
The first assertion is the control. Without it, a handshake that failed for
1858+
an unrelated reason would look like the option taking effect.
1859+
1860+
The context is live, so `SSLContext._set_options` and `_clear_options` reach
1861+
the SSL library rather than returning at the disposed check.
1862+
"""
1863+
fun name(): String => "net/ssl/SSLContext.allow_tls_v1_2"
1864+
1865+
fun apply(h: TestHelper) =>
1866+
h.assert_true(
1867+
_handshakes(h, false, false),
1868+
"a context pinned to TLS 1.2 should complete a handshake")
1869+
1870+
h.assert_false(
1871+
_handshakes(h, true, false),
1872+
"disabling TLS 1.2 should leave no version to negotiate")
1873+
1874+
h.assert_true(
1875+
_handshakes(h, true, true),
1876+
"re-enabling TLS 1.2 should let the handshake complete again")
1877+
1878+
fun _handshakes(h: TestHelper, disable: Bool, reenable: Bool): Bool =>
1879+
"""
1880+
Whether a client and a server session from a TLS 1.2 only context complete
1881+
a handshake, having disabled and then re-enabled TLS 1.2 as asked.
1882+
"""
1883+
let auth = FileAuth(h.env.root)
1884+
let sslctx =
1885+
try
1886+
recover val
1887+
let ctx = SSLContext
1888+
.> set_authority(FilePath(auth, "assets/cert.pem"))?
1889+
.> set_cert(
1890+
FilePath(auth, "assets/cert.pem"),
1891+
FilePath(auth, "assets/key.pem"))?
1892+
.> set_client_verify(false)
1893+
.> set_server_verify(false)
1894+
.> set_min_proto_version(Tls1u2Version())?
1895+
.> set_max_proto_version(Tls1u2Version())?
1896+
if disable then ctx.allow_tls_v1_2(false) end
1897+
if reenable then ctx.allow_tls_v1_2(true) end
1898+
ctx
1899+
end
1900+
else
1901+
h.fail("ssl context setup failed")
1902+
return false
1903+
end
1904+
1905+
let client: SSL =
1906+
try
1907+
sslctx.client()?
1908+
else
1909+
h.fail("failed getting ssl client session")
1910+
return false
1911+
end
1912+
1913+
let server: SSL =
1914+
try
1915+
sslctx.server()?
1916+
else
1917+
client.dispose()
1918+
h.fail("failed getting ssl server session")
1919+
return false
1920+
end
1921+
1922+
let ready = _drive(client, server)
1923+
client.dispose()
1924+
server.dispose()
1925+
ready
1926+
1927+
fun _drive(client: SSL, server: SSL): Bool =>
1928+
"""
1929+
Whether both sides reach `SSLReady` when each side's outgoing bytes are
1930+
handed straight to the other.
1931+
1932+
A handshake with no protocol version left to negotiate fails rather than
1933+
stalls, so the loop ends on its own. The round cap is a backstop against a
1934+
session that never settles, not the expected way out.
1935+
"""
1936+
let max_rounds: USize = 20
1937+
var rounds: USize = 0
1938+
1939+
while
1940+
(client.state() is SSLHandshake) or (server.state() is SSLHandshake)
1941+
do
1942+
if rounds == max_rounds then return false end
1943+
rounds = rounds + 1
1944+
1945+
try
1946+
_TestSSLTransfer(client, server)?
1947+
_TestSSLTransfer(server, client)?
1948+
else
1949+
return false
1950+
end
1951+
end
1952+
1953+
(client.state() is SSLReady) and (server.state() is SSLReady)
1954+
18451955
class \nodoc\ iso _TestSSLContextClientAfterDispose is UnitTest
18461956
"""
18471957
`client` on a disposed context raises an error rather than handing a null

ssl/net/ssl.pony

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use @SSL_ctrl[ILong](
77
parg: Pointer[None])
88
use @SSL_new[Pointer[_SSL]](ctx: Pointer[_SSLContext] tag)
99
use @SSL_free[None](ssl: Pointer[_SSL] tag)
10-
use @SSL_set_verify[None](ssl: Pointer[_SSL], mode: I32, cb: Pointer[U8])
10+
use @SSL_set_verify[None](ssl: Pointer[_SSL], mode: I32, cb: Pointer[None])
1111
use @BIO_s_mem[Pointer[U8]]()
1212
use @BIO_new[Pointer[_BIO]](typ: Pointer[U8])
1313
use @BIO_free[I32](bio: Pointer[_BIO] tag)
@@ -18,10 +18,10 @@ use @SSL_do_handshake[I32](ssl: Pointer[_SSL])
1818
use @SSL_get0_alpn_selected[None](ssl: Pointer[_SSL] tag, data: Pointer[Pointer[U8] iso],
1919
len: Pointer[U32]) if "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl"
2020
use @SSL_pending[I32](ssl: Pointer[_SSL])
21-
use @SSL_read[I32](ssl: Pointer[_SSL], buf: Pointer[U8] tag, len: U32)
22-
use @SSL_write[I32](ssl: Pointer[_SSL], buf: Pointer[U8] tag, len: U32)
23-
use @BIO_read[I32](bio: Pointer[_BIO] tag, buf: Pointer[U8] tag, len: U32)
24-
use @BIO_write[I32](bio: Pointer[_BIO] tag, buf: Pointer[U8] tag, len: U32)
21+
use @SSL_read[I32](ssl: Pointer[_SSL], buf: Pointer[U8] tag, len: I32)
22+
use @SSL_write[I32](ssl: Pointer[_SSL], buf: Pointer[U8] tag, len: I32)
23+
use @BIO_read[I32](bio: Pointer[_BIO] tag, buf: Pointer[U8] tag, len: I32)
24+
use @BIO_write[I32](bio: Pointer[_BIO] tag, buf: Pointer[U8] tag, len: I32)
2525
use @SSL_get_error[I32](ssl: Pointer[_SSL], ret: I32)
2626
use @BIO_ctrl_pending[USize](bio: Pointer[_BIO] tag)
2727
use @SSL_has_pending[I32](ssl: Pointer[_SSL]) if "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x"
@@ -98,7 +98,7 @@ class SSL
9898
if _ssl.is_null() then error end
9999

100100
let mode = if verify then I32(3) else I32(0) end
101-
@SSL_set_verify(_ssl, mode, Pointer[U8])
101+
@SSL_set_verify(_ssl, mode, Pointer[None])
102102

103103
_input = @BIO_new(@BIO_s_mem())
104104
if _input.is_null() then error end
@@ -188,11 +188,11 @@ class SSL
188188
end
189189

190190
_read_buf.undefined(offset + len)
191-
@SSL_read(_ssl, _read_buf.cpointer(offset), len.u32())
191+
@SSL_read(_ssl, _read_buf.cpointer(offset), len.i32())
192192
else
193193
_read_buf.undefined(offset + len)
194194
let r =
195-
@SSL_read(_ssl, _read_buf.cpointer(offset), len.u32())
195+
@SSL_read(_ssl, _read_buf.cpointer(offset), len.i32())
196196

197197
if r <= 0 then
198198
match @SSL_get_error(_ssl, r)
@@ -249,7 +249,7 @@ class SSL
249249
if _state isnt SSLReady then error end
250250

251251
if data.size() > 0 then
252-
@SSL_write(_ssl, data.cpointer(), data.size().u32())
252+
@SSL_write(_ssl, data.cpointer(), data.size().i32())
253253
end
254254

255255
fun ref receive(data: ByteSeq) =>
@@ -259,7 +259,7 @@ class SSL
259259
"""
260260
if _ssl.is_null() then return end
261261

262-
@BIO_write(_input, data.cpointer(), data.size().u32())
262+
@BIO_write(_input, data.cpointer(), data.size().i32())
263263

264264
if _state is SSLHandshake then
265265
let r = @SSL_do_handshake(_ssl)
@@ -294,7 +294,7 @@ class SSL
294294
if len == 0 then error end
295295

296296
let buf = recover Array[U8] .> undefined(len) end
297-
@BIO_read(_output, buf.cpointer(), buf.size().u32())
297+
@BIO_read(_output, buf.cpointer(), buf.size().i32())
298298
buf
299299

300300
fun ref dispose() =>

0 commit comments

Comments
 (0)