--- a/framing.c 2026-09-23 13:42:50 +++ b/framing.c 2026-09-23 13:42:50 @@ -1,20 +1,23 @@ -/* EXERCISE 07 — BROKEN ON PURPOSE. Do not copy this shape into real code. +/* EXERCISE 07 — REPAIRED. * * A helper process and its client talk over a Unix domain socket. Each message * is a self-describing frame: a header carrying a sequence number, a payload * length and a checksum, followed by that many payload bytes. * - * The author tested it with short messages, where one write(2) reliably - * produced exactly one read(2), and concluded that a stream socket delivers - * messages. + * The repair: a stream carries bytes, not messages, so the receiver must + * impose the message boundary itself. Read EXACTLY the header, then read + * EXACTLY payload_len more bytes, looping in both cases until the requested + * count has arrived. The length that was always in the header is now used for + * the thing it exists for. * - * Symptom as reported by the field: "the helper works perfectly in testing and - * corrupts replies in production, but only for large documents, and only - * sometimes. Small documents are always fine." + * This is not a macOS detail and not a socket-buffer tuning problem. It is + * what "stream" means, and the identical bug exists over pipes and over TCP. + * A SOCK_DGRAM socket does preserve boundaries — that is a different + * mechanism with a different set of costs, discussed in the exercise README. * * Build and run: - * clang -O2 -g -Wall -Wextra framing.c -o /tmp/framing_broken - * /tmp/framing_broken + * clang -O2 -g -Wall -Wextra framing.c -o /tmp/framing_fixed + * /tmp/framing_fixed * * Every measurement is printed as key=value so a script can assert on it. */ @@ -62,6 +65,20 @@ return sizes[i % (int)(sizeof sizes / sizeof sizes[0])]; } +/* Read exactly n bytes, or report how many arrived before end of stream. + * Returns 1 on a complete read, 0 at a clean end of stream, -1 on error. */ +static int read_fully(int fd, void *p, size_t n, long *calls) { + unsigned char *b = p; size_t off = 0; + while (off < n) { + ssize_t r = read(fd, b + off, n - off); + if (calls) (*calls)++; + if (r == 0) return off == 0 ? 0 : -1; /* clean EOF, or a truncated frame */ + if (r < 0) return -1; + off += (size_t)r; + } + return 1; +} + static void write_fully(int fd, const void *p, size_t n) { const unsigned char *b = p; size_t off = 0; while (off < n) { @@ -106,22 +123,22 @@ long long bytesRead = 0; for (;;) { - /* One read per message. A stream socket is a BYTE STREAM: this call - * returns whatever bytes happen to be available, which is not the same - * thing as one message. The loop keeps draining so the sender never - * wedges — the bug shows up as wrong answers, not as a hang. */ - ssize_t n = read(sv[0], buf, cap); - if (n <= 0) break; - readCalls++; - bytesRead += n; - - if (n < (ssize_t)sizeof(struct msg_header)) { bad++; continue; } + /* Step 1: the header, exactly. Nothing about the message is known + * until all of it has arrived. */ struct msg_header h; - memcpy(&h, buf, sizeof h); - if (h.magic != MAGIC) { badMagic++; bad++; continue; } - if (h.payload_len > MAX_PAYLOAD) { bad++; continue; } - if (n - (ssize_t)sizeof h != (ssize_t)h.payload_len) { wrongLength++; bad++; continue; } - if (sum_of(buf + sizeof h, h.payload_len) == h.checksum) good++; else bad++; + int r = read_fully(sv[0], &h, sizeof h, &readCalls); + if (r == 0) break; /* clean end of stream */ + if (r < 0) { bad++; break; } + bytesRead += (long long)sizeof h; + + if (h.magic != MAGIC) { badMagic++; bad++; break; } + if (h.payload_len > MAX_PAYLOAD) { bad++; break; } + + /* Step 2: exactly payload_len more bytes. */ + if (read_fully(sv[0], buf, h.payload_len, &readCalls) != 1) { wrongLength++; bad++; break; } + bytesRead += h.payload_len; + + if (sum_of(buf, h.payload_len) == h.checksum) good++; else bad++; } int st; waitpid(child, &st, 0);