Logic Bugs
Nothing goes out of bounds
Section titled “Nothing goes out of bounds”The till needed an oversized name before it got its bookkeeping wrong. A parcel depot can make a rather different mistake: accept a perfectly ordinary request from someone who has no business making it. There needn’t be anything unusual about the request itself. Asking for locker 4 is fine if locker 4 belongs to you.
In the requests below, the program stays inside its buffers and both of its checks work. The problem is the question neither check asks: does this locker belong to the signed-in courier? We’ll compare those checks with the depot’s stated rules, then see what happens when Sam asks for the master key.
A depot that publishes its own rules
Section titled “A depot that publishes its own rules”Before calling a request wrong, we need to know what the depot is supposed to allow. Conveniently, this program prints its own rules. We can compare them with the code instead of guessing what its author meant.
In the Ubuntu terminal, create the working directory if needed and move into it:
mkdir -p ~/underdunn-course/bug-classes ~/underdunn-course/notescd ~/underdunn-course/bug-classesSave the complete program below as depot.c. Keep the terminal in this directory for the build commands; no source file from an earlier lesson is needed.
/* * A parcel depot with six lockers. * * The rules the depot is supposed to follow are printed by the `rules` * command, so you do not have to infer them. */#include <stdio.h>#include <stdlib.h>#include <string.h>
#define LOCKERS 6
static const char *contents[LOCKERS] = { "(empty)", "parcel 8841, for Nadia", "parcel 8842, for Omar", "(empty)", "depot master key", "parcel 8843, for Priya",};
static const char *assigned_to[LOCKERS] = { "sam", "sam", "ines", "ines", "depot", "sam",};
static char courier[32] = "";
static void rules(void){ puts(" 1. A courier must sign in before opening anything."); puts(" 2. A courier may open a locker assigned to them."); puts(" 3. Locker 4 is assigned to depot staff."); puts(" Assignments: 0 sam 1 sam 2 ines 3 ines 4 depot 5 sam");}
static void open_locker(int n){ if (courier[0] == '\0') { puts(" denied: not signed in"); return; } if (n < 0 || n >= LOCKERS) { puts(" denied: no such locker"); return; } printf(" locker %d: %s\n", n, contents[n]);}
int main(void){ char line[128];
setvbuf(stdout, NULL, _IONBF, 0); puts("Parcel depot. Commands: rules, signin <name>, open <n>, quit");
while (fgets(line, sizeof line, stdin)) { line[strcspn(line, "\n")] = '\0';
if (strcmp(line, "rules") == 0) { rules(); } else if (strncmp(line, "signin ", 7) == 0) { snprintf(courier, sizeof courier, "%s", line + 7); printf(" signed in as %s\n", courier); } else if (strncmp(line, "open ", 5) == 0) { open_locker(atoi(line + 5)); } else if (strcmp(line, "quit") == 0) { break; } else if (line[0] != '\0') { puts(" unknown command"); } } return 0;}The parser removes the newline from fgets, compares the command names, and uses atoi to turn the locker text into an integer. snprintf copies the supplied courier name into its fixed buffer with a size limit. setvbuf makes the replies appear immediately. Those details keep this example’s input handling bounded; none of them performs the missing ownership check.
gcc -g -O0 -fno-omit-frame-pointer -Wall -Wextra depot.c -o depot-Wall -Wextra enables the warning groups used later on this page. Warnings can point us toward suspicious code, but they do not know the depot’s authorization rules.
Run ./depot and enter:
rulesquitParcel depot. Commands: rules, signin <name>, open <n>, quit 1. A courier must sign in before opening anything. 2. A courier may open a locker assigned to them. 3. Locker 4 is assigned to depot staff. Assignments: 0 sam 1 sam 2 ines 3 ines 4 depot 5 samThat’s the intended machine, written down by the machine itself. Sam works here and has three lockers. We’ll compare those four rules with what open_locker actually allows.
Sam asks for someone else’s locker
Section titled “Sam asks for someone else’s locker”Sam signs in and collects the parcels assigned to him, which is an unremarkable morning. Run ./depot and enter:
signin samopen 0open 1quit signed in as sam locker 0: (empty) locker 1: parcel 8841, for NadiaNow the same command with a different number. Run ./depot and enter:
signin samopen 4quit signed in as sam locker 4: depot master keyNo input was malformed. Nothing overflowed, the number was in range, and open is a command Sam is entitled to use. But compare the rules with the checks:
- The rules say locker 4 belongs to depot staff, and a courier may open a locker assigned to them.
- The code checks that somebody is signed in and that the locker exists. It never looks at who the locker belongs to.
The checks that still work
Section titled “The checks that still work”Opening locker 4 doesn’t tell us whether we can skip sign-in or request a locker that doesn’t exist. Those are separate checks in open_locker, and we can test them without changing anything else. Run ./depot and enter:
open 4quitRun it again and enter:
signin samopen 6open -1open 99quit denied: not signed in signed in as sam denied: no such locker denied: no such locker denied: no such lockerSo the bound is real on both sides. You need some sign-in, and the locker number has to be one of the six that exist. There’s no command that writes, either, which you can confirm by trying one. Run ./depot and enter:
signin samwrite 4 helloopen 4quit signed in as sam unknown command locker 4: depot master keyWe can now describe the access without making the master key stand in for the whole bug:
- A signed-in user can read any of the six lockers, rather than only their assigned ones.
- The range check still keeps the index between zero and five.
- There is no command to replace a locker’s contents. The rejected
writecommand agrees with the source.
Sam has more access than he should, but not unrestricted access to the depot’s memory.
The instruments have nothing to say
Section titled “The instruments have nothing to say”We can check the same requests with tools that look for memory errors and certain undefined operations. AddressSanitizer instruments memory accesses, while UndefinedBehaviorSanitizer adds checks for selected operations whose behavior C does not define. Valgrind’s Memcheck observes memory use while the program runs. None of them knows the depot’s ownership rules, though. Here’s the instrumented build:
gcc -g -O0 -fsanitize=address,undefined depot.c -o depot-sanRun ./depot-san and enter:
signin samopen 4open 99quitThen ask the shell what status it exited with:
echo "exit $?" signed in as sam locker 4: depot master key denied: no such lockerexit 0printf 'signin sam\nopen 4\nquit\n' | valgrind -q --error-exitcode=9 ./depot > /dev/nullecho "exit $?"exit 0None of the tools reports an error in these runs. The source explains why the unauthorized read can escape their attention: locker 4 is an existing element of contents, and printing it uses the same operation as printing Sam’s own locker. The missing ownership comparison doesn’t require an invalid memory access.
One hint, and it came from the compiler
Section titled “One hint, and it came from the compiler”You built with -Wall -Wextra, so you already saw this:
depot.c:22:20: warning: ‘assigned_to’ defined but not used [-Wunused-variable]The table of who owns what is in the program and nothing reads it. That’s a fairly direct clue that an authorization decision isn’t being made.
If another command used assigned_to to print a roster, the warning could disappear while open_locker still omitted the check. So enjoy the convenient clue, but follow it back to the ownership decision.
The repair, and what it confirms
Section titled “The repair, and what it confirms”Four lines, using the table that was already there:
if (strcmp(assigned_to[n], courier) != 0) { puts(" denied: not your locker"); return; }cp depot.c depot-fixed.c # then add the check above, after the range checkgcc -g -O0 -Wall -Wextra depot-fixed.c -o depot-fixedRun ./depot-fixed and enter:
signin samopen 1open 4quitRun it again and enter:
signin depotopen 4quit signed in as sam locker 1: parcel 8841, for Nadia denied: not your locker signed in as depot locker 4: depot master keySam keeps his own lockers, loses the master key, and depot staff are unaffected. The unused-variable warning goes away at the same time, for the same reason.
The new comparison ties the request to the assignment table without changing the buffer layout or the range check.
There is still a separate problem with signing in. The repair controls access under the supplied name, but this toy program lets anyone type signin depot. A real sign-in system would also have to establish that the caller is entitled to that identity.
Keep the identity fixed as Sam when testing the ownership check, so we’re comparing the same request before and after the repair.
Knowing a locker number isn’t permission
Section titled “Knowing a locker number isn’t permission”This is a missing authorization check: knowing which resource to request is being treated as permission to read it. CWE-639 describes this pattern when a user-controlled key selects another user’s resource. Our key is a locker number, but the same question applies to a document identifier or an account number: where does the program connect that resource to the caller’s permission?
Knowledge check
Question 1 of 3
Practice complete
You've checked the key ideas. Revisit any explanation you need, then continue when it makes sense.
When the results don’t match
Section titled “When the results don’t match”- If
open 4is denied on your first build, check you haven’t already added the repair. The unrepairedopen_lockerhas exactly twoifstatements. - If every command reports
unknown command, your input lines are picking up carriage returns. Useprintfwith explicit\nas shown rather than pasting from a document. - If the build fails on
atoi, add#include <stdlib.h>. The listing above has it. - If you see no
assigned_towarning, you built without-Wextra. The warning is incidental to the lesson and its absence changes nothing about the program’s behaviour.
To recover a known state, rebuild from the listing above. The depot writes nothing to disk and keeps no state between runs.
Check your understanding
Section titled “Check your understanding”Describe the access Sam gained and the checks that still limit it. Then use the assignment table to answer the underlying question: if the requests caused no memory error, what makes this a vulnerability?
Compare your reasoning
The basis is the depot’s own published rules. Rule 2 grants a courier access to lockers assigned to them, the assignment table puts locker 4 with depot staff, and Sam reached it while signed in as himself. That is the program contradicting its own specification, and it needed no malformed input and no memory error to do it.
If you found yourself reaching for impact instead, notice that you don’t actually know what the master key opens. Neither do I. That is a question about the depot, not about the program, and the program alone cannot answer it.
Beyond the six lockers
Section titled “Beyond the six lockers”The depot’s range check is the only thing standing between a courier and memory that isn’t a locker at all. You supplied 99 and it said no.
The next lesson takes that check away and asks what a read at an index nobody validated is actually worth, which is where the sanitizers start earning their place.