Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
all: kilo
CC = gcc -g -Wall -W -ansi -pedantic -std=c99 -pthread -o
C+ = g++ -g -Wall -pthread -std=c++11 -o

all: kilo server

kilo: kilo.c
$(CC) -o kilo kilo.c -Wall -W -pedantic -std=c99
$(CC) kilo kilo.c

server: server.cpp
$(C+) server server.cpp

clean:
rm kilo
rm -f kilo server transfer
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ Kilo is a small text editor in less than 1K lines of code (counted with cloc).
A screencast is available here: https://asciinema.org/a/90r2i9bq8po03nazhqtsifksb

Usage: kilo `<filename>`
New Usage: kilo <host> <port>

Keys:
'get' to copy file from server

Editor Keys:

CTRL-S: Save
CTRL-Q: Quit
Expand Down
10 changes: 0 additions & 10 deletions TODO

This file was deleted.

91 changes: 85 additions & 6 deletions kilo.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
#include <stdarg.h>
#include <fcntl.h>
#include <signal.h>
#include <netdb.h>

/* Syntax highlight types */
#define HL_NORMAL 0
Expand Down Expand Up @@ -109,6 +110,8 @@ struct editorConfig {
struct editorSyntax *syntax; /* Current syntax highlight, or NULL. */
};

//Note: we may want to add a few fields to the erow and editorConfig structs

static struct editorConfig E;

enum KEY_ACTION{
Expand Down Expand Up @@ -198,6 +201,7 @@ struct editorSyntax HLDB[] = {
#define HLDB_ENTRIES (sizeof(HLDB)/sizeof(HLDB[0]))

/* ======================= Low level terminal handling ====================== */
//Note: probably don't need to edit these

static struct termios orig_termios; /* In order to restore at exit.*/

Expand All @@ -214,7 +218,7 @@ void editorAtExit(void) {
disableRawMode(STDIN_FILENO);
}

/* Raw mode: 1960 magic shit. */
/* Raw mode: 1960 magic*/
int enableRawMode(int fd) {
struct termios raw;

Expand Down Expand Up @@ -362,6 +366,7 @@ int getWindowSize(int ifd, int ofd, int *rows, int *cols) {
}

/* ====================== Syntax highlight color scheme ==================== */
//Note: probably don't need to edit these

int is_separator(int c) {
return c == '\0' || isspace(c) || strchr(",.()+-/*=~%[];",c) != NULL;
Expand Down Expand Up @@ -551,6 +556,10 @@ void editorSelectSyntaxHighlight(char *filename) {
}

/* ======================= Editor rows implementation ======================= */
//Note: probably want to edit these. perhaps at the end of an update function, we call another
// function to send an update message to the server.
//Note: we can copy the logic from these functions to allow for editing after receuving an
// update message from the server.

/* Update the rendered version and the syntax highlight of a row. */
void editorUpdateRow(erow *row) {
Expand Down Expand Up @@ -616,7 +625,7 @@ void editorFreeRow(erow *row) {
free(row->hl);
}

/* Remove the row at the specified position, shifting the remainign on the
/* Remove the row at the specified position, shifting the remaining on the
* top. */
void editorDelRow(int at) {
erow *row;
Expand Down Expand Up @@ -852,6 +861,7 @@ int editorSave(void) {
}

/* ============================= Terminal update ============================ */
//Note: probably don't need to edit these

/* We define a very simple "append buffer" structure, that is an heap
* allocated string where we can append to. This is useful in order to
Expand Down Expand Up @@ -1008,6 +1018,7 @@ void editorSetStatusMessage(const char *fmt, ...) {
}

/* =============================== Find mode ================================ */
//Note: probably don't need to edit these

#define KILO_QUERY_LEN 256

Expand Down Expand Up @@ -1107,6 +1118,7 @@ void editorFind(int fd) {
}

/* ========================= Editor events handling ======================== */
//Note: we probably don't need to edit these

/* Handle cursor position change because arrow keys were pressed. */
void editorMoveCursor(int key) {
Expand Down Expand Up @@ -1288,14 +1300,81 @@ void initEditor(void) {
signal(SIGWINCH, handleSigWinCh);
}

/* ========================= Communication with Server ======================== */

void receiveFile(int fd){
ssize_t n;
FILE *file = fopen("transfer", "w");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 In receiveFile() at kilo.c:1307, fopen("transfer", "w") is called but the return value is never checked for NULL. If the file cannot be created (e.g., permission denied, disk full, read-only filesystem), the subsequent fprintf(file, ...) and fclose(file) calls will dereference the NULL pointer, causing a segfault. Fix by adding if (!file) { perror("fopen"); return; } immediately after the fopen call.

Extended reasoning...

What the bug is and how it manifests

In receiveFile() (kilo.c:1307), the code calls FILE file = fopen("transfer", "w") to open the local output file for writing. The return value of fopen() is stored in file but is never checked before use. If fopen() fails for any reason, it returns NULL, and the code continues to use this NULL pointer as a valid FILE.

The specific code path that triggers it

The call at line 1307 is: FILE *file = fopen("transfer", "w"). After that, the function directly proceeds to use file in the inner loop at line 1322: fprintf(file, "%s\n", buffer) — and in the cleanup at line 1319: fclose(file). Neither usage has any guard against file being NULL.

Why existing code doesn't prevent it

There is no null check anywhere between the fopen() call and the first use of file. The function does not validate the file pointer before passing it to fprintf() or fclose(). The C standard library does not protect against NULL FILE* dereferences; passing NULL to fprintf() or fclose() is undefined behavior that typically results in a segfault.

What the impact would be

If kilo is run in a directory where "transfer" cannot be created (e.g., a read-only filesystem, no write permission, or disk full), the process will segfault immediately upon the first get command. This is a hard crash with no error message to the user, making diagnosis difficult. In a security context, a remote server could potentially trigger this crash by causing the client to attempt a file transfer in adverse filesystem conditions.

How to fix it

Add an early-exit guard immediately after the fopen() call:

Step-by-step proof

  1. User runs kilo in /tmp/readonly/ (a read-only mounted filesystem) and types get.
  2. receiveFile(serverFd) is called.
  3. fopen("transfer", "w") attempts to create /tmp/readonly/transfer — fails with EACCES, returns NULL.
  4. file is now NULL.
  5. The while loop runs; read() returns data; code executes fprintf(NULL, "%s\n", buffer).
  6. fprintf dereferences the NULL FILE* internally (accessing file->_IO_write_ptr or equivalent) — segfault.

char buffer[1024];

n = read(fd, buffer, 1024);
buffer[n] = '\0';
Comment on lines +1310 to +1311

@staging-devin-ai-integration staging-devin-ai-integration Bot Apr 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Out-of-bounds write in receiveFile when read() returns 1024 or -1

At kilo.c:1310-1311, n = read(fd, buffer, 1024) can return up to 1024, and then buffer[n] = '\0' writes to buffer[1024], which is one past the end of char buffer[1024] (valid indices 0–1023). This is a stack buffer overflow. Similarly, if read() returns -1 on error, buffer[-1] is written. The same pattern occurs at line 1314/1316 inside the loop.

Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground


while (1){
if ((n = read(fd, buffer, 1024)) > 0){
// printf("Line: %s\n", buffer);
buffer[n] = '\0';
Comment on lines +1310 to +1316

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 In receiveFile() (kilo.c:1310-1316), buffer is declared as char[1024] but both read() calls pass 1024 as the count limit; when read() returns exactly 1024 bytes, buffer[1024] is written one past the end of the stack array (off-by-one stack overflow). Additionally, the initial read() at line 1310 has no error check: if read() returns -1 on a socket error, buffer[-1] is written, corrupting stack memory before the buffer. Fix: use read(fd, buffer, sizeof(buffer)-1) for both calls, and add if (n <= 0) return; before the first buffer[n] = '\0'.

Extended reasoning...

What the bug is and how it manifests

receiveFile() in kilo.c declares a stack buffer of exactly 1024 bytes: char buffer[1024] (valid indices 0–1023). The function then calls n = read(fd, buffer, 1024) twice — once before the loop and once inside it — and immediately writes buffer[n] = '\0' after each call. There are two distinct memory-safety issues at these two locations.

The specific code paths that trigger it

Issue 1 (overflow): The server explicitly calls send(fd, (void*)"Start Transfer", 1024, 0) and sends each line followed by read(fd, buffer, 1024) to consume an ACK. Because the client's first read(fd, buffer, 1024) requests up to 1024 bytes, it can receive exactly 1024 bytes (e.g., the "Start Transfer" message padded to 1024 bytes). When n == 1024, the assignment buffer[1024] = '\0' writes one byte beyond the end of the array — a classic stack buffer overflow. The identical pattern on line 1315 inside the while loop has the same exposure.

Issue 2 (underflow): The initial read at line 1310 has no error guard. If the socket experiences an error (connection reset, network failure, etc.), read() returns -1. n is of type ssize_t (signed), so n = -1 and buffer[-1] = '\0' writes one byte before the start of the stack-allocated array — corrupting adjacent stack data. The inner loop correctly guards with if ((n = read(fd, buffer, 1024)) > 0), making that read safe; only the initial read is unprotected.

Why existing code doesn't prevent it

There is no bounds check between the read() call and the buffer[n] write. C does not prevent out-of-bounds writes. The only guard in the function is the > 0 check in the loop condition, but this guard comes after the dereference in the case of the inner-loop write (buffer[n] = '\0' is inside the if (n > 0) block, so n=1024 still passes), and is entirely absent for the initial read.

What the impact would be

Both issues constitute undefined behavior with concrete consequences. The off-by-one overflow (n=1024) overwrites the byte immediately following buffer on the stack — potentially part of the saved frame pointer, return address, or adjacent local variables, enabling stack corruption and possible code execution. The underflow (n=-1) writes to buffer[-1], which is the byte immediately before buffer on the stack, with similar corruption risk. Either condition can be triggered by a malicious or misbehaving server.

How to fix it

Change both read() calls to request at most sizeof(buffer) - 1 bytes: read(fd, buffer, sizeof(buffer) - 1). This guarantees that n can be at most 1023, making buffer[n] = buffer[1023] always in-bounds. Additionally, add an early-exit guard on the initial read: if (n <= 0) return; before the first buffer[n] = '\0' assignment, mirroring the protection already present in the loop.

Step-by-step proof

  1. Server calls send(fd, (void*)"Start Transfer", 1024, 0) — sending exactly 1024 bytes.
  2. Client's receiveFile() calls n = read(fd, buffer, 1024).
  3. TCP delivers all 1024 bytes in a single read; read() returns 1024.
  4. Code executes buffer[1024] = '\0'.
  5. buffer occupies indices 0–1023; index 1024 is one byte past the end, on the stack.
  6. This write corrupts the stack frame — potentially the saved return address of receiveFile().

if (!strcmp(buffer, "End Transfer")){
// printf("Closing\n");
fclose(file);
return;
}
fprintf(file, "%s\n", buffer);
send(fd, "ACK", 1024, 0); //why are we sending this?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 send() reads 1024 bytes from a 4-byte string literal "ACK", causing buffer over-read

send(fd, "ACK", 1024, 0) specifies a length of 1024 bytes, but the string literal "ACK" is only 4 bytes. This reads 1020 bytes beyond the string literal, leaking arbitrary memory contents to the remote peer.

Suggested change
send(fd, "ACK", 1024, 0); //why are we sending this?
send(fd, "ACK", strlen("ACK"), 0); //why are we sending this?
Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 All four protocol message sends pass 1024 as the byte count but use short string literals ("ACK", "get", "Start Transfer", "End Transfer"), causing send() to read 1021–1021 bytes past the string into adjacent .rodata memory. Fix by using strlen() or the exact literal length for each call: send(fd, "ACK", 3, 0), send(serverFd, "get", 3, 0), send(fd, "Start Transfer", 14, 0), and send(fd, "End Transfer", 12, 0).

Extended reasoning...

What the bug is and how it manifests

The code passes the buffer size constant (1024) as the third argument to send() when sending short string literals. For example, send(fd, "ACK", 1024, 0) tells the kernel to send 1024 bytes starting at the address of the 3-character string literal "ACK". Since the literal is only 3 bytes (4 including the null terminator), the kernel reads 1020–1021 bytes beyond the end of the literal from whatever memory follows it in the process's .rodata section.

The specific code paths that trigger it

There are four affected call sites:

  1. kilo.creceiveFile(): send(fd, "ACK", 1024, 0) — "ACK" is 3 bytes
  2. kilo.cmain(): send(serverFd, "get", 1024, 0) — "get" is 3 bytes
  3. server.cppsendFile(): send(fd, (void*)"Start Transfer", 1024, 0) — "Start Transfer" is 14 bytes
  4. server.cppsendFile(): send(fd, (void*)"End Transfer", 1024, 0) — "End Transfer" is 12 bytes

Why existing code doesn't prevent it

The C standard does not bound-check pointer arithmetic or memory reads that stay within mapped pages. String literals in C reside in the .rodata segment, which is a readable (though non-writable) mapping. Adjacent constants, function pointers encoded in the binary, version strings, and other read-only data from the same compilation unit typically follow in the same page, so there is no segfault — the extra bytes are silently read and transmitted.

What the impact would be

This is an information disclosure vulnerability. Every "ACK" acknowledgment sends ~1020 bytes of process memory contents (other string literals, constants, possibly embedded paths or other sensitive data compiled into the binary) to the remote peer. While the server currently reads and discards the ACK bytes, the data is still transmitted on the wire. For "get" in main(), the server receives 1021 extra bytes which it compares against the string "get" using a std::string equality check — the comparison happens to work because std::string uses the length it received, not a fixed-size comparison, but the extra garbage bytes still traverse the network.

How to fix it

Replace the hardcoded 1024 with the actual string length at each call site:

  • send(fd, "ACK", 3, 0) or send(fd, "ACK", strlen("ACK"), 0)
  • send(serverFd, "get", 3, 0) or send(serverFd, "get", strlen("get"), 0)
  • send(fd, "Start Transfer", 14, 0) or send(fd, "Start Transfer", strlen("Start Transfer"), 0)
  • send(fd, "End Transfer", 12, 0) or send(fd, "End Transfer", strlen("End Transfer"), 0)

Step-by-step proof

  1. Compiler places the string literal "ACK\0" at some address A in the .rodata segment, occupying exactly 4 bytes.
  2. send(fd, "ACK", 1024, 0) is called; the kernel receives: buffer pointer = A, length = 1024.
  3. The kernel calls copy_from_user() (or equivalent) to read 1024 bytes starting at A from the process address space.
  4. Since .rodata is a full page (≥4096 bytes), the read succeeds — no fault.
  5. The kernel sends bytes A through A+1023 to the remote socket. Bytes A+4 through A+1023 contain whatever other constants, string literals, or padding the compiler placed after "ACK" in the read-only data section.
  6. The server's read(clientFd, buffer, 1024) receives all 1024 bytes; the string line(buffer) constructor stops at the first null byte (position 3), so the comparison line == "get" or similar sees only the intended content — but the 1020 extra bytes of .rodata were still sent over the network.

}
}
Comment on lines +1313 to +1325

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The receiveFile() loop at kilo.c:1313 spins forever at 100% CPU if the server closes the connection: while(1){ if ((n = read(...)) > 0) { ... } } — when read() returns 0 (peer closed), the if-body is skipped and the loop immediately calls read() again, forever. Fix by replacing while(1) with while((n = read(fd, buffer, 1023)) > 0) and adding fclose(file) after the loop to prevent the file handle leak.

Extended reasoning...

What the bug is and how it manifests

The loop in receiveFile() (kilo.c:1313) is structured as:

while (1) {
    if ((n = read(fd, buffer, 1024)) > 0) {
        buffer[n] = '\0';
        if (\!strcmp(buffer, "End Transfer")) {
            fclose(file);
            return;
        }
        fprintf(file, "%s\n", buffer);
        send(fd, "ACK", 1024, 0);
    }
}

When the server closes the TCP connection, read() returns 0 (EOF). Since 0 is not > 0, the if-body is never entered. The while(1) has no break or other exit path for this case, so execution loops back and immediately calls read() again — which again returns 0. This repeats forever in a tight busy-loop, consuming 100% of a CPU core with no sleep, yield, or timeout.

The specific code path that triggers it

The only exit path from the function is return inside the strcmp(buffer, "End Transfer") branch at kilo.c:1319. That branch is inside the if (n > 0) guard. If the server crashes, is killed, loses its network connection, or disconnects for any other reason before sending the "End Transfer" sentinel, n will be 0 on every subsequent call and the function never returns. On read() error (-1), the same spin occurs since -1 is also not > 0.

Why existing code doesn't prevent it

The programmer placed the read() call inside an if-guard rather than in the while condition itself. This pattern is commonly written as while ((n = read(...)) > 0) { ... }, which exits naturally when read() returns 0 or -1. By moving the read into the if-guard inside while(1), they removed the only natural loop exit. There is no break, no else { break; }, no timeout, and no errno check that could escape the loop.

What the impact would be

Any abnormal server termination (crash, kill signal, network drop) permanently hangs the kilo client process, consuming 100% CPU indefinitely. The client must be killed manually with SIGKILL. The open file handle to "transfer" is also leaked since fclose(file) is only called in the "End Transfer" path. This is a liveness bug introduced by this PR in the core networking feature.

Step-by-step proof

  1. User connects: ./kilo localhost 10001
  2. User types get; client sends "get" to server, calls receiveFile(serverFd)
  3. Server is killed with kill -9 \<pid\> before sending "End Transfer"
  4. TCP FIN is delivered to the client socket
  5. read(fd, buffer, 1024) in the while loop returns 0
  6. The condition 0 > 0 is false; the if-body is skipped entirely
  7. The loop immediately calls read() again, which returns 0 again
  8. This repeats indefinitely at CPU speed — 100% CPU busy-loop, no return

How to fix it

Change the loop from:

while (1) {
    if ((n = read(fd, buffer, 1024)) > 0) { ... }
}

to:

while ((n = read(fd, buffer, 1023)) > 0) {
    buffer[n] = '\0';
    ...
}
fclose(file);

This exits the loop on both EOF (n=0) and error (n=-1), and ensures fclose(file) is called on all exit paths.

//editorOpen("test");
}
Comment on lines +1305 to +1327

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Multiple critical issues in receiveFile().

  1. Line 1307: fopen() result not checked - will crash if file can't be created.
  2. Line 1310: read() return value not checked for -1 (error).
  3. Line 1311: If n == 1024, writing buffer[n] = '\0' is a buffer overflow.
  4. Line 1313-1325: Infinite loop - if server disconnects, read() returns 0 repeatedly forever.
  5. Line 1323: send(fd, "ACK", 1024, 0) sends 1024 bytes but only 3 are meaningful - sends stack garbage.
🐛 Proposed fix
 void receiveFile(int fd){
     ssize_t n;
     FILE *file = fopen("transfer", "w");
+    if (!file) {
+        perror("Cannot create transfer file");
+        return;
+    }
     char buffer[1024];

     n = read(fd, buffer, 1024);
-    buffer[n] = '\0';
+    if (n <= 0) {
+        fclose(file);
+        return;
+    }
+    buffer[n < 1024 ? n : 1023] = '\0';

-    while (1){
-        if ((n = read(fd, buffer, 1024)) > 0){
+    while ((n = read(fd, buffer, 1023)) > 0){
             buffer[n] = '\0';
             if (!strcmp(buffer, "End Transfer")){
                 fclose(file);
                 return;
             }
             fprintf(file, "%s\n", buffer);
-            send(fd, "ACK", 1024, 0);
-        }
+            send(fd, "ACK", 3, 0);
     }
+    fclose(file);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void receiveFile(int fd){
ssize_t n;
FILE *file = fopen("transfer", "w");
char buffer[1024];
n = read(fd, buffer, 1024);
buffer[n] = '\0';
while (1){
if ((n = read(fd, buffer, 1024)) > 0){
// printf("Line: %s\n", buffer);
buffer[n] = '\0';
if (!strcmp(buffer, "End Transfer")){
// printf("Closing\n");
fclose(file);
return;
}
fprintf(file, "%s\n", buffer);
send(fd, "ACK", 1024, 0); //why are we sending this?
}
}
//editorOpen("test");
}
void receiveFile(int fd){
ssize_t n;
FILE *file = fopen("transfer", "w");
if (!file) {
perror("Cannot create transfer file");
return;
}
char buffer[1024];
n = read(fd, buffer, 1024);
if (n <= 0) {
fclose(file);
return;
}
buffer[n < 1024 ? n : 1023] = '\0';
while ((n = read(fd, buffer, 1023)) > 0){
// printf("Line: %s\n", buffer);
buffer[n] = '\0';
if (!strcmp(buffer, "End Transfer")){
// printf("Closing\n");
fclose(file);
return;
}
fprintf(file, "%s\n", buffer);
send(fd, "ACK", 3, 0); //why are we sending this?
}
fclose(file);
//editorOpen("test");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@kilo.c` around lines 1305 - 1327, In receiveFile, fix multiple robustness
bugs: check fopen("transfer","w") return and handle/fail early if NULL; always
check read(fd, buffer, sizeof buffer) return value for -1 (error) and 0 (peer
closed) and break/cleanup accordingly; avoid buffer overflow by only writing a
terminator when n < sizeof buffer (or treat buffer as raw bytes and use fwrite
with the exact n bytes instead of buffer[n]='\0' and fprintf); stop the infinite
loop by returning/closing file when read returns 0 or an unrecoverable error;
and replace send(fd, "ACK", 1024, 0) with sending the actual length (e.g.,
send(fd, "ACK", 3, 0) or strlen("ACK")) and handle its return value; ensure
fclose(file) is called on all exit paths.


/* ============================= Main Program ================================== */

//main program of text-editor client
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr,"Usage: kilo <filename>\n");
//check command-line args
if (argc != 3) {
fprintf(stderr,"Usage: kilo <host> <port>\n");
exit(1);
}

initEditor();
editorSelectSyntaxHighlight(argv[1]);
//setup
struct addrinfo hints, *res, *traverser;
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
int r = getaddrinfo(argv[1], argv[2], &hints, &res);
if (r != 0){
fprintf(stderr,"Error: Can't find server.\n");
return 1;
}

// Try addresses until one is successful
int serverFd;
for (traverser = res; traverser; traverser = traverser->ai_next){
if ((serverFd = socket(traverser->ai_family, traverser->ai_socktype, traverser->ai_protocol)) != -1){
if ((connect(serverFd, traverser->ai_addr, traverser->ai_addrlen)) == 0){
break;
}
}
}
printf("Connected\n");
Comment on lines +1358 to +1359

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 No check for connection failure after address traversal loop

After the for loop at kilo.c:1352-1358, if no address succeeds in connecting, traverser is NULL but the code continues to printf("Connected\n") and uses serverFd (which may reference a socket that failed to connect, or be uninitialized if all socket() calls failed). There's no check like if (traverser == NULL) { /* handle error */ }.

Suggested change
}
printf("Connected\n");
}
if (!traverser) {
fprintf(stderr, "Error: Could not connect to server.\n");
freeaddrinfo(res);
return 1;
}
printf("Connected\n");
Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Comment on lines +1350 to +1359

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Connection loop doesn't verify success and leaks addrinfo.

If no address succeeds, traverser becomes NULL but the code proceeds to use serverFd (which may be invalid or from a failed connect()). Also, freeaddrinfo(res) is never called.

🐛 Proposed fix
     for (traverser = res; traverser; traverser = traverser->ai_next){
         if ((serverFd = socket(traverser->ai_family, traverser->ai_socktype, traverser->ai_protocol)) != -1){
             if ((connect(serverFd, traverser->ai_addr, traverser->ai_addrlen)) == 0){
                 break;
             }
+            close(serverFd);
         }
     }
+    freeaddrinfo(res);
+    if (!traverser) {
+        fprintf(stderr, "Error: Could not connect to server.\n");
+        return 1;
+    }
     printf("Connected\n");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Try addresses until one is successful
int serverFd;
for (traverser = res; traverser; traverser = traverser->ai_next){
if ((serverFd = socket(traverser->ai_family, traverser->ai_socktype, traverser->ai_protocol)) != -1){
if ((connect(serverFd, traverser->ai_addr, traverser->ai_addrlen)) == 0){
break;
}
}
}
printf("Connected\n");
// Try addresses until one is successful
int serverFd;
for (traverser = res; traverser; traverser = traverser->ai_next){
if ((serverFd = socket(traverser->ai_family, traverser->ai_socktype, traverser->ai_protocol)) != -1){
if ((connect(serverFd, traverser->ai_addr, traverser->ai_addrlen)) == 0){
break;
}
close(serverFd);
}
}
freeaddrinfo(res);
if (!traverser) {
fprintf(stderr, "Error: Could not connect to server.\n");
return 1;
}
printf("Connected\n");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@kilo.c` around lines 1350 - 1359, The connection loop can leave traverser
NULL and may proceed with an invalid serverFd while also never freeing the
addrinfo; update the loop that iterates over res/traverser to track success
(e.g., a boolean or check traverser not NULL after loop), only use serverFd
after confirming connect succeeded, call freeaddrinfo(res) before returning or
on error, and on failure close any opened serverFd and return an error; adjust
logic around socket()/connect() in the loop (referencing traverser, serverFd,
socket, connect, and res) to ensure proper cleanup and validation.


Comment on lines +1352 to +1360

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The connection loop at kilo.c lines 1352-1360 has two bugs: (1) if all socket()/connect() attempts fail, traverser ends up NULL but the code unconditionally prints "Connected" and passes the invalid serverFd to send() and receiveFile(), causing undefined behavior; (2) freeaddrinfo(res) is never called on any code path, leaking the entire addrinfo linked list returned by getaddrinfo(). Fix by adding a traverser == NULL check with early return after the loop, and calling freeaddrinfo(res) before any return path.

Extended reasoning...

What the bug is and how it manifests

The networking code in main() calls getaddrinfo() to resolve the server hostname, then iterates over the returned linked list trying socket() and connect() on each candidate address. If every candidate fails (socket() returns -1 or connect() returns non-zero), the loop exits with traverser == NULL and serverFd holding either -1 (from a failed socket()) or a file descriptor for a socket that failed to connect. The code then unconditionally executes printf("Connected\n") and falls through to the user input loop, which calls send(serverFd, "get", 1024, 0) and receiveFile(serverFd) using this invalid fd. Separately, freeaddrinfo(res) is never called on any code path — not on success, not on failure, and not before process exit — leaking all memory allocated by getaddrinfo.

The specific code path that triggers it

At line 1344, getaddrinfo() allocates a linked list rooted at res. The for-loop at lines 1352-1358 iterates traverser over this list; on break (success), traverser points to the winning addrinfo node. If the loop completes without break, traverser == NULL. Line 1359 then executes printf("Connected\n") unconditionally — no check on traverser. Lines 1366-1370 then call send(serverFd, ...) and receiveFile(serverFd) with whatever serverFd happens to hold. freeaddrinfo(res) appears nowhere in the file.

Why existing code doesn't prevent it

There is simply no if (!traverser) guard between the end of the for-loop and the printf. The pattern of iterating an addrinfo list with a break-on-success requires an explicit post-loop check to distinguish the success case (traverser != NULL) from the exhausted-all-addresses case (traverser == NULL). The current code omits this check entirely. The freeaddrinfo leak is equally straightforward: there is no call site for it in the file at all.

What the impact would be

If the server is unreachable (wrong hostname, server down, firewall blocking the port), every address attempt fails, traverser is NULL, and the program silently reports "Connected" — a false positive that hides the real error. Subsequent send() on the invalid fd returns -1 with EBADF or ENOTCONN (return value not checked), and receiveFile() calls read() on the same bad fd — also EBADF — leading to undefined behavior in the buffer indexing (buffer[n] where n == -1 writes buffer[-1]). The memory leak from freeaddrinfo is minor relative to the functional failure but still a resource bug on every invocation.

How to fix it

After the for-loop, call freeaddrinfo(res) unconditionally, then check traverser: if (!traverser) { fprintf(stderr, "Error: Could not connect\n"); return 1; }. Additionally, close(serverFd) should be called inside the loop when connect() fails to avoid leaking socket file descriptors during the address traversal.

Step-by-step proof of the traverser NULL path

  1. User runs: ./kilo unreachable-host 10001
  2. getaddrinfo() succeeds and returns res pointing to one addrinfo node.
  3. Loop iteration: socket() succeeds (returns fd=5); connect() fails (ECONNREFUSED); the inner if-body exits without break; the loop advances traverser to traverser->ai_next == NULL.
  4. Loop condition fails; traverser is now NULL; serverFd == 5 (socket that failed connect).
  5. printf("Connected\n") executes — false output.
  6. User types "get"; send(5, "get", 1024, 0) is called on a socket in a failed-connect state; returns -1 (ENOTCONN).
  7. receiveFile(5) is called; read(5, buffer, 1024) returns -1; buffer[-1] = '\0' corrupts the stack.

char buffer[1024];
while (fgets(buffer, 1024, stdin)){
buffer[strcspn(buffer, "\r\n")] = 0;
if (!strcmp(buffer, "get")){
printf("sending get\n");
send(serverFd, "get", 1024, 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 send() reads 1024 bytes from a 4-byte string literal "get", causing buffer over-read

send(serverFd, "get", 1024, 0) specifies a length of 1024 bytes, but the string literal "get" is only 4 bytes (including the null terminator). This reads 1020 bytes past the string literal from whatever is in memory, sending garbage data over the network and potentially leaking sensitive memory contents. The length should be the actual size of the string (e.g., strlen("get") or 4).

Suggested change
send(serverFd, "get", 1024, 0);
send(serverFd, "get", strlen("get"), 0);
Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

receiveFile(serverFd);
}
// printf("%s\n", buffer);
}
Comment on lines +1361 to +1370

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

send() transmits garbage beyond the string.

send(serverFd, "get", 1024, 0) sends 1024 bytes but the string "get" is only 3 characters. This sends uninitialized stack data to the server.

🐛 Proposed fix
-send(serverFd, "get", 1024, 0);
+send(serverFd, "get", 3, 0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
char buffer[1024];
while (fgets(buffer, 1024, stdin)){
buffer[strcspn(buffer, "\r\n")] = 0;
if (!strcmp(buffer, "get")){
printf("sending get\n");
send(serverFd, "get", 1024, 0);
receiveFile(serverFd);
}
// printf("%s\n", buffer);
}
char buffer[1024];
while (fgets(buffer, 1024, stdin)){
buffer[strcspn(buffer, "\r\n")] = 0;
if (!strcmp(buffer, "get")){
printf("sending get\n");
send(serverFd, "get", 3, 0);
receiveFile(serverFd);
}
// printf("%s\n", buffer);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@kilo.c` around lines 1361 - 1370, The send call is writing uninitialized
stack data because it uses 1024 as the length; replace the hardcoded 1024 in
send(serverFd, "get", 1024, 0) with the actual message length (e.g.
strlen("get") or sizeof("get")-1, or strlen("get")+1 if you need the terminating
NUL) so only the bytes of the string "get" are transmitted; update the call near
where receiveFile(serverFd) is invoked and keep serverFd and message unchanged.


close(serverFd);
// exit(0);

//start editor
initEditor();
editorSelectSyntaxHighlight("transfer");
editorOpen(argv[1]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 editorOpen opens hostname (argv[1]) instead of the received "transfer" file

editorOpen(argv[1]) passes the hostname argument to the editor's file-open function. Since the PR changed argv[1] from a filename to a hostname, this attempts to open a file named after the server hostname rather than the "transfer" file that was received and written by receiveFile(). Line 1377 correctly uses "transfer" for syntax highlighting, so the intent was clearly to open "transfer".

Suggested change
editorOpen(argv[1]);
editorOpen("transfer");
Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Comment on lines +1375 to 1378

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

editorOpen(argv[1]) uses hostname instead of transferred file.

After the networking changes, argv[1] is the hostname, not a filename. The transferred content is written to "transfer", so this should open "transfer" instead.

🐛 Proposed fix
     initEditor();
     editorSelectSyntaxHighlight("transfer");
-    editorOpen(argv[1]);
+    editorOpen("transfer");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
//start editor
initEditor();
editorSelectSyntaxHighlight("transfer");
editorOpen(argv[1]);
//start editor
initEditor();
editorSelectSyntaxHighlight("transfer");
editorOpen("transfer");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@kilo.c` around lines 1375 - 1378, The code calls editorOpen(argv[1]) which
now points to the hostname after networking changes; change the call to open the
transferred file instead (open "transfer" or the constant used for the received
file) so the editor displays the received content—update the invocation in the
init sequence (around initEditor and editorSelectSyntaxHighlight) to call
editorOpen("transfer") or the transferred-file identifier used elsewhere rather
than argv[1].

Comment on lines +1377 to 1378

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 After receiving the file via the server and storing it as "transfer", the code calls editorOpen(argv[1]) where argv[1] is the server hostname (e.g. "localhost"), not the received file. This means the editor will attempt to open a file named after the hostname instead of the transferred content; the fix is to change editorOpen(argv[1]) to editorOpen("transfer").

Extended reasoning...

What the bug is and how it manifests

In kilo.c's main(), after the networking phase downloads the server's file and stores it locally as "transfer", the code initializes the editor and then calls editorSelectSyntaxHighlight("transfer") followed immediately by editorOpen(argv[1]). Because argv[1] is the server hostname string supplied on the command line (e.g. "localhost"), editorOpen tries to open a file whose name equals the hostname rather than the received file.

The specific code path that triggers it

kilo.c lines 1377–1378:

editorSelectSyntaxHighlight("transfer");
editorOpen(argv[1]);          // argv[1] == hostname, not "transfer"

Both calls are in the same block right after receiveFile(serverFd) stores the server content in a file named "transfer".

Why existing code does not prevent it

editorOpen simply calls fopen(filename, "r") on whatever string it receives. If a file named after the hostname does not exist, editorOpen returns 1 (ENOENT path) and the editor opens empty. If some unrelated file with the hostname name happens to exist, that file is opened silently. There is no assertion or guard that the argument must match the file written by receiveFile.

What the impact would be

Users who run kilo <host> <port> and issue the get command will receive the file from the server (stored as "transfer") but the editor will display either an empty buffer or an unrelated file. The transferred content is effectively silently discarded. This is a complete functional failure of the primary feature introduced by this PR.

Step-by-step proof

  1. User runs: ./kilo localhost 10001
    • argv[1] = "localhost", argv[2] = "10001"
  2. User types get at the prompt; receiveFile(serverFd) writes server content to a local file named "transfer".
  3. The input loop exits (EOF), initEditor() runs, then:
    • editorSelectSyntaxHighlight("transfer") — correct, selects syntax based on "transfer" filename.
    • editorOpen(argv[1]) = editorOpen("localhost") — incorrect; tries to open a file named "localhost".
  4. fopen("localhost", "r") fails with ENOENT (no such file), editorOpen returns 1, the editor starts with an empty buffer.
  5. The content that was successfully transferred and written to "transfer" is never displayed.

How to fix it

Change editorOpen(argv[1]) to editorOpen("transfer") on line 1378, mirroring the correct hardcoded string already used on line 1377.

enableRawMode(STDIN_FILENO);
editorSetStatusMessage(
Comment on lines +1362 to 1380

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Editor code is unreachable after stdin EOF in client main()

The while (fgets(buffer, 1024, stdin)) loop at kilo.c:1362 reads from stdin until EOF (Ctrl-D). After EOF, the code at kilo.c:1376-1384 initializes the editor and enters its main loop reading keypresses from STDIN_FILENO. On a terminal, Ctrl-D signals EOF for buffered reads but doesn't permanently close stdin — subsequent reads in raw mode may still work. However, the design is fragile and confusing: the user must know to press Ctrl-D to transition from the command loop to the editor. If stdin is redirected from a pipe or file, it will be permanently at EOF and the editor will be completely non-functional. This isn't flagged as a bug because the terminal case technically works, but the UX is very problematic.

(Refers to lines 1362-1385)

Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Comment on lines +1376 to 1380

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Editor code unreachable — stdin is at EOF after fgets loop completes

At kilo.c:1362-1384, the while (fgets(buffer, 1024, stdin)) loop runs until stdin reaches EOF. After the loop, at line 1376 the code starts the editor which relies on reading from STDIN_FILENO (editorProcessKeypress(STDIN_FILENO) at line 1384 and enableRawMode(STDIN_FILENO) at line 1379). Since stdin is already at EOF, the editor will not be able to read any keystrokes, making it non-functional. The editor startup code is effectively dead code.

(Refers to lines 1376-1384)

Prompt for agents
In kilo.c main(), the while(fgets()) loop at line 1362 consumes stdin until EOF. After the loop ends (lines 1376-1384), the code tries to start the editor which reads keystrokes from STDIN_FILENO. Since stdin is already at EOF, the editor cannot receive any input. The entire editor portion is unreachable / non-functional. The design needs rethinking — either the networking/command phase needs to use a different mechanism to know when to stop (e.g., a specific 'edit' command that breaks the loop), or the editor needs to read from a different fd, or stdin needs to be reopened from /dev/tty.
Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Expand Down
143 changes: 143 additions & 0 deletions server.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// C Headers
#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
// C++ Headers
#include <fstream>
#include <iostream>
#include <sstream>
#include <map>
#include <string>
#include <vector>
using namespace std;

//readFile - reads lines in file into the data structure lines
vector<string> readFile(){
string line;
vector<string> lines;
ifstream file("test");
while (getline(file, line)){
lines.push_back(line);
}
return lines;
}

//sendFile - send file line-by-line to a client at fd
void sendFile(int fd, vector<string> lines){
vector<string>::iterator i;

send(fd, (void*)"Start Transfer", 1024, 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 send() reads 1024 bytes from short string literals in sendFile, causing buffer over-read

send(fd, (void*)"Start Transfer", 1024, 0) on line 37 and send(fd, (void*)"End Transfer", 1024, 0) on line 55 both specify a length of 1024 bytes, but the string literals are only 15 and 13 bytes respectively. This reads far past the string literals, sending garbage memory to the client and potentially leaking sensitive data.

Suggested change
send(fd, (void*)"Start Transfer", 1024, 0);
send(fd, (void*)"Start Transfer", strlen("Start Transfer"), 0);
Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground


/* for (i = lines.begin(); i != lines.end(); i++){
char buffer[1024];
string msg = *i;
// cout << "Line: " << msg << endl;
send(fd, msg.c_str(), msg.length(), 0);
read(fd, buffer, 1024);
} */

//this is the same as the loop that is commented out
for(string msg : lines){
char buffer[1024];

send(fd, msg.c_str(), msg.length(), 0);
read(fd, buffer, 1024);
}

send(fd, (void*)"End Transfer", 1024, 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 send() reads 1024 bytes from 13-byte "End Transfer" string literal

Same buffer over-read as server.cpp:37. send(fd, (void*)"End Transfer", 1024, 0) sends 1024 bytes starting from a 13-byte string literal, reading 1011 bytes of arbitrary memory.

Suggested change
send(fd, (void*)"End Transfer", 1024, 0);
send(fd, (void*)"End Transfer", strlen("End Transfer"), 0);
Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Comment on lines +48 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Protocol mismatch: server sends msg.length() bytes but client expects null-terminated strings

In server.cpp:51, send(fd, msg.c_str(), msg.length(), 0) sends exactly msg.length() bytes (no null terminator). The client at kilo.c:1314-1316 reads into a buffer and null-terminates based on read count, then does strcmp(buffer, "End Transfer"). Meanwhile the server's "End Transfer" sentinel at line 55 sends 1024 bytes (the over-read bug). Even if the over-read bugs are fixed, the protocol has no framing — TCP is a stream protocol, so multiple sends can be coalesced into one read or one send can be split across multiple reads. The strcmp checks against "End Transfer" could match partial data or miss the sentinel entirely. This fundamental design issue means the protocol is unreliable.

Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

}
Comment on lines +34 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Protocol deadlock: ACK synchronization mismatch with client.

The server sends "Start Transfer" (Line 37) and immediately proceeds to send file lines, expecting an ACK after each (Line 52). However, the client's receiveFile() (kilo.c:1305-1327) reads "Start Transfer" but does not send an ACK for it. The client only sends ACKs after receiving data lines. This causes deadlock: the server blocks on read() at Line 52 waiting for an ACK that never arrives.

Additionally, send() at Lines 37 and 55 passes 1024 as the length but the actual strings are much shorter, sending uninitialized stack/heap data.

🐛 Proposed fix for send length issue
-send(fd, (void*)"Start Transfer", 1024, 0);
+send(fd, "Start Transfer", strlen("Start Transfer"), 0);
 // ...
-send(fd, (void*)"End Transfer", 1024, 0);
+send(fd, "End Transfer", strlen("End Transfer"), 0);

The ACK protocol needs to be redesigned so both sides agree on when ACKs are sent. Either:

  1. Client sends ACK after "Start Transfer", or
  2. Server doesn't wait for ACK after each line (simpler)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server.cpp` around lines 34 - 56, In sendFile(), the server blocks waiting
for ACKs the client never sends and also sends incorrect lengths (1024) for
short strings; to fix: in sendFile() (the Start Transfer/send loop/End Transfer
sequence) stop expecting an immediate ACK for "Start Transfer" and "End
Transfer" (remove the read() call around those sends) and either remove the
per-line read() ACK wait or make it optional/timeout-based to match the client's
protocol — the simplest fix is to eliminate the blocking read() after send(fd,
msg.c_str(), ...) so the server just streams lines; also change all send(...)
calls to use the actual byte counts (use strlen("Start Transfer")+1 or
strlen(...), msg.size() for vector<string> lines, and strlen("End Transfer")+1)
instead of 1024 to avoid leaking stack/heap data. Ensure any remaining recv/read
calls check return values and handle EAGAIN/EWOULDBLOCK/timeouts consistently
with the client protocol.


//threadFunc - thread function to read any messages from a client
void *threadFunc(void *args){
ssize_t n;
int clientFd = *(int*)args;
char buffer[1024];
pthread_detach(pthread_self());

// Read until disconnection
while ((n = read(clientFd, buffer, 1024)) > 0){
string line(buffer);
Comment on lines +66 to +67

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Buffer not null-terminated after read() in threadFunc, causing undefined string construction

read(clientFd, buffer, 1024) at server.cpp:66 does not null-terminate buffer, but line 67 constructs string line(buffer) which expects a null-terminated C string. This reads past the valid data until a null byte is found in memory, causing undefined behavior and incorrect command matching.

Suggested change
while ((n = read(clientFd, buffer, 1024)) > 0){
string line(buffer);
while ((n = read(clientFd, buffer, 1023)) > 0){
buffer[n] = '\0';
string line(buffer);
Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Comment on lines +66 to +67

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 In server.cpp threadFunc (lines 66-67), read(clientFd, buffer, 1024) fills a 1024-byte stack buffer but string line(buffer) is constructed immediately without null-terminating first; if exactly 1024 bytes are received, the std::string const char* constructor reads past buffer[1023] into unowned memory looking for a null terminator, causing undefined behavior. Fix by using read(clientFd, buffer, 1023) followed by buffer[n] = '\0', or by constructing via std::string(buffer, n).

Extended reasoning...

What the bug is and how it manifests

In threadFunc (server.cpp line 61), buffer is declared as char buffer[1024], giving valid indices 0–1023. Line 66 reads from the client socket with read(clientFd, buffer, 1024), which can return up to 1024 bytes. Line 67 then constructs std::string line(buffer), which invokes the const char* constructor. This constructor scans forward from the pointer looking for a null byte to determine the string's length. If read() returns exactly 1024, all 1024 bytes of buffer are filled with data and no null byte exists within the array; the constructor continues scanning past buffer[1023] into adjacent stack memory (undefined behavior).

The specific code path that triggers it

The client in kilo.c sends commands via send(serverFd, "get", 1024, 0), which sends a full 1024 bytes. When this 1024-byte payload arrives at the server in a single read() call, n == 1024 and buffer[0]..buffer[1023] are all occupied by data from the network. The string line(buffer) call on the very next line then reads past the array boundary searching for '\0'.

Why existing code doesn't prevent it

There is no null-termination step between read() and the string constructor. Unlike the inner loop in kilo.c's receiveFile() (which does buffer[n] = '\0'), threadFunc has no such guard. The while loop condition only checks n > 0, which is satisfied when n == 1024, so no early exit occurs.

What the impact would be

When n == 1024, the string constructor performs an out-of-bounds read on the stack, which is undefined behavior under the C++ standard. In practice this corrupts the string's length and content, causing the subsequent comparisons line == "exit" and line == "get" to fail even when the intended command was sent, breaking the server's command dispatch entirely. Depending on stack layout, it can also trigger crashes or security vulnerabilities.

How to fix it

Option 1: Limit read to 1023 bytes and null-terminate: change line 66 to while ((n = read(clientFd, buffer, 1023)) > 0) and add buffer[n] = '\0' before the string construction on line 67. Option 2: Use the length-aware constructor: std::string line(buffer, n), which does not require a null terminator and constructs exactly n bytes.

Step-by-step proof

  1. Client executes send(serverFd, "get", 1024, 0), sending a 1024-byte packet (3 bytes "get" + 1021 bytes from .rodata).
  2. Server's threadFunc calls read(clientFd, buffer, 1024); TCP delivers all 1024 bytes in one read; n = 1024.
  3. buffer[0]=='g', buffer[1]=='e', buffer[2]=='t', buffer[3]=='\0' (from the literal), buffer[4..1023] = arbitrary .rodata bytes.
  4. string line(buffer) invokes the const char* constructor, scans from buffer[0] and finds '\0' at buffer[3], stopping there.
  5. In this specific protocol, the embedded null at position 3 happens to terminate the string correctly — but only because the client accidentally transmits a null via the send() over-read bug. If any client sends 1024 bytes of purely non-null data (binary payload, or if the adjacent .rodata byte after "get" is not null), the constructor reads past buffer[1023] off the end of the stack array, causing UB and possible crash.

🔬 also observed by staging-devin-ai-integration


if (line == "exit"){
close(clientFd);
}
Comment on lines +66 to +71

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Buffer not null-terminated before string construction.

read() returns the number of bytes read, but the buffer isn't null-terminated before constructing std::string line(buffer). If exactly 1024 bytes are read, the string constructor reads past the buffer.

🐛 Proposed fix
 while ((n = read(clientFd, buffer, 1024)) > 0){
+    buffer[n] = '\0';
     string line(buffer);
     
-    if (line == "exit"){
+    if (line == "exit" || line == "exit\n"){
         close(clientFd);
+        break;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server.cpp` around lines 66 - 71, The code constructs std::string line from
buffer without using the actual byte count returned by read(), so it can read
past the buffer; use the read byte count to create the string (e.g., construct
line with the length n via std::string(buffer, n)) or ensure buffer is sized n+1
and explicitly null-terminate with buffer[n] = '\0' before creating line; update
the loop around read(), the buffer variable and the line construction (and
close(clientFd) handling in that block if needed) to use one of these safe
approaches.

else if (line == "get"){
cout << "Get Received" << endl;
sendFile(clientFd, readFile());
}
}
return NULL;
}
Comment on lines +66 to +78

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Server thread doesn't close clientFd on normal read loop exit

In server.cpp:66-77, the while loop exits when read() returns 0 (client disconnected) or -1 (error). In neither case is clientFd closed by the thread (it's only closed on an explicit "exit" message at line 70). After the thread returns, the file descriptor leaks since the thread is detached and no one else closes it.

Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground


//handleSigInt - action performed when user types ctrl-C
void handleSigInt(int unused __attribute__((unused))){
exit(0);
}

//main server program
int main(void){

//setup SIGINT signal handler
signal(SIGINT, handleSigInt);

// Create server socket
int serverFd;
if ((serverFd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
std::cerr << "Error: Can't create socket." << std::endl;
return 1;
}

// Set options for socket
int val;
if (setsockopt(serverFd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(int))){
Comment on lines +99 to +100

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Uninitialized value passed to setsockopt for SO_REUSEADDR

int val; at line 99 is never initialized before being passed to setsockopt() at line 100. SO_REUSEADDR checks whether the pointed-to int is non-zero to enable the option. Using an uninitialized value is undefined behavior and may non-deterministically fail to set SO_REUSEADDR.

Suggested change
int val;
if (setsockopt(serverFd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(int))){
int val = 1;
if (setsockopt(serverFd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(int))){
Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

std::cerr << "Error: Can't reuse socket." << std::endl;
return 2;
}
Comment on lines +99 to +103

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Uninitialized variable passed to setsockopt.

val is declared but never initialized. SO_REUSEADDR expects a non-zero value to enable the option.

🐛 Proposed fix
 // Set options for socket
-int val;
+int val = 1;
 if (setsockopt(serverFd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(int))){
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
int val;
if (setsockopt(serverFd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(int))){
std::cerr << "Error: Can't reuse socket." << std::endl;
return 2;
}
int val = 1;
if (setsockopt(serverFd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(int))){
std::cerr << "Error: Can't reuse socket." << std::endl;
return 2;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server.cpp` around lines 99 - 103, The variable `val` is uninitialized before
being passed to setsockopt for SO_REUSEADDR; initialize it to a non-zero value
(e.g., int val = 1) before the call that uses serverFd and setsockopt so the
reuse address option is actually enabled and avoid undefined behavior.

Comment on lines +99 to +103

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 In server.cpp line 99, int val; is declared without initialization before being passed to setsockopt(serverFd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(int)), meaning SO_REUSEADDR may be silently disabled (if val happens to be 0 on the stack) or trigger undefined behavior. Fix by changing the declaration to int val = 1; to ensure the option is properly enabled.

Extended reasoning...

What the bug is and how it manifests

At server.cpp line 99, the code declares int val; without initializing it, then immediately passes &val to setsockopt(serverFd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(int)) at line 100. The SO_REUSEADDR socket option uses the integer value pointed to by the option argument to determine whether to enable (non-zero) or disable (zero) the option. With val uninitialized, its value is indeterminate — undefined behavior in C++.

The specific code path that triggers it

The sequence is: socket() creates serverFd at ~line 95, then lines 99-103:

int val;   // uninitialized
if (setsockopt(serverFd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(int))){
    std::cerr << "Error: Can't reuse socket." << std::endl;
    return 2;
}

This is in main(), which runs on a fresh stack frame. The local variable val is never assigned before its address is passed to setsockopt.

Why existing code doesn't prevent it

There is no initializer on the declaration and no assignment before the setsockopt call. The error check only validates that setsockopt itself didn't fail (return non-zero errno), but setsockopt with a zero value succeeds without error — it simply disables the option rather than enabling it. So the error check provides no protection against the wrong value being passed.

What the impact would be

SO_REUSEADDR is intended to allow the server to bind to a port that's still in TIME_WAIT state after a previous run. Without it enabled, restarting the server quickly after stopping it will fail with "Address already in use" (EADDRINUSE) until the OS releases the port (typically 60–120 seconds). On most platforms, freshly allocated stack frames in main() are zeroed by the OS when a new process starts (stack pages come from zero-filled physical pages), meaning val will frequently be 0, disabling SO_REUSEADDR in practice. Additionally, reading an uninitialized variable is formally undefined behavior in C++, which compilers are permitted to optimize in unexpected ways.

How to fix it

Change line 99 from int val; to int val = 1;. This is the canonical idiom for enabling socket options with setsockopt and directly expresses the intent.

Step-by-step proof

  1. Server process starts; main() stack frame is allocated. On Linux, the first access to a fresh stack page gets a zero-filled physical page from the OS.
  2. int val; at line 99 reserves space on the stack. Since this is the first call frame in a fresh process, the stack memory is likely zero-filled, so val == 0.
  3. setsockopt(serverFd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(int)) is called with the value 0.
  4. The kernel sets SO_REUSEADDR to 0 (disabled) on serverFd. setsockopt returns 0 (success) because the call itself was valid.
  5. The error check passes (setsockopt returned 0, so the if-body is not entered).
  6. Server proceeds to bind() and listen() with SO_REUSEADDR disabled.
  7. User stops and immediately restarts the server. bind() fails with EADDRINUSE because the previous socket is in TIME_WAIT. The server prints "Error: Can't bind socket to port." and exits.

🔬 also observed by coderabbitai


// Configure addr and bind
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = htons(10001);
if (bind(serverFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1){
cerr << "Error: Can't bind socket to port." << endl;
return 3;
}

// Listen for client connections
if (listen(serverFd, 20) < 0){
cerr << "Error: Can't listen for clients." << endl;
return 4;
}

// Get name and port assigned to server
char *name = new char[1024];
struct sockaddr_in infoAddr;
socklen_t len = sizeof(infoAddr);
gethostname(name, 1024);
getsockname(serverFd, (struct sockaddr *)&infoAddr, &len);

// Report name and port
cout << name << ":" << ntohs(serverAddr.sin_port)<< endl;
delete name;
Comment on lines +122 to +130

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Memory allocation/deallocation mismatch and missing error check.

  1. new char[1024] must be paired with delete[], not delete. This causes undefined behavior.
  2. gethostname() return value is not checked.
🐛 Proposed fix
 // Get name and port assigned to server
-char *name = new char[1024];
+char name[1024];
 struct sockaddr_in infoAddr;
 socklen_t len = sizeof(infoAddr);
-gethostname(name, 1024);
+if (gethostname(name, 1024) == -1) {
+    cerr << "Error: Can't get hostname." << endl;
+}
 getsockname(serverFd, (struct sockaddr *)&infoAddr, &len);

 // Report name and port
 cout << name << ":" << ntohs(serverAddr.sin_port)<< endl;
-delete name;
🧰 Tools
🪛 Cppcheck (2.20.0)

[error] 130-130: Mismatching allocation and deallocation

(mismatchAllocDealloc)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server.cpp` around lines 122 - 130, The code allocates a buffer "name" with
new char[1024] but frees it with delete (should be delete[]), and it calls
gethostname(name, 1024) without checking the return value; change the
deallocation to delete[] name (or better yet use a std::string or
std::vector<char> to manage the buffer) and add error checking for gethostname
(check for -1 / nonzero return and handle/log the error before using the
buffer); keep the existing getsockname(serverFd, ...) and serverAddr/ntohs usage
but ensure you only print name if gethostname succeeded.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 delete instead of delete[] for array allocation

delete name; at line 130 should be delete[] name; since name was allocated with new char[1024] at line 122. Using delete instead of delete[] for array allocations is undefined behavior.

Suggested change
delete name;
delete[] name;
Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Comment on lines +122 to +130

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 In server.cpp at line 122, name is allocated with new char[1024] (array form) but freed at line 130 with delete name (scalar form); the C++ standard requires delete[] for array allocations. Using scalar delete on an array-allocated pointer is undefined behavior that may corrupt heap allocator internal state. Fix: change delete name to delete[] name, or use a stack-allocated char name[1024] and remove the delete entirely.

Extended reasoning...

What the bug is and how it manifests

At server.cpp line 122, the code allocates a hostname buffer as an array: char *name = new char[1024]. This is the array form of new, which allocates 1024 chars and requires a matching delete[] to free them. However, at line 130, the code frees the buffer with delete name — the scalar form of delete. This mismatch is explicitly undefined behavior per the C++ standard (ISO/IEC 14882, [expr.delete]).

The specific code path that triggers it

The code path is straightforward and executes on every server startup, unconditionally. After binding and listening, the server calls gethostname(name, 1024) and getsockname(...) to report the server address, then immediately frees the buffer with delete name before entering the accept() loop. This sequence always runs.

Why existing code doesn't prevent it

There is no mechanism in C++ that prevents a scalar delete from being called on an array-allocated pointer at the language level. The compiler may warn (some do), but the code compiles and links. The runtime has no guard: delete calls the global operator delete, which does not know whether the pointer originated from new or new[]. For types with trivial destructors (like char), the observable behavior on common implementations is often identical, but the C++ standard explicitly makes this undefined behavior regardless.

What the impact would be

On implementations where new[] stores a count before the allocation (a common implementation detail for tracking how many destructors to call on delete[]), scalar delete passes the raw pointer to the allocator, which may then misinterpret allocation metadata — potentially corrupting the heap's free-list bookkeeping. While char has a trivial destructor (so no destructor miscounting occurs), the pointer passed to the allocator's dealloc function may differ by sizeof(size_t) bytes from what the allocator expects, depending on the ABI. This can cause silent heap corruption or a crash in subsequent allocations. Static analyzers including Cppcheck flag this as a definite error (mismatchAllocDealloc).

How to fix it

Two options: (1) Change line 130 from delete name to delete[] name — the minimal correct fix. (2) Replace the heap allocation entirely with a stack array: char name[1024]; and remove the delete line — this is simpler and avoids the issue altogether. Since name is a fixed-size scratch buffer that never escapes the function, a stack allocation is appropriate and preferred.

Step-by-step proof

  1. Server starts; main() runs.
  2. Line 122: char *name = new char[1024] — operator new[] allocates 1024 bytes (possibly with hidden bookkeeping overhead); the returned pointer name points to the usable region.
  3. Lines 123–129: gethostname and getsockname populate name; the hostname is printed.
  4. Line 130: delete name — operator delete (scalar) is called with the pointer. On an implementation that stores array size metadata before the block, delete passes the wrong address to the underlying allocator, corrupting heap metadata.
  5. The corruption may not manifest immediately (the server proceeds to accept()), but any subsequent dynamic allocation (e.g., in the pthread library or std::string operations in threadFunc) may read corrupt heap metadata, leading to unpredictable behavior or crashes.


// Connect a client
struct sockaddr_in cliAddr;
len = sizeof(cliAddr);
while (true){
int clientFd = accept(serverFd, (struct sockaddr *)&serverAddr, &len);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 accept() overwrites serverAddr instead of populating cliAddr

accept(serverFd, (struct sockaddr *)&serverAddr, &len) at server.cpp:136 passes &serverAddr (the server's address) instead of &cliAddr (declared at line 133 for this purpose). This overwrites the server's address structure with the connecting client's address information, corrupting server state.

Suggested change
int clientFd = accept(serverFd, (struct sockaddr *)&serverAddr, &len);
int clientFd = accept(serverFd, (struct sockaddr *)&cliAddr, &len);
Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground


// Create thread to deal with client
pthread_t thread;
pthread_create(&thread, NULL, threadFunc, (void *)&clientFd);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Race condition: address of loop-local variable passed to thread

pthread_create(&thread, NULL, threadFunc, (void *)&clientFd) at server.cpp:140 passes the address of clientFd, which is a local variable scoped to the while loop body. The main thread immediately loops back and may call accept() again, overwriting clientFd before the new thread dereferences the pointer at server.cpp:61 (int clientFd = *(int*)args). This race condition can cause multiple threads to use the same (wrong) file descriptor.

Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

}
Comment on lines +135 to +141

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Race condition: clientFd address reused across threads.

&clientFd is passed to pthread_create, but the next loop iteration overwrites clientFd before the thread may have copied it. This causes threads to receive wrong or identical file descriptors.

🐛 Proposed fix
 while (true){
-    int clientFd = accept(serverFd, (struct sockaddr *)&serverAddr, &len);
+    int *clientFd = new int;
+    *clientFd = accept(serverFd, (struct sockaddr *)&cliAddr, &len);
+    if (*clientFd < 0) {
+        delete clientFd;
+        continue;
+    }

     // Create thread to deal with client
     pthread_t thread;
-    pthread_create(&thread, NULL, threadFunc, (void *)&clientFd);
+    pthread_create(&thread, NULL, threadFunc, (void *)clientFd);
 }

Then in threadFunc, free the allocated memory:

void *threadFunc(void *args){
    int clientFd = *(int*)args;
    delete (int*)args;  // Free the allocated int
    // ... rest of function

Also note: Line 136 passes &serverAddr to accept() but should pass &cliAddr to receive the client's address.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server.cpp` around lines 135 - 141, The loop passes the address of the stack
variable clientFd into pthread_create and also passes &serverAddr to accept,
causing a race and wrong peer addr; fix by 1) change the accept call to pass
&cliAddr (the sockaddr for the client) instead of &serverAddr, and 2) allocate a
new int on the heap for each accepted socket (e.g. new int(clientFd)) and pass
that pointer to pthread_create so each thread gets its own copy, then in
threadFunc dereference and delete the heap pointer to free it after reading the
fd; keep using accept, pthread_create, clientFd, cliAddr and threadFunc names
when editing.

Comment on lines +135 to +141

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Two bugs in the accept loop in server.cpp lines 135–141: (1) accept() passes &serverAddr instead of &cliAddr, overwriting the server's own address struct on every incoming connection — cliAddr is declared at line 133 specifically for this purpose but is never used; (2) pthread_create receives &clientFd where clientFd is a stack-local loop variable, so the main thread can overwrite it before the spawned thread dereferences it at line 61, causing threads to operate on the wrong file descriptor. Fix by passing &cliAddr to accept(), and heap-allocating a new int(clientFd) per connection to pass to pthread_create, freeing it inside threadFunc after reading.

Extended reasoning...

Bug 1 — accept() passes &serverAddr instead of &cliAddr

The accept call on line 136 is written as:
int clientFd = accept(serverFd, (struct sockaddr *)&serverAddr, &len);
The intent of the second argument is to receive the connecting client's address. The code even declares cliAddr at line 133 and initializes len = sizeof(cliAddr) at line 134 precisely for this purpose, but then passes &serverAddr instead. Every time a client connects, the kernel overwrites serverAddr with the client's address family, IP, and port. While serverAddr happens to not be read again after line 129 (where the port is printed), the server's own binding address is silently corrupted on each accepted connection. If the code is ever extended to use serverAddr again — or if a code path that binds/reconnects is added — it will find garbage. cliAddr is also a dead variable that carries no useful data.

Bug 2 — Race condition: loop-local clientFd stack address passed to pthread_create

On line 140, pthread_create receives (void *)&clientFd where clientFd is declared inside the while(true) body on line 136. In C++, a variable declared inside a loop body occupies a fixed stack slot that is reused on every iteration. The main thread calls pthread_create and then immediately loops back to the top of the while(true), where accept() assigns a new value to the same stack address (clientFd). Meanwhile, the spawned thread has not necessarily yet executed line 61: int clientFd = (int)args. If the main thread reaches accept() — and especially if another connection arrives — before the new thread reads the pointed-to int, the thread reads the wrong file descriptor (the one from the next accept() call). Multiple threads can end up sharing a single fd, and the fd intended for one of them is leaked with no close().

Why existing code does not prevent the race

There is no synchronization between pthread_create returning and the new thread reading its argument. pthread_create only guarantees the thread will eventually start; it places no ordering constraint on when the thread first executes. On a busy server or a system where the scheduler defers the new thread, the main loop is free to overrun clientFd before the thread reads it. This is a textbook TOCTOU race described in every pthreads tutorial.

Impact

Under concurrent load (two clients connecting in rapid succession), the second client's fd may be read by the first thread, causing both threads to interact with the same client while the other client is abandoned. Depending on timing, this also causes double-close of a file descriptor — undefined behavior that can silently corrupt subsequent open() or accept() calls that reuse the same fd number.

How to fix both bugs

  1. Change line 136 to: int clientFd = accept(serverFd, (struct sockaddr *)&cliAddr, &len);
  2. Heap-allocate the fd for each accepted connection and pass that pointer:
    int *fdPtr = new int(clientFd);
    pthread_create(&thread, NULL, threadFunc, (void )fdPtr);
    Then inside threadFunc, dereference and free: int clientFd = (int)args; delete (int
    )args;

Step-by-step proof of the race

  1. Connection A arrives; accept() returns fd=5, stored in clientFd at stack address S.
  2. pthread_create is called with arg = S (value 5 not yet read by new thread).
  3. Main thread loops back instantly; connection B arrives; accept() stores fd=6 at stack address S (same slot), overwriting the 5.
  4. Thread for connection A finally executes int clientFd = (int)args, reading S — now 6.
  5. Both the original thread for A and the new thread for B dereference fd 6; fd 5 is never read and never closed — file descriptor leak plus wrong-client interaction.

return 0;
}
4 changes: 4 additions & 0 deletions test
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
this is a test file
these are some words
these are some more
bye!
Empty file added transfer
Empty file.