diff --git a/src/network.c b/src/network.c index 9e4156e..7db58c8 100644 --- a/src/network.c +++ b/src/network.c @@ -214,10 +214,12 @@ int recv_file(NODE *np, char *fname) FILE *fp; time_t prevtime; word16 len; + size_t total; /* init recv_file() */ time(&prevtime); tx = &(np->tx); + total = 0; /* open file for writing recv'd data */ fp = fopen(fname, "wb"); @@ -235,6 +237,11 @@ int recv_file(NODE *np, char *fname) break; } len = get16(tx->len); + total += len; + if (total > MAX_RECV_FILE_BYTES) { + pdebug("(%s, %s) *** exceeded MAX_RECV_FILE_BYTES", np->id, fname); + break; + } if (len && fwrite(tx->buffer, len, 1, fp) != 1) { pdebug("(%s, %s) *** I/O error", np->id, fname); break; diff --git a/src/test/_f18-recv-file-limit.c.disable b/src/test/_f18-recv-file-limit.c.disable new file mode 100644 index 0000000..ee825a5 --- /dev/null +++ b/src/test/_f18-recv-file-limit.c.disable @@ -0,0 +1,212 @@ +/** + * F-18 test harness. Disabled (by `.disable` suffix) so the Makefile + * test glob skips it; build and run manually for local validation. + * + * Forks two cooperating processes: + * - child: acts as a malicious peer on 127.0.0.1:, accepts + * one connection, does the 3-way handshake, then streams + * OP_SEND_FILE packets filled with max-size buffers indefinitely + * (never sends a short EOF-signalling packet). + * - parent: waits briefly, connects to the child via callserver(), + * then calls recv_file() and reports whether the size cap tripped. + * + * Compile with a small MAX_RECV_FILE_BYTES override for fast testing: + * + * cc -DF18_TEST_CAP=1048576 -Isrc -Iinclude/crypto-c/src \ + * -Iinclude/extended-c/src src/test/_f18-recv-file-limit.c.disable \ + * -Lbuild -Linclude/crypto-c/build -Linclude/extended-c/build \ + * -fopenmp -Wl,-\( -lm -lmochimo -lcrypto-c -lextended-c -Wl,-\) \ + * -o build/test/f18-recv-file-limit + * + * Pass: recv_file() returns VERROR and the partial download is + * deleted. Fail: recv_file runs until OOM or disk-full. + */ + +#include "network.h" +#include "types.h" +#include "extinet.h" +#include "extio.h" +#include "extlib.h" +#include "crc16.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Port for the fake peer. Avoid colliding with any real mochimo + * instance. */ +#ifndef F18_TEST_PORT +#define F18_TEST_PORT 22095 +#endif + +/* If the harness was compiled with a reduced cap, report it so the + * expected trip point is clear. */ +#ifndef MAX_RECV_FILE_BYTES +#error "expected MAX_RECV_FILE_BYTES from types.h" +#endif + +/* How much cap we expect recv_file() to enforce. By default use the + * production value. Compile with -DF18_TEST_CAP= to reduce. */ +#ifndef F18_TEST_CAP +#define F18_TEST_CAP MAX_RECV_FILE_BYTES +#endif + +/* How far past the cap we will stream (bytes) before declaring victory + * on the sender side (as an upper bound if the receiver never stops). */ +#define OVERSHOOT_BYTES (F18_TEST_CAP + (16ULL * 1024 * 1024)) + +static void run_fake_peer(int listen_sd) +{ + SOCKET sd; + NODE node; + size_t sent_total; + word16 id1; + + /* accept one connection */ + sd = accept(listen_sd, NULL, NULL); + if (sd == INVALID_SOCKET) { + perror("child: accept failed"); + exit(1); + } + sock_set_nonblock(sd); + + /* handshake: receive OP_HELLO, respond OP_HELLO_ACK */ + memset(&node, 0, sizeof(NODE)); + node.sd = sd; + if (recv_tx(&node, 5) != VEOK) { + fprintf(stderr, "child: recv OP_HELLO failed\n"); + exit(1); + } + if (get16(node.tx.opcode) != OP_HELLO) { + fprintf(stderr, "child: expected OP_HELLO, got %d\n", + get16(node.tx.opcode)); + exit(1); + } + id1 = get16(node.tx.id1); + node.id1 = id1; + node.id2 = rand16(); + put16(node.tx.opcode, OP_HELLO_ACK); + if (send_tx(&node, 5) != VEOK) { + fprintf(stderr, "child: send OP_HELLO_ACK failed\n"); + exit(1); + } + fprintf(stderr, "child: handshake done, streaming OP_SEND_FILE...\n"); + + /* stream OP_SEND_FILE with max-size buffers forever */ + memset(node.tx.buffer, 0xA5, sizeof(node.tx.buffer)); + put16(node.tx.len, (word16) sizeof(node.tx.buffer)); + sent_total = 0; + while (sent_total < OVERSHOOT_BYTES) { + put16(node.tx.opcode, OP_SEND_FILE); + if (send_tx(&node, 5) != VEOK) { + fprintf(stderr, + "child: send_tx returned non-VEOK after %zu bytes (receiver " + "likely closed the socket -- this is the expected outcome)\n", + sent_total); + exit(0); + } + sent_total += sizeof(node.tx.buffer); + } + fprintf(stderr, + "child: sent %zu bytes past cap with no socket close -- receiver " + "did NOT trip the size breaker!\n", sent_total); + exit(2); +} + +int main(void) +{ + SOCKET listen_sd; + struct sockaddr_in addr; + int one = 1; + pid_t pid; + NODE node; + int rc; + FILE *fp; + long long got; + + Running = 1; + sock_startup(); + + /* set up listen socket */ + listen_sd = socket(AF_INET, SOCK_STREAM, 0); + if (listen_sd == INVALID_SOCKET) { + perror("socket"); + return 1; + } + setsockopt(listen_sd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons(F18_TEST_PORT); + if (bind(listen_sd, (struct sockaddr *)&addr, sizeof(addr)) != 0) { + perror("bind"); + return 1; + } + if (listen(listen_sd, 1) != 0) { + perror("listen"); + return 1; + } + + pid = fork(); + if (pid < 0) { + perror("fork"); + return 1; + } + if (pid == 0) { + /* child: fake peer */ + run_fake_peer(listen_sd); + /* NOT REACHED */ + return 0; + } + + /* parent: close the listen fd, connect, call recv_file */ + close(listen_sd); + sleep(1); + + /* configure Dstport so callserver() talks to our child */ + Dstport = F18_TEST_PORT; + + fprintf(stderr, "parent: cap = %llu bytes\n", + (unsigned long long) F18_TEST_CAP); + fprintf(stderr, "parent: calling callserver() then recv_file()...\n"); + + if (callserver(&node, aton("127.0.0.1")) != VEOK) { + fprintf(stderr, "parent: callserver failed\n"); + kill(pid, SIGKILL); + waitpid(pid, NULL, 0); + return 1; + } + + rc = recv_file(&node, "/tmp/f18_recv.tmp"); + fprintf(stderr, "parent: recv_file returned %d (expect %d = VERROR)\n", + rc, VERROR); + + /* the file should have been removed by recv_file's failure path */ + fp = fopen("/tmp/f18_recv.tmp", "rb"); + if (fp) { + fseek(fp, 0, SEEK_END); + got = ftell(fp); + fclose(fp); + fprintf(stderr, "parent: partial file NOT removed, size=%lld\n", got); + } else { + fprintf(stderr, "parent: partial file correctly removed\n"); + } + + /* clean up child */ + kill(pid, SIGKILL); + waitpid(pid, NULL, 0); + sock_cleanup(); + + if (rc == VERROR) { + fprintf(stderr, "[PASS] F-18: recv_file enforced the size cap\n"); + return 0; + } else { + fprintf(stderr, "[FAIL] F-18: recv_file did NOT enforce the cap\n"); + return 1; + } +} diff --git a/src/types.h b/src/types.h index 9a745f3..007ef8f 100644 --- a/src/types.h +++ b/src/types.h @@ -46,6 +46,7 @@ #define BCONFREQ 3 /**< Run con at least */ #define CBITS 0 /**< 8 capability bits for TX */ #define MFEE 500 +#define MAX_RECV_FILE_BYTES ((size_t)(1024ULL * 1024 * 1024)) #define UBANDWIDTH 14300 /**< Dynamic upload bandwidth -- not zero */