LCOV - code coverage report
Current view: top level - tests/functional - test_transport_resilience.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 100.0 % 283 283
Test Date: 2026-04-20 19:54:22 Functions: 100.0 % 21 21

            Line data    Source code
       1              : /**
       2              :  * @file test_transport_resilience.c
       3              :  * @brief TEST-82 — functional coverage for transport.c error paths.
       4              :  *
       5              :  * Drives the real `transport_*` helpers over the mock socket while
       6              :  * injecting the faults listed in US-31:
       7              :  *
       8              :  *   1. connect() refused          → bootstrap fails clean with errno visible.
       9              :  *   2. Partial send()             → transport_send loops to completion.
      10              :  *   3. Partial recv()             → transport_recv reassembles the frame.
      11              :  *   4. EINTR mid-send             → silent retry.
      12              :  *   5. EAGAIN mid-send            → silent retry (blocking socket + signal).
      13              :  *   6. Mid-RPC EOF                → transport_recv surfaces -1, explicit
      14              :  *                                    close + reconnect brings the channel
      15              :  *                                    back on the next poll.
      16              :  *   7. SIGPIPE                    → ignored at process level; write after
      17              :  *                                    peer close surfaces EPIPE (-1) rather
      18              :  *                                    than killing the test binary.
      19              :  *
      20              :  * All injection is mock-side (see `mock_socket_*` helpers).  No changes to
      21              :  * production transport.c beyond the EINTR→EAGAIN widening that US-31
      22              :  * itself mandated.
      23              :  */
      24              : 
      25              : #include "test_helpers.h"
      26              : 
      27              : #include "mock_socket.h"
      28              : #include "mock_tel_server.h"
      29              : 
      30              : #include "api_call.h"
      31              : #include "mtproto_rpc.h"
      32              : #include "mtproto_session.h"
      33              : #include "transport.h"
      34              : #include "app/session_store.h"
      35              : #include "tl_serial.h"
      36              : 
      37              : #include <errno.h>
      38              : #include <signal.h>
      39              : #include <stdint.h>
      40              : #include <stdio.h>
      41              : #include <stdlib.h>
      42              : #include <string.h>
      43              : #include <unistd.h>
      44              : 
      45              : /* ---- TL CRCs local to this suite ---- */
      46              : #define CRC_ping 0x7abe77ecU
      47              : #define CRC_pong 0x347773c5U
      48              : 
      49              : /* ================================================================ */
      50              : /* Boilerplate                                                       */
      51              : /* ================================================================ */
      52              : 
      53           32 : static void with_tmp_home(const char *tag) {
      54              :     char tmp[256];
      55           32 :     snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-ft-resilience-%s", tag);
      56              :     char bin[512];
      57           32 :     snprintf(bin, sizeof(bin), "%s/.config/tg-cli/session.bin", tmp);
      58           32 :     (void)unlink(bin);
      59           32 :     setenv("HOME", tmp, 1);
      60           32 : }
      61              : 
      62              : /** Canned ping responder — emits pong echoing the ping_id. */
      63           12 : static void on_ping(MtRpcContext *ctx) {
      64           12 :     if (ctx->req_body_len < 4 + 8) return;
      65           12 :     uint64_t ping_id = 0;
      66          108 :     for (int i = 0; i < 8; ++i) {
      67           96 :         ping_id |= ((uint64_t)ctx->req_body[4 + i]) << (i * 8);
      68              :     }
      69           12 :     TlWriter w; tl_writer_init(&w);
      70           12 :     tl_write_uint32(&w, CRC_pong);
      71           12 :     tl_write_uint64(&w, ctx->req_msg_id);
      72           12 :     tl_write_uint64(&w, ping_id);
      73           12 :     mt_server_reply_result(ctx, w.data, w.len);
      74           12 :     tl_writer_free(&w);
      75              : }
      76              : 
      77           12 : static void send_ping(MtProtoSession *s, Transport *t, uint64_t ping_id) {
      78           12 :     TlWriter req; tl_writer_init(&req);
      79           12 :     tl_write_uint32(&req, CRC_ping);
      80           12 :     tl_write_uint64(&req, ping_id);
      81           12 :     ASSERT(rpc_send_encrypted(s, t, req.data, req.len, 1) == 0,
      82              :            "send ping");
      83           12 :     tl_writer_free(&req);
      84              : }
      85              : 
      86           10 : static void expect_pong(MtProtoSession *s, Transport *t, uint64_t ping_id) {
      87              :     uint8_t reply[1024];
      88           10 :     size_t reply_len = 0;
      89           10 :     ASSERT(rpc_recv_encrypted(s, t, reply, sizeof(reply), &reply_len) == 0,
      90              :            "recv pong");
      91           10 :     TlReader r = tl_reader_init(reply, reply_len);
      92           10 :     ASSERT(tl_read_uint32(&r) == 0xf35c6d01U, "rpc_result outer");
      93           10 :     tl_read_uint64(&r);  /* req_msg_id */
      94           10 :     ASSERT(tl_read_uint32(&r) == CRC_pong, "inner is pong");
      95           10 :     tl_read_uint64(&r);  /* echoed msg_id */
      96           10 :     ASSERT(tl_read_uint64(&r) == ping_id, "ping_id round-trips");
      97              : }
      98              : 
      99              : /* ================================================================ */
     100              : /* 1. connect() refused                                              */
     101              : /* ================================================================ */
     102              : 
     103              : /* Refuse-connect is a persistent mode — every reconnect attempt in a
     104              :  * retry loop keeps failing.  We assert the distinct errno and a clean
     105              :  * failure (transport.fd stays -1). */
     106            2 : static void test_connect_refused_is_fatal_with_clean_exit(void) {
     107            2 :     with_tmp_home("conn-refused");
     108            2 :     mt_server_init(); mt_server_reset();
     109            2 :     mock_socket_refuse_connect();
     110              : 
     111              :     Transport t;
     112            2 :     transport_init(&t);
     113              : 
     114            2 :     errno = 0;
     115            2 :     int rc = transport_connect(&t, "127.0.0.1", 443);
     116            2 :     ASSERT(rc == -1,
     117              :            "transport_connect returns -1 when peer refuses");
     118            2 :     ASSERT(errno == ECONNREFUSED,
     119              :            "errno preserved as ECONNREFUSED across the failed call");
     120            2 :     ASSERT(t.fd == -1,
     121              :            "transport fd reset to -1 on failure (no leaked socket)");
     122            2 :     ASSERT(t.connected == 0,
     123              :            "transport.connected stays 0 after failed connect");
     124              : 
     125              :     /* A second attempt still fails — refuse is persistent until reset. */
     126            2 :     errno = 0;
     127            2 :     rc = transport_connect(&t, "127.0.0.1", 443);
     128            2 :     ASSERT(rc == -1, "second attempt still refused");
     129            2 :     ASSERT(errno == ECONNREFUSED, "errno still ECONNREFUSED on retry");
     130              : 
     131              :     /* Close on an already-failed transport must be a safe no-op. */
     132            2 :     transport_close(&t);
     133            2 :     ASSERT(t.fd == -1, "close on unconnected transport leaves fd at -1");
     134              : 
     135            2 :     mt_server_reset();
     136              : }
     137              : 
     138              : /* ================================================================ */
     139              : /* 2. Partial send() retries to completion                           */
     140              : /* ================================================================ */
     141              : 
     142              : /* With a 16-byte send cap, every sys_socket_send returns at most 16 bytes.
     143              :  * The full encrypted ping frame is ~72 bytes, so transport_send has to
     144              :  * loop multiple times for the payload portion.  The server-side reply is
     145              :  * still assembled correctly. */
     146            2 : static void test_partial_send_retries_to_completion(void) {
     147            2 :     with_tmp_home("partial-send");
     148            2 :     mt_server_init(); mt_server_reset();
     149            2 :     mt_server_expect(CRC_ping, on_ping, NULL);
     150              : 
     151            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     152            2 :     MtProtoSession s; mtproto_session_init(&s);
     153            2 :     int dc = 0;
     154            2 :     ASSERT(session_store_load(&s, &dc) == 0, "load session");
     155              : 
     156            2 :     Transport t; transport_init(&t);
     157            2 :     ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
     158              : 
     159              :     /* Now turn on fragmentation — the connect's own marker byte is
     160              :      * already gone so the cap applies only to real payload sends. */
     161            2 :     mock_socket_set_send_fragment(16);
     162              : 
     163            2 :     send_ping(&s, &t, 0xDEADBEEF12345678ULL);
     164            2 :     expect_pong(&s, &t, 0xDEADBEEF12345678ULL);
     165              : 
     166            2 :     ASSERT(mt_server_rpc_call_count() == 1,
     167              :            "exactly one logical RPC despite many partial writes");
     168              : 
     169            2 :     transport_close(&t);
     170            2 :     mt_server_reset();
     171              : }
     172              : 
     173              : /* ================================================================ */
     174              : /* 3. Partial recv() reassembles the frame                           */
     175              : /* ================================================================ */
     176              : 
     177              : /* With a 16-byte recv cap the encrypted pong comes back across many
     178              :  * sys_socket_recv calls.  transport_recv must continue reading until
     179              :  * the announced abridged length is fully drained. */
     180            2 : static void test_partial_recv_reassembles_frame(void) {
     181            2 :     with_tmp_home("partial-recv");
     182            2 :     mt_server_init(); mt_server_reset();
     183            2 :     mt_server_expect(CRC_ping, on_ping, NULL);
     184              : 
     185            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     186            2 :     MtProtoSession s; mtproto_session_init(&s);
     187            2 :     int dc = 0;
     188            2 :     ASSERT(session_store_load(&s, &dc) == 0, "load");
     189              : 
     190            2 :     Transport t; transport_init(&t);
     191            2 :     ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
     192              : 
     193            2 :     send_ping(&s, &t, 0xCAFEBABE00000001ULL);
     194              : 
     195              :     /* Arm fragmentation only for the reply path. */
     196            2 :     mock_socket_set_recv_fragment(16);
     197              : 
     198            2 :     expect_pong(&s, &t, 0xCAFEBABE00000001ULL);
     199              : 
     200            2 :     transport_close(&t);
     201            2 :     mt_server_reset();
     202              : }
     203              : 
     204              : /* ================================================================ */
     205              : /* 4. EINTR is silently retried                                      */
     206              : /* ================================================================ */
     207              : 
     208            2 : static void test_eintr_is_silent_retry(void) {
     209            2 :     with_tmp_home("eintr");
     210            2 :     mt_server_init(); mt_server_reset();
     211            2 :     mt_server_expect(CRC_ping, on_ping, NULL);
     212              : 
     213            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     214            2 :     MtProtoSession s; mtproto_session_init(&s);
     215            2 :     int dc = 0;
     216            2 :     ASSERT(session_store_load(&s, &dc) == 0, "load");
     217              : 
     218            2 :     Transport t; transport_init(&t);
     219            2 :     ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
     220              : 
     221              :     /* Inject one EINTR on the next send AND on the next recv to exercise
     222              :      * both retry loops. */
     223            2 :     mock_socket_inject_eintr_next_send();
     224            2 :     mock_socket_inject_eintr_next_recv();
     225              : 
     226            2 :     send_ping(&s, &t, 0x0000EE1200000001ULL);
     227            2 :     expect_pong(&s, &t, 0x0000EE1200000001ULL);
     228              : 
     229            2 :     transport_close(&t);
     230            2 :     mt_server_reset();
     231              : }
     232              : 
     233              : /* ================================================================ */
     234              : /* 5. EAGAIN is silently retried                                     */
     235              : /* ================================================================ */
     236              : 
     237            2 : static void test_eagain_is_silent_retry(void) {
     238            2 :     with_tmp_home("eagain");
     239            2 :     mt_server_init(); mt_server_reset();
     240            2 :     mt_server_expect(CRC_ping, on_ping, NULL);
     241              : 
     242            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     243            2 :     MtProtoSession s; mtproto_session_init(&s);
     244            2 :     int dc = 0;
     245            2 :     ASSERT(session_store_load(&s, &dc) == 0, "load");
     246              : 
     247            2 :     Transport t; transport_init(&t);
     248            2 :     ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
     249              : 
     250            2 :     mock_socket_inject_eagain_next_send();
     251            2 :     mock_socket_inject_eagain_next_recv();
     252              : 
     253            2 :     send_ping(&s, &t, 0x0A0A0A0A11111111ULL);
     254            2 :     expect_pong(&s, &t, 0x0A0A0A0A11111111ULL);
     255              : 
     256            2 :     transport_close(&t);
     257            2 :     mt_server_reset();
     258              : }
     259              : 
     260              : /* ================================================================ */
     261              : /* 6. Mid-RPC EOF: next poll reconnects and resumes                  */
     262              : /* ================================================================ */
     263              : 
     264              : /* Send ping #1 → force EOF on the recv so the RPC fails.
     265              :  * Close, rearm the mt_server reconnect detector, connect again, and
     266              :  * verify a second ping round-trips cleanly.  That mirrors what a
     267              :  * polling watch/upload loop does: treat a transport failure as a
     268              :  * transient and reopen the channel on the next iteration. */
     269            2 : static void test_mid_rpc_disconnect_reconnects(void) {
     270            2 :     with_tmp_home("mid-rpc-eof");
     271            2 :     mt_server_init(); mt_server_reset();
     272            2 :     mt_server_expect(CRC_ping, on_ping, NULL);
     273              : 
     274            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     275            2 :     MtProtoSession s; mtproto_session_init(&s);
     276            2 :     int dc = 0;
     277            2 :     ASSERT(session_store_load(&s, &dc) == 0, "load");
     278              : 
     279            2 :     Transport t; transport_init(&t);
     280            2 :     ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect 1");
     281              : 
     282              :     /* Queue the EOF before we even send — the reply that on_ping would
     283              :      * normally append gets shadowed by the kill-on-next knob. */
     284            2 :     mock_socket_kill_on_next_recv();
     285            2 :     send_ping(&s, &t, 0x1111222233334444ULL);
     286              : 
     287              :     uint8_t reply[512];
     288            2 :     size_t reply_len = 0;
     289            2 :     int rc = rpc_recv_encrypted(&s, &t, reply, sizeof(reply), &reply_len);
     290            2 :     ASSERT(rc == -1,
     291              :            "rpc_recv_encrypted surfaces -1 on mid-RPC EOF");
     292              : 
     293              :     /* Simulate the poll-loop reconnect: close, rearm the mt_server
     294              :      * reconnect detector (so the 0xEF on the new socket is parsed as a
     295              :      * fresh connection), connect again. */
     296            2 :     transport_close(&t);
     297            2 :     ASSERT(t.fd == -1, "transport fd cleared after close");
     298              : 
     299            2 :     mt_server_arm_reconnect();
     300            2 :     transport_init(&t);
     301            2 :     ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "reconnect");
     302              : 
     303              :     /* Second ping over the fresh socket — succeeds end-to-end. */
     304            2 :     send_ping(&s, &t, 0x5555666677778888ULL);
     305            2 :     expect_pong(&s, &t, 0x5555666677778888ULL);
     306              : 
     307            2 :     transport_close(&t);
     308            2 :     mt_server_reset();
     309              : }
     310              : 
     311              : /* ================================================================ */
     312              : /* 7. SIGPIPE is ignored                                             */
     313              : /* ================================================================ */
     314              : 
     315              : /* The real binaries call `signal(SIGPIPE, SIG_IGN)` inside their
     316              :  * entry points.  We replicate that same guard here and assert that a
     317              :  * write-after-peer-close surfaces EPIPE from sys_socket_send rather
     318              :  * than killing the process with signal 13.
     319              :  *
     320              :  * Because we use the mock socket there is no real kernel SIGPIPE to
     321              :  * receive — instead the test validates that production transport_send
     322              :  * treats the -1/EPIPE path as a normal -1 return and does not loop
     323              :  * forever.  Combined with the SIG_IGN guard this covers the same
     324              :  * contract on a real OS.
     325              :  */
     326            2 : static void test_sigpipe_is_ignored(void) {
     327            2 :     with_tmp_home("sigpipe");
     328            2 :     mt_server_init(); mt_server_reset();
     329              : 
     330              :     /* Install the same handler the production binaries do. */
     331              :     struct sigaction sa;
     332            2 :     memset(&sa, 0, sizeof(sa));
     333            2 :     sa.sa_handler = SIG_IGN;
     334            2 :     sigemptyset(&sa.sa_mask);
     335            2 :     ASSERT(sigaction(SIGPIPE, &sa, NULL) == 0,
     336              :            "installed SIG_IGN for SIGPIPE");
     337              : 
     338              :     /* Verify the disposition took — subsequent writes on a broken
     339              :      * pipe do not terminate the process. */
     340              :     struct sigaction current;
     341            2 :     ASSERT(sigaction(SIGPIPE, NULL, &current) == 0, "query disposition");
     342            2 :     ASSERT(current.sa_handler == SIG_IGN,
     343              :            "SIGPIPE disposition is SIG_IGN");
     344              : 
     345            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     346            2 :     MtProtoSession s; mtproto_session_init(&s);
     347            2 :     int dc = 0;
     348            2 :     ASSERT(session_store_load(&s, &dc) == 0, "load");
     349              : 
     350            2 :     Transport t; transport_init(&t);
     351            2 :     ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
     352              : 
     353              :     /* Prime the NEXT payload-carrying send to fail.  Connect already
     354              :      * consumed call #1 (the 0xEF marker), so we target call #2 — the
     355              :      * length-prefix of the next transport_send.  Setting errno=EPIPE
     356              :      * emulates a real broken-pipe return without relying on the mock
     357              :      * to propagate it. */
     358            2 :     mock_socket_fail_send_at(2);
     359            2 :     errno = EPIPE;
     360              : 
     361            2 :     uint8_t payload[4] = {0xAA, 0xBB, 0xCC, 0xDD};
     362            2 :     int rc = transport_send(&t, payload, sizeof(payload));
     363            2 :     ASSERT(rc == -1,
     364              :            "transport_send surfaces broken-pipe error without aborting");
     365              : 
     366              :     /* The process survived — just close the transport and move on.
     367              :      * Touching stderr here would normally trigger SIGPIPE on a real
     368              :      * head -c 100 consumer; we simulate that by writing to /dev/null. */
     369            2 :     FILE *f = fopen("/dev/null", "w");
     370            2 :     ASSERT(f != NULL, "/dev/null open");
     371            2 :     fprintf(f, "tg-cli watch output line\n");
     372            2 :     fclose(f);
     373              : 
     374            2 :     transport_close(&t);
     375              : 
     376              :     /* Restore default so other tests run unaffected. */
     377            2 :     sa.sa_handler = SIG_DFL;
     378            2 :     sigaction(SIGPIPE, &sa, NULL);
     379            2 :     mt_server_reset();
     380              : }
     381              : 
     382              : /* ================================================================ */
     383              : /* 8. Additional fault-path corners                                  */
     384              : /* ================================================================ */
     385              : 
     386              : /* Unaligned payload is rejected at the transport layer — the abridged
     387              :  * length prefix is in 4-byte units, so a non-multiple length would
     388              :  * desync the stream. */
     389            2 : static void test_unaligned_payload_rejected(void) {
     390            2 :     with_tmp_home("unaligned");
     391            2 :     mt_server_init(); mt_server_reset();
     392              : 
     393            2 :     Transport t; transport_init(&t);
     394            2 :     ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
     395              : 
     396            2 :     uint8_t payload[3] = {0x01, 0x02, 0x03};
     397            2 :     int rc = transport_send(&t, payload, sizeof(payload));
     398            2 :     ASSERT(rc == -1,
     399              :            "transport_send rejects payload not multiple of 4 bytes");
     400              : 
     401            2 :     transport_close(&t);
     402            2 :     mt_server_reset();
     403              : }
     404              : 
     405              : /* sys_socket_create failure: every subsequent connect attempt fails
     406              :  * before even reaching sys_socket_connect.  Exercises the socket()
     407              :  * error branch that the refuse-connect test doesn't. */
     408            2 : static void test_socket_create_failure(void) {
     409            2 :     with_tmp_home("sock-create");
     410            2 :     mt_server_init(); mt_server_reset();
     411            2 :     mock_socket_fail_create();
     412              : 
     413            2 :     Transport t; transport_init(&t);
     414            2 :     int rc = transport_connect(&t, "127.0.0.1", 443);
     415            2 :     ASSERT(rc == -1,
     416              :            "transport_connect fails when sys_socket_create returns -1");
     417            2 :     ASSERT(t.fd == -1, "fd stays -1 on socket() failure");
     418              : 
     419            2 :     mt_server_reset();
     420              : }
     421              : 
     422              : /* Abridged marker send fails in transport_connect — covers the cleanup
     423              :  * branch that closes the socket and resets fd. */
     424            2 : static void test_marker_send_failure_closes_socket(void) {
     425            2 :     with_tmp_home("marker-fail");
     426            2 :     mt_server_init(); mt_server_reset();
     427              : 
     428              :     /* Marker send is sys_socket_send call #1 — prime it to fail. */
     429            2 :     mock_socket_fail_send_at(1);
     430              : 
     431            2 :     Transport t; transport_init(&t);
     432            2 :     int rc = transport_connect(&t, "127.0.0.1", 443);
     433            2 :     ASSERT(rc == -1,
     434              :            "transport_connect fails when marker send fails");
     435            2 :     ASSERT(t.fd == -1,
     436              :            "fd reset to -1 after marker-send failure");
     437            2 :     ASSERT(t.connected == 0,
     438              :            "connected flag stays 0 after marker-send failure");
     439              : 
     440            2 :     mt_server_reset();
     441              : }
     442              : 
     443              : /* Mid-send payload failure: after a successful length prefix the
     444              :  * payload chunk send fails.  Exercises transport_send's "sent <= 0"
     445              :  * branch (line ~108-109 in transport.c). */
     446            2 : static void test_payload_send_failure(void) {
     447            2 :     with_tmp_home("payload-send-fail");
     448            2 :     mt_server_init(); mt_server_reset();
     449              : 
     450            2 :     Transport t; transport_init(&t);
     451            2 :     ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
     452              : 
     453              :     /* Call #1 = marker (done).  Call #2 = prefix (let it pass).
     454              :      * Call #3 = payload → force failure. */
     455            2 :     mock_socket_fail_send_at(3);
     456              : 
     457            2 :     uint8_t payload[8] = {1,2,3,4,5,6,7,8};
     458            2 :     int rc = transport_send(&t, payload, sizeof(payload));
     459            2 :     ASSERT(rc == -1,
     460              :            "transport_send surfaces -1 when payload chunk fails");
     461              : 
     462            2 :     transport_close(&t);
     463            2 :     mt_server_reset();
     464              : }
     465              : 
     466              : /* Large wire_len that exceeds the caller's buffer is rejected by
     467              :  * transport_recv — covers the payload_len > max_len branch. */
     468            2 : static void test_recv_frame_too_large_rejected(void) {
     469            2 :     with_tmp_home("frame-too-large");
     470            2 :     mt_server_init(); mt_server_reset();
     471              : 
     472            2 :     Transport t; transport_init(&t);
     473            2 :     ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
     474              : 
     475              :     /* Arm a response whose abridged length prefix decodes to a huge
     476              :      * payload (wire_len = 0x00FFFFFF → ~67 MB) so transport_recv's
     477              :      * max_len check triggers without us sending 67 MB of mock data. */
     478            2 :     uint8_t giant[4] = { 0x7F, 0xFF, 0xFF, 0xFF };
     479            2 :     mock_socket_set_response(giant, sizeof(giant));
     480              : 
     481              :     uint8_t buf[256];
     482            2 :     size_t out_len = 0;
     483            2 :     int rc = transport_recv(&t, buf, sizeof(buf), &out_len);
     484            2 :     ASSERT(rc == -1,
     485              :            "transport_recv rejects a frame larger than the caller buffer");
     486              : 
     487            2 :     transport_close(&t);
     488            2 :     mt_server_reset();
     489              : }
     490              : 
     491              : /* Recv length-prefix fails partway through the 3-byte extended prefix.
     492              :  * Covers the secondary prefix-recv failure branch. */
     493            2 : static void test_extended_prefix_recv_failure(void) {
     494            2 :     with_tmp_home("ext-prefix-fail");
     495            2 :     mt_server_init(); mt_server_reset();
     496              : 
     497            2 :     Transport t; transport_init(&t);
     498            2 :     ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
     499              : 
     500              :     /* Queue only the 0x7F marker byte; the 3-byte continuation is
     501              :      * missing, so sys_socket_recv returns 0 on the follow-up call. */
     502            2 :     uint8_t only_marker = 0x7F;
     503            2 :     mock_socket_set_response(&only_marker, 1);
     504              : 
     505              :     uint8_t buf[64];
     506            2 :     size_t out_len = 0;
     507            2 :     int rc = transport_recv(&t, buf, sizeof(buf), &out_len);
     508            2 :     ASSERT(rc == -1,
     509              :            "transport_recv fails when 3-byte extended prefix is truncated");
     510              : 
     511            2 :     transport_close(&t);
     512            2 :     mt_server_reset();
     513              : }
     514              : 
     515              : /* 4-byte extended length prefix fails to send.  Payload size must be
     516              :  * at least 0x7F * 4 = 508 bytes for transport_send to pick the 4-byte
     517              :  * prefix branch.  Call #1 = marker (connect), call #2 = 4-byte prefix
     518              :  * (primed to fail). */
     519            2 : static void test_extended_prefix_send_failure(void) {
     520            2 :     with_tmp_home("ext-prefix-send-fail");
     521            2 :     mt_server_init(); mt_server_reset();
     522              : 
     523            2 :     Transport t; transport_init(&t);
     524            2 :     ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
     525              : 
     526            2 :     mock_socket_fail_send_at(2);
     527              : 
     528              :     /* 508 bytes = 0x7F * 4 — the minimum that triggers the extended
     529              :      * prefix branch.  Contents don't matter, mock fails the send. */
     530            2 :     uint8_t payload[508] = {0};
     531            2 :     int rc = transport_send(&t, payload, sizeof(payload));
     532            2 :     ASSERT(rc == -1,
     533              :            "transport_send fails when 4-byte length prefix send fails");
     534              : 
     535            2 :     transport_close(&t);
     536            2 :     mt_server_reset();
     537              : }
     538              : 
     539              : /* Payload recv fails partway through: arm the first-byte length prefix
     540              :  * to read cleanly, then fail the follow-up payload read.  Covers the
     541              :  * "r <= 0" branch of the payload-read loop. */
     542            2 : static void test_payload_recv_failure_mid_frame(void) {
     543            2 :     with_tmp_home("payload-recv-fail");
     544            2 :     mt_server_init(); mt_server_reset();
     545              : 
     546            2 :     Transport t; transport_init(&t);
     547            2 :     ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
     548              : 
     549              :     /* Queue a length prefix that promises 12 bytes of payload, but
     550              :      * supply none.  Recv call #1 reads the prefix byte (1 byte of the
     551              :      * buffered response).  Recv call #2 runs against an empty queue
     552              :      * and returns 0 (EOF) → transport_recv bails. */
     553            2 :     uint8_t prefix_only = 0x03;  /* 3 * 4 = 12 bytes announced */
     554            2 :     mock_socket_set_response(&prefix_only, 1);
     555              : 
     556              :     uint8_t buf[64];
     557            2 :     size_t out_len = 0;
     558            2 :     int rc = transport_recv(&t, buf, sizeof(buf), &out_len);
     559            2 :     ASSERT(rc == -1,
     560              :            "transport_recv surfaces -1 when payload read yields EOF early");
     561              : 
     562            2 :     transport_close(&t);
     563            2 :     mt_server_reset();
     564              : }
     565              : 
     566              : /* Zero-payload frame: wire_len prefix = 0 → transport_recv returns 0
     567              :  * with out_len=0.  Covers the empty-frame shortcut branch. */
     568            2 : static void test_zero_payload_frame(void) {
     569            2 :     with_tmp_home("zero-frame");
     570            2 :     mt_server_init(); mt_server_reset();
     571              : 
     572            2 :     Transport t; transport_init(&t);
     573            2 :     ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
     574              : 
     575            2 :     uint8_t zero = 0x00;
     576            2 :     mock_socket_set_response(&zero, 1);
     577              : 
     578              :     uint8_t buf[32];
     579            2 :     size_t out_len = 99;
     580            2 :     int rc = transport_recv(&t, buf, sizeof(buf), &out_len);
     581            2 :     ASSERT(rc == 0,
     582              :            "transport_recv returns 0 on zero-length frame");
     583            2 :     ASSERT(out_len == 0,
     584              :            "out_len reset to 0 for zero-length frame");
     585              : 
     586            2 :     transport_close(&t);
     587            2 :     mt_server_reset();
     588              : }
     589              : 
     590              : /* ================================================================ */
     591              : /* Suite entry point                                                 */
     592              : /* ================================================================ */
     593              : 
     594            2 : void run_transport_resilience_tests(void) {
     595            2 :     RUN_TEST(test_connect_refused_is_fatal_with_clean_exit);
     596            2 :     RUN_TEST(test_partial_send_retries_to_completion);
     597            2 :     RUN_TEST(test_partial_recv_reassembles_frame);
     598            2 :     RUN_TEST(test_eintr_is_silent_retry);
     599            2 :     RUN_TEST(test_eagain_is_silent_retry);
     600            2 :     RUN_TEST(test_mid_rpc_disconnect_reconnects);
     601            2 :     RUN_TEST(test_sigpipe_is_ignored);
     602              : 
     603              :     /* Extra error-path corners for coverage completeness. */
     604            2 :     RUN_TEST(test_unaligned_payload_rejected);
     605            2 :     RUN_TEST(test_socket_create_failure);
     606            2 :     RUN_TEST(test_marker_send_failure_closes_socket);
     607            2 :     RUN_TEST(test_payload_send_failure);
     608            2 :     RUN_TEST(test_recv_frame_too_large_rejected);
     609            2 :     RUN_TEST(test_extended_prefix_recv_failure);
     610            2 :     RUN_TEST(test_extended_prefix_send_failure);
     611            2 :     RUN_TEST(test_payload_recv_failure_mid_frame);
     612            2 :     RUN_TEST(test_zero_payload_frame);
     613            2 : }
        

Generated by: LCOV version 2.0-1