Skip to content

Commit b169d76

Browse files
authored
feat: reduce read syscalls to improve performance (#4485)
1 parent b36c578 commit b169d76

20 files changed

+905
-40
lines changed

api/s2n.h

+59
Original file line numberDiff line numberDiff line change
@@ -1812,6 +1812,65 @@ S2N_API extern int s2n_connection_prefer_throughput(struct s2n_connection *conn)
18121812
*/
18131813
S2N_API extern int s2n_connection_prefer_low_latency(struct s2n_connection *conn);
18141814

1815+
/**
1816+
* Configure the connection to reduce potentially expensive calls to recv.
1817+
*
1818+
* If this setting is disabled, s2n-tls will call read twice for every TLS record,
1819+
* which can be expensive but ensures that s2n-tls will always attempt to read the
1820+
* exact number of bytes it requires. If this setting is enabled, s2n-tls will
1821+
* instead reduce the number of calls to read by attempting to read as much data
1822+
* as possible in each read call, storing the extra in the existing IO buffers.
1823+
* This may cause it to request more data than will ever actually be available.
1824+
*
1825+
* There is no additional memory cost of enabling this setting. It reuses the
1826+
* existing IO buffers.
1827+
*
1828+
* This setting is disabled by default. Depending on how your application detects
1829+
* data available for reading, buffering reads may break your event loop.
1830+
* In particular, note that:
1831+
*
1832+
* 1. File descriptor reads or calls to your custom s2n_recv_cb may request more
1833+
* data than is available. Reads must return partial data when available rather
1834+
* than blocking until all requested data is available.
1835+
*
1836+
* 2. s2n_negotiate may read and buffer application data records.
1837+
* You must call s2n_recv at least once after negotiation to ensure that you
1838+
* handle any buffered data.
1839+
*
1840+
* 3. s2n_recv may read and buffer more records than it parses and decrypts.
1841+
* You must call s2n_recv until it reports S2N_ERR_T_BLOCKED, rather than just
1842+
* until it reports S2N_SUCCESS.
1843+
*
1844+
* 4. s2n_peek reports available decrypted data. It does not report any data
1845+
* buffered by this feature.
1846+
*
1847+
* 5. s2n_connection_release_buffers will not release the input buffer if it
1848+
* contains buffered data.
1849+
*
1850+
* For example: if your event loop uses `poll`, you will receive a POLLIN event
1851+
* for your read file descriptor when new data is available. When you call s2n_recv
1852+
* to read that data, s2n-tls reads one or more TLS records from the file descriptor.
1853+
* If you stop calling s2n_recv before it reports S2N_ERR_T_BLOCKED, some of those
1854+
* records may remain in s2n-tls's read buffer. If you read part of a record,
1855+
* s2n_peek will report the remainder of that record as available. But if you don't
1856+
* read any of a record, it remains encrypted and is not reported by s2n_peek.
1857+
* And because the data is buffered in s2n-tls instead of in the file descriptor,
1858+
* another call to `poll` will NOT report any more data available. Your application
1859+
* may hang waiting for more data.
1860+
*
1861+
* @warning This feature cannot be enabled for a connection that will enable kTLS for receiving.
1862+
*
1863+
* @warning This feature may work with blocking IO, if used carefully. Your blocking
1864+
* IO must support partial reads (so MSG_WAITALL cannot be used). You will need
1865+
* to know how much data will eventually be available rather than relying on
1866+
* S2N_ERR_T_BLOCKED as noted in #3 above.
1867+
*
1868+
* @param conn The connection object being updated
1869+
* @param enabled Set to `true` to enable, `false` to disable.
1870+
* @returns S2N_SUCCESS on success. S2N_FAILURE on failure
1871+
*/
1872+
S2N_API extern int s2n_connection_set_recv_buffering(struct s2n_connection *conn, bool enabled);
1873+
18151874
/**
18161875
* Configure the connection to free IO buffers when they are not currently in use.
18171876
*

api/unstable/ktls.h

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
* The TLS kernel module currently doesn't support renegotiation.
3939
* - By default, you must negotiate TLS1.2. See s2n_config_ktls_enable_tls13
4040
* for the requirements to also support TLS1.3.
41+
* - You must not use s2n_connection_set_recv_buffering
4142
*/
4243

4344
/**

stuffer/s2n_stuffer.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ int s2n_stuffer_wipe_n(struct s2n_stuffer *stuffer, const uint32_t size)
215215

216216
bool s2n_stuffer_is_consumed(struct s2n_stuffer *stuffer)
217217
{
218-
return stuffer && (stuffer->read_cursor == stuffer->write_cursor);
218+
return stuffer && (stuffer->read_cursor == stuffer->write_cursor) && !stuffer->tainted;
219219
}
220220

221221
int s2n_stuffer_wipe(struct s2n_stuffer *stuffer)

tests/cbmc/proofs/s2n_stuffer_is_consumed/s2n_stuffer_is_consumed_harness.c

+6-3
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,13 @@ void s2n_stuffer_is_consumed_harness()
3333
save_byte_from_blob(&stuffer->blob, &old_byte_from_stuffer);
3434

3535
/* Operation under verification. */
36-
if (s2n_stuffer_is_consumed(stuffer)) {
37-
assert(stuffer->read_cursor == old_stuffer.write_cursor);
36+
bool result = s2n_stuffer_is_consumed(stuffer);
37+
if (old_stuffer.read_cursor != old_stuffer.write_cursor) {
38+
assert(result == false);
39+
} else if (old_stuffer.tainted) {
40+
assert(result == false);
3841
} else {
39-
assert(stuffer->read_cursor != old_stuffer.write_cursor);
42+
assert(result == true);
4043
}
4144

4245
/* Post-conditions. */

tests/unit/s2n_connection_size_test.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ int main(int argc, char **argv)
4545
}
4646

4747
/* Carefully consider any increases to this number. */
48-
const uint16_t max_connection_size = 4290;
48+
const uint16_t max_connection_size = 4350;
4949
const uint16_t min_connection_size = max_connection_size * 0.9;
5050

5151
size_t connection_size = sizeof(struct s2n_connection);

tests/unit/s2n_ktls_io_test.c

+41-2
Original file line numberDiff line numberDiff line change
@@ -1118,7 +1118,8 @@ int main(int argc, char **argv)
11181118
EXPECT_SUCCESS(s2n_ktls_read_full_record(conn, &record_type));
11191119
EXPECT_EQUAL(record_type, TLS_ALERT);
11201120

1121-
EXPECT_EQUAL(conn->in.blob.allocated, max_frag_len);
1121+
EXPECT_EQUAL(conn->buffer_in.blob.allocated, max_frag_len);
1122+
EXPECT_EQUAL(conn->in.blob.size, max_frag_len);
11221123
EXPECT_EQUAL(s2n_stuffer_data_available(&conn->in), max_frag_len);
11231124
uint8_t *read = s2n_stuffer_raw_read(&conn->in, max_frag_len);
11241125
EXPECT_BYTEARRAY_EQUAL(read, test_data, max_frag_len);
@@ -1152,7 +1153,8 @@ int main(int argc, char **argv)
11521153
/* Verify that conn->in reflects the correct size of the "record"
11531154
* read and doesn't just assume the maximum read size.
11541155
*/
1155-
EXPECT_EQUAL(conn->in.blob.allocated, max_frag_len);
1156+
EXPECT_EQUAL(conn->buffer_in.blob.allocated, max_frag_len);
1157+
EXPECT_EQUAL(conn->in.blob.size, small_frag_len);
11561158
EXPECT_EQUAL(s2n_stuffer_data_available(&conn->in), small_frag_len);
11571159
uint8_t *read = s2n_stuffer_raw_read(&conn->in, small_frag_len);
11581160
EXPECT_BYTEARRAY_EQUAL(read, test_data, small_frag_len);
@@ -1172,6 +1174,8 @@ int main(int argc, char **argv)
11721174
/* Write half the test data into conn->in */
11731175
const size_t offset = sizeof(test_data) / 2;
11741176
EXPECT_SUCCESS(s2n_stuffer_write_bytes(&conn->in, test_data, offset));
1177+
/* Resize conn->buffer_in to match conn->in */
1178+
EXPECT_SUCCESS(s2n_stuffer_resize(&conn->buffer_in, offset));
11751179

11761180
/* Write the other half into a new record */
11771181
size_t written = 0;
@@ -1201,6 +1205,41 @@ int main(int argc, char **argv)
12011205
read = s2n_stuffer_raw_read(&conn->in, offset_iovec.iov_len);
12021206
EXPECT_BYTEARRAY_EQUAL(read, offset_iovec.iov_base, offset_iovec.iov_len);
12031207
};
1208+
1209+
/* Test: Receive multiple records */
1210+
{
1211+
const size_t small_frag_len = 10;
1212+
EXPECT_TRUE(small_frag_len < max_frag_len);
1213+
EXPECT_TRUE(small_frag_len < sizeof(test_data));
1214+
struct iovec small_test_iovec = test_iovec;
1215+
small_test_iovec.iov_len = small_frag_len;
1216+
1217+
DEFER_CLEANUP(struct s2n_connection *conn = s2n_connection_new(S2N_CLIENT),
1218+
s2n_connection_ptr_free);
1219+
EXPECT_NOT_NULL(conn);
1220+
1221+
DEFER_CLEANUP(struct s2n_test_ktls_io_stuffer_pair pair = { 0 },
1222+
s2n_ktls_io_stuffer_pair_free);
1223+
EXPECT_OK(s2n_test_init_ktls_io_stuffer(conn, conn, &pair));
1224+
struct s2n_test_ktls_io_stuffer *ctx = &pair.client_in;
1225+
1226+
for (size_t i = 0; i < 100; i++) {
1227+
size_t written = 0;
1228+
EXPECT_OK(s2n_ktls_sendmsg(ctx, TLS_ALERT, &small_test_iovec, 1, &blocked, &written));
1229+
EXPECT_EQUAL(written, small_frag_len);
1230+
1231+
uint8_t record_type = 0;
1232+
EXPECT_SUCCESS(s2n_ktls_read_full_record(conn, &record_type));
1233+
EXPECT_EQUAL(record_type, TLS_ALERT);
1234+
EXPECT_EQUAL(s2n_stuffer_data_available(&conn->in), written);
1235+
uint8_t *read = s2n_stuffer_raw_read(&conn->in, small_frag_len);
1236+
EXPECT_BYTEARRAY_EQUAL(read, test_data, small_frag_len);
1237+
1238+
EXPECT_OK(s2n_record_wipe(conn));
1239+
size_t space_remaining = s2n_stuffer_space_remaining(&conn->buffer_in);
1240+
EXPECT_EQUAL(space_remaining, max_frag_len);
1241+
}
1242+
};
12041243
};
12051244

12061245
/* Test: key encryption limit tracked */

tests/unit/s2n_ktls_test.c

+16
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,22 @@ int main(int argc, char **argv)
387387
EXPECT_SUCCESS(s2n_connection_ktls_enable_recv(server_conn));
388388
};
389389

390+
/* Fail if buffer_in contains any data.
391+
* A connection that will enable ktls needs to disable recv_greedy
392+
*/
393+
{
394+
DEFER_CLEANUP(struct s2n_connection *server_conn = s2n_connection_new(S2N_SERVER),
395+
s2n_connection_ptr_free);
396+
EXPECT_OK(s2n_test_configure_connection_for_ktls(server_conn));
397+
398+
EXPECT_SUCCESS(s2n_stuffer_write_uint8(&server_conn->buffer_in, 1));
399+
EXPECT_FAILURE_WITH_ERRNO(s2n_connection_ktls_enable_recv(server_conn),
400+
S2N_ERR_KTLS_UNSUPPORTED_CONN);
401+
402+
EXPECT_SUCCESS(s2n_stuffer_skip_read(&server_conn->buffer_in, 1));
403+
EXPECT_SUCCESS(s2n_connection_ktls_enable_recv(server_conn));
404+
};
405+
390406
/* Fail if not using managed IO for send */
391407
{
392408
DEFER_CLEANUP(struct s2n_connection *server_conn = s2n_connection_new(S2N_SERVER),

tests/unit/s2n_quic_support_io_test.c

+81
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,87 @@ int main(int argc, char **argv)
244244
EXPECT_SUCCESS(s2n_stuffer_free(&stuffer));
245245
EXPECT_SUCCESS(s2n_connection_free(conn));
246246
};
247+
248+
/* Succeeds for a handshake message larger than the input buffer */
249+
{
250+
DEFER_CLEANUP(struct s2n_connection *conn = s2n_connection_new(S2N_CLIENT),
251+
s2n_connection_ptr_free);
252+
EXPECT_NOT_NULL(conn);
253+
254+
DEFER_CLEANUP(struct s2n_stuffer stuffer = { 0 }, s2n_stuffer_free);
255+
EXPECT_SUCCESS(s2n_stuffer_growable_alloc(&stuffer, 0));
256+
EXPECT_SUCCESS(s2n_connection_set_io_stuffers(&stuffer, &stuffer, conn));
257+
258+
uint8_t actual_message_type = 0;
259+
260+
/* Read a small message to initialize the input buffer */
261+
const size_t small_message_size = 10;
262+
EXPECT_SUCCESS(s2n_stuffer_write_uint8(&stuffer, 7));
263+
EXPECT_SUCCESS(s2n_stuffer_write_uint24(&stuffer, small_message_size));
264+
EXPECT_SUCCESS(s2n_stuffer_skip_write(&stuffer, small_message_size));
265+
EXPECT_OK(s2n_quic_read_handshake_message(conn, &actual_message_type));
266+
EXPECT_EQUAL(s2n_stuffer_data_available(&conn->in), small_message_size);
267+
268+
EXPECT_SUCCESS(s2n_stuffer_wipe(&conn->handshake.io));
269+
EXPECT_OK(s2n_record_wipe(conn));
270+
const size_t max_buffer_size = s2n_stuffer_space_remaining(&conn->buffer_in);
271+
EXPECT_TRUE(max_buffer_size > small_message_size);
272+
273+
/* Read a large message to force the input buffer to resize */
274+
const size_t large_message_size = max_buffer_size + 10;
275+
EXPECT_SUCCESS(s2n_stuffer_write_uint8(&stuffer, 7));
276+
EXPECT_SUCCESS(s2n_stuffer_write_uint24(&stuffer, large_message_size));
277+
EXPECT_SUCCESS(s2n_stuffer_skip_write(&stuffer, large_message_size));
278+
EXPECT_OK(s2n_quic_read_handshake_message(conn, &actual_message_type));
279+
EXPECT_EQUAL(s2n_stuffer_data_available(&conn->in), large_message_size);
280+
281+
EXPECT_SUCCESS(s2n_stuffer_wipe(&conn->handshake.io));
282+
EXPECT_OK(s2n_record_wipe(conn));
283+
const size_t resized_buffer_size = s2n_stuffer_space_remaining(&conn->buffer_in);
284+
EXPECT_TRUE(resized_buffer_size >= large_message_size);
285+
286+
/* Read another message to check that the resize doesn't prevent future reads */
287+
EXPECT_SUCCESS(s2n_stuffer_write_uint8(&stuffer, 7));
288+
EXPECT_SUCCESS(s2n_stuffer_write_uint24(&stuffer, TEST_DATA_SIZE));
289+
EXPECT_SUCCESS(s2n_stuffer_write_bytes(&stuffer, TEST_DATA, TEST_DATA_SIZE));
290+
EXPECT_OK(s2n_quic_read_handshake_message(conn, &actual_message_type));
291+
EXPECT_EQUAL(s2n_stuffer_data_available(&conn->in), TEST_DATA_SIZE);
292+
EXPECT_BYTEARRAY_EQUAL(s2n_stuffer_raw_read(&conn->in, TEST_DATA_SIZE),
293+
TEST_DATA, sizeof(TEST_DATA));
294+
};
295+
296+
/* Succeeds for multiple messages */
297+
{
298+
DEFER_CLEANUP(struct s2n_connection *conn = s2n_connection_new(S2N_CLIENT),
299+
s2n_connection_ptr_free);
300+
EXPECT_NOT_NULL(conn);
301+
302+
DEFER_CLEANUP(struct s2n_stuffer stuffer = { 0 }, s2n_stuffer_free);
303+
EXPECT_SUCCESS(s2n_stuffer_growable_alloc(&stuffer, 0));
304+
EXPECT_SUCCESS(s2n_connection_set_io_stuffers(&stuffer, &stuffer, conn));
305+
306+
uint8_t actual_message_type = 0;
307+
size_t expected_buffer_size = 0;
308+
for (size_t i = 0; i < 100; i++) {
309+
EXPECT_SUCCESS(s2n_stuffer_write_uint8(&stuffer, 7));
310+
EXPECT_SUCCESS(s2n_stuffer_write_uint24(&stuffer, TEST_DATA_SIZE));
311+
EXPECT_SUCCESS(s2n_stuffer_write_bytes(&stuffer, TEST_DATA, TEST_DATA_SIZE));
312+
EXPECT_OK(s2n_quic_read_handshake_message(conn, &actual_message_type));
313+
EXPECT_EQUAL(s2n_stuffer_data_available(&conn->in), TEST_DATA_SIZE);
314+
EXPECT_BYTEARRAY_EQUAL(s2n_stuffer_raw_read(&conn->in, TEST_DATA_SIZE),
315+
TEST_DATA, sizeof(TEST_DATA));
316+
317+
EXPECT_SUCCESS(s2n_stuffer_wipe(&conn->handshake.io));
318+
EXPECT_OK(s2n_record_wipe(conn));
319+
320+
/* Ensure buffer size stays constant */
321+
const size_t buffer_size = s2n_stuffer_space_remaining(&conn->buffer_in);
322+
if (i == 0) {
323+
expected_buffer_size = buffer_size;
324+
}
325+
EXPECT_EQUAL(expected_buffer_size, buffer_size);
326+
}
327+
};
247328
};
248329

249330
/* Functional Tests */

0 commit comments

Comments
 (0)