courses Memory: who owns what 5/5

Cache Corruption

Capstone: a tiny cache hides three different memory bugs. Direct the junior to fix all of them.

Lesson

Capstone: three bugs, three different symptoms

This tiny cache copies keys and values, evicts the oldest entry when full, and can dump its contents into a caller's buffer. It hides three independent bugs:

  1. a leak
  2. a use-after-free
  3. an off-by-one in a bounds check

Each one is found by a different signal: a failing test, the sanitizer, or a hidden test.

Your mission

Work like a tech lead. One bug per instruction. After each patch, run the tests and the sanitizer again and read what changed. Do not move on until the report is different, and do not accept a patch you cannot explain.

What to watch for

  • Each slot owns two allocations.
  • cache_get returns a pointer that is only valid until the next cache_put. What if the caller passes it straight back in?
  • snprintf returns the length it would have written. What does it mean when that equals the space left?

Try this exercise

Create a free account to tell the AI junior what to change, run the tests and the memory checker, and earn a completion code you can verify.

Starting code
cache.c
#include "cache.h"#include <stdio.h>#include <stdlib.h>#include <string.h> void cache_init(Cache *c) {    c->count = 0;    memset(c->slots, 0, sizeof c->slots);} const char *cache_get(const Cache *c, const char *key) {    for (size_t i = 0; i < c->count; i++) {        if (strcmp(c->slots[i].key, key) == 0) return c->slots[i].val;    }    return NULL;} void cache_put(Cache *c, const char *key, const char *val) {    for (size_t i = 0; i < c->count; i++) {        if (strcmp(c->slots[i].key, key) == 0) {            free(c->slots[i].val);            c->slots[i].val = strdup(val);            return;        }    }    if (c->count == CACHE_CAP) {        free(c->slots[0].key);        for (size_t i = 1; i < c->count; i++) c->slots[i - 1] = c->slots[i];        c->count--;    }    c->slots[c->count].key = strdup(key);    c->slots[c->count].val = strdup(val);    c->count++;} size_t cache_dump(const Cache *c, char *buf, size_t n) {    if (n == 0) return 0;    size_t used = 0;    for (size_t i = 0; i < c->count; i++) {        int w = snprintf(buf + used, n - used, "%s=%s;", c->slots[i].key, c->slots[i].val);        if (w < 0 || (size_t)w > n - used) {            used = n - 1;            break;        }        used += (size_t)w;    }    buf[used] = '\0';    return used;} void cache_free(Cache *c) {    for (size_t i = 0; i < c->count; i++) {        free(c->slots[i].key);        free(c->slots[i].val);    }    c->count = 0;}
cache.h
#ifndef CACHE_H#define CACHE_H#include <stddef.h> #define CACHE_CAP 3 typedef struct {    char *key;    char *val;} Slot; typedef struct {    Slot slots[CACHE_CAP];    size_t count;} Cache; void cache_init(Cache *c); /* Stores private copies of key and val. Updates the value if the key exists. * When the cache is full the OLDEST entry is evicted. */void cache_put(Cache *c, const char *key, const char *val); /* Returns the value for key, or NULL. The pointer stays valid until the next cache_put. */const char *cache_get(const Cache *c, const char *key); /* Writes "k=v;k=v;" into buf. The result is ALWAYS NUL-terminated and truncated to fit in n bytes. * Returns the number of characters written, not counting the NUL. */size_t cache_dump(const Cache *c, char *buf, size_t n); /* Frees everything the cache owns. */void cache_free(Cache *c);#endif