The current implementation stores its state in a single static char *leftover.
That variable is shared across every call to tci_getline, regardless of which
file descriptor is passed. Open two files simultaneously and alternate calls
— the second call overwrites the state the first one saved.
The problem
int fd1 = open("file1.txt", O_RDONLY);
int fd2 = open("file2.txt", O_RDONLY);
char *a = tci_getline(fd1); /* reads from file1, saves leftover for fd1 */
char *b = tci_getline(fd2); /* reads from file2 — overwrites leftover */
char *c = tci_getline(fd1); /* reads from file1 — leftover is wrong */On the third call, leftover holds whatever was saved after reading
fd2, not fd1. The result is either garbage or a crash.
The fix
Replace the single pointer with an array indexed by file descriptor:
#define FD_MAX 1024
static char *leftover[FD_MAX];Every access to leftover becomes leftover[fd]. The rest of the
function is unchanged:
char *tci_getline(int fd)
{
char buf[BUFFER_SIZE + 1]; /* +1 for null terminator */
char *nl;
char *tmp;
ssize_t bytes;
if (fd < 0 || fd >= FD_MAX) /* reject out-of-range descriptors */
return (NULL);
if (!leftover[fd])
leftover[fd] = tci_strdup(""); /* initialise slot on first call for this fd */
while (1) {
nl = tci_strchr(leftover[fd], '\n');
if (nl)
return (extract_line(&leftover[fd], nl)); /* line ready — no read needed */
bytes = read(fd, buf, BUFFER_SIZE);
if (bytes <= 0)
return (flush_leftover(&leftover[fd])); /* EOF or error */
buf[bytes] = '\0'; /* read() does not null-terminate */
tmp = leftover[fd];
leftover[fd] = strjoin(leftover[fd], buf); /* accumulate new chunk */
free(tmp); /* free old leftover after joining */
}
}FD_MAX 1024 matches the default Linux open file descriptor limit. If
fd is out of range — negative or ≥ 1024 — return NULL immediately.
The static array is zero-initialised: leftover[0] through
leftover[1023] are all NULL at program start. Each fd accumulates its
own state independently.
Memory at close
FILE * — the stdio stream type used by fgets and getline —
is an opaque struct that owns its internal buffer. fclose() frees
that buffer because the state is attached to the object being closed.
tci_getline works with a raw int fd. There is no wrapper struct,
no object to attach state to. close(fd) tells the kernel to release
the file descriptor; it has no knowledge of leftover[fd] in user
space. That is the tradeoff of operating below stdio: the caller
controls the fd, so the caller is also responsible for consuming the
file fully before closing it. Reading every line until NULL is the
normal use case — and that is exactly what flush_leftover handles.
Run the multi-fd suite
make re
bash test.shThe multi-fd suite opens two fixture files, reads them interleaved, and checks that each call returns the correct line from the correct file. All tests should now pass.