Audit 2.37 - #111
Audit 2.37#111Dylan-Brotherston wants to merge 55 commits into
Conversation
do_tests.sh exited 0 even when tests failed, so a broken suite would have reported success. A test whose output could not be checked was also lost when xargs stopped early, and single_test.sh recorded unreviewed output as though it had been accepted and could block waiting for an answer that was never coming. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
incremental_compilation.sh had been mangled by a source formatter and could not run; its accepted output was the resulting error. print_path.sh used continue outside a loop. The MemorySanitizer variant of uninitialized-array-element-if compared uninitialized stack contents, which differ on every run, and was the suite's permanent failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Runs pylint, mypy and a compile with Python warnings made fatal, shellcheck on every script, and a -Wall -Wextra -Werror compile of the C embedded in every student program across each combination of sanitizers. dcc refuses to compile a program if that code produces any diagnostic, so a warning enabled by default in a future compiler would otherwise break every compilation. Replaces the disabled prospector configuration. A GitHub Actions workflow runs it alongside the end-to-end suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The project had no unit tests. These cover the C string-literal encoder (round-tripped through clang), the compiler-message parser, explanation selection and label uniqueness, option parsing, the source-line lookup, the output-difference explanations, value display and the call traceback. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A failed compile left a temporary file in /tmp every time and leaked its file descriptor. A response file was expanded by dcc and then again by clang, so every argument was doubled. -pthread was not recognised and -l never disabled the valgrind sanitizer, because the test for it examined the wrong flag. --help printed a blank line, leaving the generated man page empty. Paths spliced into the embedded C were not escaped, so a quote, a backslash or a trigraph in an install path produced code that would not compile. A retry path rebuilt the support code without its sanitizer flags, and the no-main message was produced by an unanchored substring match. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five explanation entries shared a regular expression with another, so the label that names their test file was not unique and one pair was an exact duplicate. Four reproduce programs did not produce the error they were meant to demonstrate. The uninitialized-variable entry matched wording gcc dropped in gcc 10, so it never fired. A stray reset escape was emitted in uncoloured output, and helper scripts were given colour codes because a function was passed where a boolean was expected. A mistake in one template replaced the compiler's message with a Python traceback; it now falls back to the plain message. Notes quoting system headers and include chains are no longer shown to the student. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An error on one of the first lines of a file displayed lines from the end of it, because the source lookup indexed with a negative line number. Debug output produced inside gdb was discarded, since the function setting the stream ignored its argument. A very deep stack made gdb walk hundreds of thousands of frames, and truncating it broke the assumption that the outermost frame is main, so the traceback now ends explicitly and the command line is only read when main was reached. A non-integer DCC_DEBUG raised an exception. valgrind output dcc does not recognise is reported rather than discarded, so a renamed message cannot silently switch off a class of checking. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The byte count for excessive output went to a stream directed at /dev/null, a wrong space was never highlighted because bytes were compared with a string, and the non-printable range began at the wrong byte. Extra characters at the end of the expected output produced the message for the opposite problem, or raised an exception. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
To make uninitialized memory recognisable dcc refills the stack after library calls. That refill wrote 256 KB on every printf and on every stdio read, write, seek and close, and because the support code is compiled with pattern initialisation the array was filled twice per call. glibc's stdio functions dirty about 4 KB, so the refill now scans from the top in 4 KB chunks and rewrites only what was disturbed, stopping once eight consecutive chunks are found intact; under valgrind the same scan runs after telling memcheck the memory is defined. The stdio callback reached from inside dcc's own printf no longer scans the same stack a second time. A program printing 300,000 lines: 1.74s to 0.23s with one sanitizer, 69s to 27s with the default pair. Values displayed for uninitialized variables were compared against the previous build on programs designed to defeat the scan and are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
UndefinedBehaviorSanitizer reports were copied into 128-byte buffers, so a long message or any path over about 100 characters lost the source excerpt and the variable values; those buffers are now 4096 bytes and static, as putenv keeps the pointer rather than copying. gettid needs glibc 2.30 and does not exist on macOS. A stack overflow, usually from infinite recursion, produced no explanation at all under AddressSanitizer because the handler faulted again on the exhausted stack; SIGSEGV is now handled on an alternate stack and a fault past the end of the stack is explained. stderr was left fully buffered, so output to it appeared out of order and was lost when a program was stopped. A valgrind error occurring after main returned let the program exit successfully. Forwarded standard input was read with a single read, so a short read looked like lost synchronisation. Restoring a C++ stream twice deleted its buffer twice. Four latent compiler warnings were fixed so the new warning check passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Accepted output which recorded the stray escape, the truncated UndefinedBehaviorSanitizer message, the notes quoting system headers or the off-by-one output limit can no longer be produced and has been removed, so those defects cannot pass again. Three tests needed a version recording clang 19's wording, which was the suite's other standing failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds 37 tests: the exit status of every error class under every combination of sanitizers, the runtime and compile helper interfaces, output checking under a single sanitizer and the messages it produces, stack clearing after deep recursion and large frames, infinite recursion, CPU limits, assertions, leaks under AddressSanitizer, C++ input through cin, multi-file programs, more open files than dcc has stream cookies, and the option handling and temporary-file behaviour this branch fixes. Each was checked by reintroducing the defect and confirming the test fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The helper interfaces documented variable names the code does not use, the sample outputs showed a message format dcc has not produced for years, and the man page examples still showed the fill byte used before 0xaa. Adds the undocumented output-size limit, the conditions which disable the valgrind sanitizer, how the helper programs are found, and how to run the tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It was written to standard output on every release, where any terminal log or CI transcript would capture it. The environment variable now takes precedence over the token file rather than being overwritten by it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Half the processors are used by default, which is a lot of memory on a machine where each test also runs the program under valgrind. DCC_TEST_JOBS now overrides it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…g them The tar of the Python which explains errors, and in dual-sanitizer mode the whole second executable, were written into the generated C as hexadecimal array initializers, so the compiler parsed them on every compilation. They are now assembled into their own object files with .incbin and linked, which takes the generated C from 265 KB and 104 KB down to 72 KB each and leaves it as code rather than data. The size of the tar reaches the valgrind watcher in the environment instead of being written into a command line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The C dcc embeds is identical for every program compiled by the same dcc with the same compiler and options, but it was recompiled at -O3 every time. The object is now kept under $XDG_CACHE_HOME/dcc, keyed on the dcc version, the platform, the compiler's version and the file itself, the compile arguments and the source, so upgrading the compiler cannot re-use an object the old one produced. Compiling a hello-world program goes from 0.66s to 0.34s with the default sanitizers and from 0.41s to 0.25s with one. A miss only costs the time to compile, so anything unexpected is treated as one: an object which is not an ordinary file of our own, or whose recorded digest does not match, is ignored and recompiled. DCC_NO_WRAPPER_CACHE turns it off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wrapper_c/*.c was not valid C. About 22 placeholders were replaced textually by dcc before it could be compiled, so no editor, language server or analyser could open it, and the only way to check it was to run dcc. The placeholders are now macros. dcc writes their values as #define lines ahead of the source, and wrapper_c/dcc_defines.h gives every one of them a default, so the sources compile on their own: cat wrapper_c/dcc_main.c wrapper_c/dcc_dual_sanitizers.c wrapper_c/dcc_util.c wrapper_c/dcc_check_output.c wrapper_c/dcc_save_stdin.c | clang -fsyntax-only -Wall -Wextra -D_GNU_SOURCE -include wrapper_c/dcc_defines.h -x c - The five places where a placeholder sat inside a string literal now build the string by concatenation or stringification. tests/check_wrapper_warnings.sh checks both compilers accept the sources on their own under eight settings, as well as the generated ones, which takes it from 27 compilations to 43. Compiling the same program before and after produces wrapper objects which are identical except for the one deliberate change: the command which reads valgrind's output is passed to the debugging message rather than being written into it a second time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… arguments clang 22 underlines the whole call rather than the function name, so the explanation for an ignored return value read "the value returned by function atoi(argv[0])". The helper which strips the arguments already existed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e to fail The tests_all_clang_versions target ended its loop body with echo, so it always succeeded however many tests failed, and it gave every clang the default clang++ rather than its own. With that fixed the whole suite passes with clang 17, 18, 19, 21 and 22. Two compiler wording changes needed an accepted output: clang 21 calls comparisons "non-overlapping" where earlier versions said "overlapping", and clang 22 underlines a call differently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two sanitizers write the same bytes at different widths when a program prints a pointer: ASan's heap addresses are wider than valgrind's, so %p makes the write lengths disagree. That was treated as lost synchronisation, which kills the valgrind process, so from the first %p onwards the student silently lost leak checking and uninitialized value checking, with nothing said. The write lengths never needed to agree. sanitizer2 discards its own buffer and length in __dcc_cookie_write, so only which system call it is carries any information. Exempt sc_write from the length comparison and cap the result handed back to sanitizer2 at the size it asked for, because stdio reads a larger return as a failed write. Fixes #80. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README described how to use dcc but never its scope, so the questions in issue #97 -- embedded targets, whether it compiles the same as gcc, speed, whether its executables can be shipped, whether it helps with security -- had no answer anywhere in the repository. Add a Scope section answering all five, and the same text as the [DESCRIPTION] the generated man page was missing. The performance numbers are measured rather than estimated, and say which machine and compilers they came from. The claims about the second process are qualified, because dcc drops to a single sanitizer by itself for several common cases and the costs are then much smaller. help2man is not installed here, so the generated dcc.1 has not been checked. Fixes #97. Completes #100, whose other half -- --help printing nothing, so help2man had no material -- was fixed by 3aacad6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sigaltstack handler decided a fault was a stack overflow from the faulting
address alone, and required it to be within 1 MB of the stack limit. A single
oversized frame faults its own size past the limit, which is unbounded, so
int a[3000000];
was reported as "invalid pointer or string" rather than as a stack overflow.
An oversized local array is one of the two ways a novice overflows the stack.
Decide from the interrupted stack pointer instead, which the handler already
received and ignored: in an overflow the stack pointer is itself past the end
of the stack and the fault is where it points, while a wild or NULL pointer
faults far from a stack pointer still well inside the stack. The test no
longer depends on frame size, so an arbitrarily large frame is recognised.
Only the main thread is classified. stack_top and main_thread_id describe its
stack, so without that gate any fault on another thread's stack looked like an
overflow -- a wild pointer in a pthread was reported as infinite recursion.
Another thread overflowing its own stack keeps the generic explanation.
Where the stack pointer cannot be read, the previous test is unchanged, so
platforms other than Linux x86-64, i386 and aarch64 behave as before.
Fixes #44.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
valgrind returns 0 from posix_spawn where it would natively fail, so dcc pre-checked the path and returned 2. Three things were wrong with that. posix_spawnp was not covered at all, so it still returned 0. It resolves a bare name against $PATH, which the check now does the way execvp does, including reporting EACCES when a candidate existed but was not executable. The errno was always 2, where execve reports EACCES for a directory or an unexecutable file, and ENOTDIR, ELOOP or ENAMETOOLONG for a malformed path. Report what stat and faccessat actually say. The check resolves the path in the parent, before file_actions has run, so it cannot be applied to a relative path when file_actions is present -- an addchdir_np changes what that path means. Skip it there and let the child resolve it, which also fixes posix_spawn with addchdir_np, broken before this. An eight case comparison against a native build now agrees in every case. Fixes #59. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
printf("%s\n", p) with p == NULL was explained wrongly. gcc rewrites that
call into __builtin_puts and then numbers the null argument for the rewritten
call, so dcc reported it as argument 1 to printf, which is the format string.
Recognise the printf to puts rewrite and say the string being printed is NULL.
Only that rewrite is recognised: gcc rewrites plenty of calls which print
nothing, and strstr(NULL, "a") must keep the explanation it already had.
Three diagnostics which passed through bare now have explanations:
array has incomplete element type 'int[]' -- a multidimensional array
needs a size for every dimension but the first. A plain incomplete struct
element type is deliberately not matched, as that advice would be wrong.
'%s' directive argument is null -- the directive gcc named is quoted back
rather than assuming %s, and the wording does not say "printing", because
sprintf and snprintf produce this too.
incomplete definition of type 'struct x' -- a misspelt struct name is a
likelier cause for a novice than an ADT, so that is suggested first.
Fixes #72, #95, #96. Part of #24.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two files which include each other make the compiler print the same two line include chain about a hundred times before each diagnostic, so the error the student has to read is somewhere in two hundred lines of noise. Buffer the lines no explanation was found for, and print a consecutively repeated block once followed by a count. Collapsing is only done where it removes more lines than the count it adds, so ordinary output is untouched and a legitimate deep include chain still reads normally. Looking for the repeating block is quadratic in its length, so it is bounded. "In file included from" no longer parses as a message, since it names the chain a later message arrived through rather than a mistake, which means the suppression of notes about system headers moves to where messages are printed. Fixes #62. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
UndefinedBehaviorSanitizer reports "applying zero offset to null pointer" for
(void)((int *)0 + 0);
which dcc turned into "accessing a value via a NULL pointer", telling the
student to look for a dereference that is not there.
"applying" was matched alongside "load" and "store", which really are
dereferences. Give it its own case and decide from the source line whether a
dereference plausibly happened, so p[i] and *(p + i) with a NULL p keep the
message they had, which is the common novice mistake, and arithmetic alone
gets a message about arithmetic.
The wording avoids saying this is definitely an error: C23 makes it undefined
behaviour, but the next draft defines ptr + 0 on a null pointer.
Fixes #109.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…viour sanitizer The exclusion was a bare condition with an identical commented-out copy above it and no explanation, which is why issue #58 -- dcc missing uninitialized values that dcc --valgrind reports -- went six years without a diagnosis. Record what it does and why it cannot simply be removed: memcheck reports an uninitialized value only once it reaches a branch, and an undefined behaviour check is often the only branch on it, so the second process not having that sanitizer is exactly the gap #58 describes. Building it with the sanitizer is not the fix. A program compiled with --use-after-return is then killed by SIGKILL and prints nothing at all, which is worse for a student than the values it would have caught. Diagnoses #58, which stays open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…itizer Where clang needs a multiplication-overflow helper the platform's libgcc does not have, the link fails with "undefined reference to `__mulodi4" and dcc silently rebuilt without -fsanitize=undefined, so the student lost that checking with nothing said. clang still emits __muloti4 on aarch64 and riscv64, where libgcc has no copy and compiler-rt does. Retry with --rtlib=compiler-rt first, and only drop the sanitizer if that link fails too. The retry is skipped for gcc, which rejects the option, and on macOS, where compiler-rt is already the default. When the sanitizer is dropped the wrapper is rebuilt as well, because it had already been compiled with DCC_UBSAN_IN_USE set and would otherwise intercept reports from a sanitizer that is no longer there. Fixes #12. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by fuzzing the diagnostic parser. Three defects, all from treating text
the compiler produced as if dcc had written it.
The word under the compiler's caret was interpolated straight into a regular
expression. Text which is not a valid regex, such as an unbalanced bracket,
raised re.PatternError, and a Python traceback replaced every word of the
compiler's own message. Text which is a valid but pathological regex, such as
a nested quantifier, backtracked exponentially and dcc hung with no output at
all. Escape the word, and run preconditions inside the same guard that already
stops a mistake in a template replacing the message with a traceback -- which
also covers int(line_number), Python refusing to convert more than 4300 digits.
The compiler positions its caret by how wide characters print, and dcc indexed
the echoed source line by how many there are, so a character which prints two
columns wide -- a Chinese character in a string or a comment -- shifted every
word taken from that line. A student writing a Chinese prompt string was told
to add an '&' before "rks", which is not in their program. Index by column.
"type specifier missing" was explained as a missing return type when the word
was followed by '(', and as a missing parameter type otherwise, so a global
declared without a type -- count = 0; -- was explained as a function parameter
in a program with no parameters. Distinguish the third case and say a variable
needs a type when you declare it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by fuzzing. dcc redirects cin through its own streambuf so the two
sanitizers stay synchronized. Its underflow() always published one readable
byte, including when fgetc returned EOF, and the byte it published was then
whatever the previous read had left in the buffer. So the stream never
reported end of file and handed back the last character over and over.
Every C++ program which reads until the input runs out was affected, which is
most of them:
while (std::cin >> value) hung until it was killed
while (getline(std::cin, s)) looped forever on the last line
compiled with g++ the same programs stop, as they should. Reading with scanf
in a C++ program was unaffected, which is why this survived: the one test
covering cin reads a single integer and never reaches the end of the input.
Leave the get area empty at end of file so the stream reports it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by fuzzing. dcc refuses to write a program over its own source, but the
check was for a name ending in .c or .h, so
d++ prog.cpp -o prog.cpp
replaced 65 bytes of the student's C++ with a 426 KB executable and said
nothing. The equivalent in C was correctly refused. Check every extension
clang accepts as source instead, which covers .cc, .cpp, .cxx, .c++ and the
header spellings.
While here, -o with nothing after it silently did nothing and the program was
written to a.out; say the argument is missing, as -l already does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four defects found by fuzzing, all in how a stopped program is described. Every abort() was explained as "A failed assert() calls abort()". Under d++ that is wrong for the commonest C++ way of reaching abort: an uncaught exception, a pure virtual call, or an exception escaping a noexcept function. Decide from the stack which it was, and say so. A function whose name begins with an underscore was dropped from the stack, because the filter meant to hide dcc's own frames matched on a leading underscore alone. A student calling _helper() was told the error was in main(). Match dcc's own names instead, which are all known. A source path containing a space or a colon did not match the frame parser's expectation of a pathname, so dcc printed "Execution stopped in ()" and no source line at all. UndefinedBehaviorSanitizer reports its own filename and line, which dcc stamped onto whichever frame gdb had given it. When the two disagree the frame is a different function, so the name shown was wrong; use the location without a function name rather than the wrong name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five defects found by fuzzing, all in the compiler driver. Under d++ the renames dcc uses to keep a student's read() or write() away from its own code were applied to the student's C++ as well, so std::cin.read and std::ostream::write became calls to functions that do not exist. Any C++ program using them failed to link with a message about __renamed_read. An unusable TMPDIR -- missing, unwritable, or not a directory -- made dcc die with "Internal error embedding dcc_tar_data", which says nothing a student can act on and blames dcc for their environment. A usable directory is chosen and passed on to the compilers dcc runs, which inherit it. A command line long enough for execve to refuse it came back as an unhandled OSError traceback. Every OSError from the compile now becomes a dcc error. The wrapper object cache updated a shared file's timestamp outside the guard that expects another dcc to be doing the same thing, so two compiles at once could fail with a traceback. A --c-compiler which exits 0 without producing a program made dcc exit 0 too, reporting success for a program that does not exist. Options which ask the compiler for something other than a program are exempt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…as missing Four defects found by fuzzing. A program with 14 files open at once exhausted a fixed table of 16 stream cookies. Past that dcc lost synchronisation and killed the valgrind process, so leak checking and uninitialized-value checking stopped for the rest of the run with nothing said. The table grows on demand instead. Output checking only saw bytes written through the stdout FILE stream, so a program writing with write(1, ...) or from a child process was told it had produced no output when it had produced exactly the right output. Both are now fed to the checker. This changes verdicts: a program which passed by writing through a channel the checker could not see will now be judged on what it actually wrote. A forked child running exit() ran dcc's cleanup, which killed the parent's valgrind process and unlinked the executable it was running. Standard error written directly with write(2, ...) appeared twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ed bytes Four defects found by fuzzing, all in the values printed after an error. balance_bracket() recursed once per character, so a line of about a thousand characters inside brackets raised RecursionError from inside gdb. A student with a long array index or a long line anywhere in the ten lines around the error got a Python traceback instead of their values. It is a loop now. The 0xAA byte dcc fills memory with was rewritten to "<uninitialized value>" wherever it appeared, including in a variable the program had deliberately set to that value, and in the middle of a multi-byte UTF-8 character. A student demonstrating memory patterns was told their initialized array was not. The guard which stops an array declaration being printed as an expression did not recognise a struct, union or enum element type, so those declarations were printed as values. Widening it to any identifier made it match N * a[i], where N is a macro -- so the whole line was discarded and the pointer that caused the error vanished from the values. A type name is lower case, or capitalised like a typedef, or FILE; an all upper case word is a macro. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two defects found by fuzzing. std::clog and the wide streams -- wcout, wcerr, wclog, wcin -- were never redirected through dcc's own streams, so in the default two-process mode their output was written once by each process and the student saw everything twice. They are redirected like cout, cerr and cin, with the wide streams converting through the stream's own imbued codecvt rather than the global C locale, so a program which imbues a locale does not lose its output. Tearing down deleted whatever streambuf was installed at exit, rather than the one dcc installed, so a program which replaced cout's buffer itself -- a common way to capture output in a test harness -- had a buffer it still owned deleted underneath it. Each stream's original buffer is restored and only dcc's own is deleted. Verified against plain clang++ for pushback, unget and non-ASCII wide output, and -static-libstdc++ still links. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e main Six defects found by fuzzing, four of them one root cause. kill(0, ...) signals every process in the sender's process group, which for a student is their shell. dcc reached it three ways, all from signalling sanitizer2_pid without asking whether that pid is a process dcc created: it is 0 before the fork, 0 in the forked child until it records its own pid, and an inherited stranger in any child the student's program forks. A SIGPIPE arriving in that window made the child kill the group, which took out the explanation being written and the shell that ran the program. Measured at 10 in 400 runs under load with --use-after-return, 1 in 400 in the default mode. Now: signals go only to a pid dcc started, the fork is done with those signals blocked, and a child of the student's program does not run dcc's cleanup. An error before main -- in a constructor, or in the initializer of a C++ global -- produced no output at all, because the explanation needs a program to look at and __wrap_main had not yet recorded one. It is read from /proc/self/exe instead. Not done for MemorySanitizer, where nothing has run to say what went wrong and any error would be called an uninitialized read. compiler-rt prints a line of 65 '=' characters before dcc's own explanation and no option turns it off, so its report stream goes to /dev/null. It is put back for leak checking, which is the sanitizer's own output and is wanted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six defects found by fuzzing, in the checking that drives automarking. Under -fsanitize=memory the checker's own code was not exempted from the sanitizer, so reading the student's output was reported as an uninitialized read. Every output check under that sanitizer failed with "Runtime error: uninitialized variable used" against the wrong line, and correct output was rejected. The accepted output for check_birds_single_sanitizer recorded exactly that, ending "correct output rejected"; it now records the real diagnosis of each case. Expected output whose last line has no trailing newline was truncated by one byte before being compared, so dcc both rejected byte-identical output and accepted output missing its last character. The same final line skipped the trailing-whitespace and empty-line handling every other line gets. A program producing only whitespace with no trailing newline was told it had produced no output. An expected line of 65536 bytes or more reported an internal error as though the student had caused it. "Your program printed N lines of correct output" counted one line too many. The zero-byte message numbered bytes from 0 while every other message numbers from 1, so it named the wrong byte. ab\0d now reports byte 3, as abxd does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… the leak Six defects found by fuzzing, in the valgrind side of dcc. valgrind is run with --vgdb-error=1, so it stops on an error and waits for the debugger dcc is supposed to attach. Any exception in the Python which reads valgrind's output left it waiting forever, with no message: the program hung until it was killed. The reader no longer raises on output which is not valid UTF-8, and the program is stopped rather than abandoned when it does. Every "Invalid write" was explained as a stack buffer overflow and every "Invalid read" as a probable invalid FILE *, whatever valgrind had actually found, so a heap overflow or a use after free was described as something the program does not contain. valgrind's own description of the memory decides, and the wording matches what dcc says when AddressSanitizer finds the same fault. Where valgrind has not said what the memory was, neither does dcc. A leak allocated inside a library function was reported at glibc's source location, sending the student to a file they have never seen. The frames are walked outwards to the first one in a file of theirs. A leak was dropped entirely when the allocating function's name began with an underscore, because the filter for library internals matched the name rather than the file. A student calling _allocate() was told nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by fuzzing. dcc drops to AddressSanitizer alone -- losing uninitialized variable detection entirely -- when a program includes a header it can not run two sanitizers with, links a library, uses threads, or compiles incrementally. It said nothing, so a student lost their best diagnostic without knowing. The reason was already worked out and then discarded; it is now printed as one note. The header named is the first in sorted order, so the same command says the same thing every time. single_test.sh treats a note as not a diagnostic, so a program whose compile produces only a note is still run. The rest: a response file including itself expanded forever; a cycle is refused and a file legitimately named twice still works a NUL byte in a path escaped into the compiler command line --c-compiler=gcc compiled the student's program with -O, leaked from a list meant only for gcc's extra warnings d++ omitted -fstandalone-debug, so gdb could not print a std::string and showed a Python exception instead every argument was read in full before the checks that it is a regular file of a sane size, so a character device made dcc block --use-funopen was offered with no check that it can work -fsanitize=undefined alone was refused as "only 1 or 2 sanitizers supported" --no_explanation turned explanations on, the opposite of its name Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two defects found by fuzzing, and two more the fixes for them introduced. "Maybe you meant printf?" was suggested for printf itself, because the regex meant to catch a misspelling matched the correctly spelled name. A student who forgot #include <stdio.h> was told to check their spelling. A note about a system header ended where the compiler echoed the line it points at, and that echoed line was then parsed as a new message, so the note was cut short and the rest discarded. Fixing that used a pattern for the line the compiler draws to point at a word, but it also matched a line of the student's program containing a ^ -- so their message was vetoed, the explanation lost, and the compiler's own summary leaked through. Only spaces and tildes may follow the gutter. Widening the missing-header test made it read any note mentioning "include" with a <...> anywhere in it, and a note echoes the student's source, so a comment naming a header made dcc tell them to include it. Only the compiler's own note lines are read now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hree messages Residual items from the fuzzing campaign, each left by an agent because the fix was in a file outside the one it was allowed to edit. system() ran its command through a pipe so the child's output could be checked, but only when two sanitizers were running. With one -- which is where a student lands as soon as they include unistd.h -- the child's output was never seen, so a correct program was failed. run_system_command moves out of the two sanitizer block, and a single sanitizer gets a wrapper of its own which does the same without the handshake there is no second process for. "The characters you printed were correct, but more characters were expected" was printed whenever the last byte of a line differed, so abce against abcd said the characters were correct when the last one was wrong. It is said only when what was printed really is the start of what was expected. Where characters are being ignored the two are compared in a form this cannot see, so the wording there is left alone. "internal error: expected line too long" blamed the student for dcc's own buffer. It now says the expected output has a line longer than dcc can check, and gives the length, which is something the person who supplied it can act on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With --c-compiler=gcc every runtime error collapsed to "illegal array, pointer
or other operation", because the code which reads the sanitizer's report was
compiled out. The condition tested the clang version, which is 0 when the
compiler is gcc, but gcc's libubsan exports the same two entry points clang's
does, so there was nothing to compile out.
Test the version only to exclude clang releases older than 7, and take the
accessor as a weak symbol checked before use, since the comment above it
records that some builds of the sanitizer runtime do not export it -- a missing
strong symbol would stop every program linking.
dcc --c-compiler=gcc, an out of bounds read
before: Runtime error: illegal array, pointer or other operation
after: Runtime error: index 5 out of bounds for type 'int [3]'
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The note dcc now prints when it drops to one sanitizer is documented in the README and the man page, with which headers keep both sanitizers in C and the fact that C++ nearly always loses the second process. Output checking now covers bytes written with write(1, ...) and the output of a command run with system(), which changes the verdict for a program that passed by writing through a channel the checker could not see. The one exception, -fsanitize=memory, is recorded, as is the line length limit applying to the expected output as well as to the program's. The C++ section says what is now redirected and that reading to end of file stops there. make tests mentions DCC_TEST_JOBS, and the usage message mentions that the older spellings of options are still accepted, since they are not listed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A regression from the check which stops a compiled program being given as
source. clang takes the value of -x, -include, -isystem and others as a
separate argument, which dcc did not know, so the value was looked at as if it
were a source file. That was harmless until the check made it fatal:
$ dcc option_value.c -o c # a program called c, as students have
$ dcc -x c option_value.c -o a
dcc: 'c' is a compiled program, not source code
master compiles that. Take the value with the option, as -o and -l already do.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
A stale expected-output variant permits the old exit status, and several new fixtures preserve misleading diagnostics or unrelated errors.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 2
Open (4)
What changed in this PR
Audits dcc’s compiler/runtime diagnostics and expands regression coverage across sanitizer synchronization, error reporting, output checking, and edge cases.
Changes:
- Hardens runtime and compile-time diagnostics.
- Adds CI/static checks and broad C/C++ regression coverage.
- Refreshes expected outputs for newer compilers and behavior.
| File | Description |
|---|---|
.github/workflows/ci.yml |
Adds CI build, checks, and end-to-end tests. |
.gitignore |
Ignores static-check artifacts. |
.pylintrc |
Configures Python static analysis. |
compile_time_python/colors.py |
Removes unreachable color parsing code. |
packaging/debian/build.sh |
Improves build-script quoting and failure handling. |
run_time_python/gdb_interface.py |
Initializes the injected gdb interface. |
wrapper_c/dcc_save_stdin.c |
Uses generated stdin-buffer configuration macros. |
tests/check_output/check_birds_single_sanitizer.sh |
Tests single-sanitizer output checking. |
tests/check_output/check_output_expected_line_too_long.sh |
Tests oversized expected-output handling. |
tests/check_output/check_output_system_single_sanitizer.sh |
Tests checking output from system(). |
tests/compile_time_errors/caret_in_source_line.c |
Tests diagnostic parsing with source carets. |
tests/compile_time_errors/cyclic_include.c |
Adds cyclic-include coverage. |
tests/compile_time_errors/cyclic_include.h |
Supports cyclic-include coverage. |
tests/compile_time_errors/diagnostic_shaped_string.c |
Tests diagnostic-like source text. |
tests/compile_time_errors/header_named_in_a_comment.c |
Tests include parsing in comments. |
tests/compile_time_errors/implicit_printf_declaration.c |
Tests undeclared printf diagnostics. |
tests/compile_time_errors/incomplete_struct_definition.c |
Tests incomplete-struct explanations. |
tests/compile_time_errors/missing_h_file.c |
Tests missing-header behavior. |
tests/compile_time_errors/multidimensional_array_no_bounds.c |
Tests missing multidimensional bounds. |
tests/compile_time_errors/nonnull_null_stream.c |
Tests NULL stream diagnostics. |
tests/compile_time_errors/nonnull_rewritten_builtin.c |
Tests rewritten built-in diagnostics. |
tests/compile_time_errors/printf_null_pointer.c |
Tests NULL %s diagnostics. |
tests/compile_time_errors/printf_null_pointer_newline.c |
Tests optimized NULL %s diagnostics. |
tests/compile_time_errors/struct_array_incomplete_element.c |
Tests incomplete struct arrays. |
tests/compile_time_errors/wide_character_before_caret.c |
Tests Unicode diagnostic columns. |
tests/run_time_no_errors/argument_list_too_long.sh |
Tests oversized argument handling. |
tests/run_time_no_errors/bad_temporary_directory.sh |
Tests unusable TMPDIR values. |
tests/run_time_no_errors/compiler_rt_multiplication.c |
Tests compiler-rt linking. |
tests/run_time_no_errors/cpp_sync_with_stdio.sh |
Tests C++ stream replacement cleanup. |
tests/run_time_no_errors/debug_level_not_integer.sh |
Tests invalid debug levels. |
tests/run_time_no_errors/desynchronization_detected.sh |
Tests genuine sanitizer divergence. |
tests/run_time_no_errors/embedded_environment_variable.sh |
Tests escaped embedded environment values. |
tests/run_time_no_errors/help.sh |
Tests help and version output. |
tests/run_time_no_errors/no_temp_file_leak.sh |
Tests temporary-file cleanup. |
tests/run_time_no_errors/option_value_not_a_source_file.sh |
Tests option-value parsing. |
tests/run_time_no_errors/posix_spawn.c |
Selects stable sanitizer behavior. |
tests/run_time_no_errors/posix_spawn_failure.c |
Tests corrected spawn failures. |
tests/run_time_no_errors/print_path.sh |
Hardens pathname test quoting. |
tests/run_time_no_errors/pthread_flag.c |
Tests thread compilation. |
tests/run_time_no_errors/response_file.sh |
Tests response-file expansion. |
tests/run_time_no_errors/syntax_only.sh |
Tests -fsyntax-only. |
tests/run_time_no_errors/use_funopen_option.sh |
Tests missing libbsd diagnostics. |
tests/run_time_no_errors/write_file_descriptor.c |
Tests direct stdout writes. |
tests/run_time_no_errors/write_file_descriptor_2.sh |
Tests direct stderr writes. |
tests/run_time_errors/abort_without_assert.c |
Distinguishes direct aborts from assertions. |
tests/run_time_errors/assert_failure.c |
Tests assertion reporting. |
tests/run_time_errors/cin_index.sh |
Tests synchronized C++ input errors. |
tests/run_time_errors/colorized_output.sh |
Tests forced diagnostic colors. |
tests/run_time_errors/compile_helper.sh |
Tests compile-error helpers. |
tests/run_time_errors/compile_logger.sh |
Tests compilation logging. |
tests/run_time_errors/cpp_string_value.cpp |
Tests C++ string value reporting. |
tests/run_time_errors/cpu_time_limit.sh |
Tests CPU-limit diagnostics. |
tests/run_time_errors/dereference_null_after_else.c |
Tests NULL writes after else. |
tests/run_time_errors/dereference_null_one_line_if.c |
Tests one-line NULL writes. |
tests/run_time_errors/dereference_null_with_arrow_after_arithmetic.c |
Tests NULL member access. |
tests/run_time_errors/dunder_function_name.c |
Tests user functions with reserved-style names. |
tests/run_time_errors/error_before_main.c |
Tests constructor-time C failures. |
tests/run_time_errors/error_before_main_memory_sanitizer.sh |
Tests pre-main MemorySanitizer failures. |
tests/run_time_errors/error_in_global_constructor.cpp |
Tests C++ global-constructor failures. |
tests/run_time_errors/error_in_missing_source.sh |
Tests unavailable source files. |
tests/run_time_errors/error_on_line_1.c |
Tests first-line source context. |
tests/run_time_errors/executable_as_source.sh |
Tests binary-as-source diagnostics. |
tests/run_time_errors/fork_child_exit.c |
Tests sanitizer ownership after fork. |
tests/run_time_errors/gcc_compiler_option.sh |
Tests GCC sanitizer optimization behavior. |
tests/run_time_errors/gcc_sanitizer_report.sh |
Tests GCC sanitizer hooks. |
tests/run_time_errors/incremental_compilation.sh |
Repairs and expands incremental-build coverage. |
tests/run_time_errors/infinite_recursion.sh |
Tests recursive stack overflow reporting. |
tests/run_time_errors/large_stack_frame.sh |
Tests oversized stack-frame reporting. |
tests/run_time_errors/large_stdin.sh |
Tests large synchronized stdin streams. |
tests/run_time_errors/leak_after_printf_pointer.c |
Tests leak checking after pointer output. |
tests/run_time_errors/leak_check_address.sh |
Tests AddressSanitizer leak reporting. |
tests/run_time_errors/leak_getline.c |
Tests getline leak locations. |
tests/run_time_errors/leak_location_binary_dir.sh |
Tests leak lookup near executables. |
tests/run_time_errors/leak_location_not_found.sh |
Tests leak reporting without source files. |
tests/run_time_errors/leak_strdup.c |
Tests strdup leak locations. |
tests/run_time_errors/leak_underscore_function.c |
Tests leaks in underscore-prefixed functions. |
tests/run_time_errors/long_path.sh |
Tests long source paths. |
tests/run_time_errors/many_open_files_uninitialized.c |
Tests sanitizer synchronization with many streams. |
tests/run_time_errors/multi_file_error.sh |
Tests diagnostics across source files. |
tests/run_time_errors/no_explanations.sh |
Tests disabled explanations. |
tests/run_time_errors/noexcept_throw.cpp |
Tests exceptions escaping noexcept. |
tests/run_time_errors/null_pointer_arithmetic.c |
Tests NULL pointer arithmetic. |
tests/run_time_errors/null_pointer_arithmetic_cast.c |
Tests casted NULL pointer arithmetic. |
tests/run_time_errors/null_pointer_index_nonzero.c |
Tests indexed NULL pointers. |
tests/run_time_errors/object_file_error_detection.sh |
Tests object-file sanitizer limitations. |
tests/run_time_errors/pure_virtual_call.cpp |
Tests pure-virtual termination. |
tests/run_time_errors/runtime_helper.sh |
Tests runtime helper metadata. |
tests/run_time_errors/runtime_helper_non_utf8_source.sh |
Tests helper handling of non-UTF-8 source. |
tests/run_time_errors/runtime_helper_stdin.sh |
Tests helper stdin capture. |
tests/run_time_errors/shared_library_link.sh |
Tests shared-library linking. |
tests/run_time_errors/source_name_with_leading_space.sh |
Tests leading-space filenames. |
tests/run_time_errors/source_path_with_space.sh |
Tests spaces and colons in paths. |
tests/run_time_errors/stderr_before_error.sh |
Tests stderr ordering before failures. |
tests/run_time_errors/stdin_as_source.sh |
Tests unsupported stdin source spellings. |
tests/run_time_errors/struct_array_declaration.c |
Tests struct-array value rendering. |
tests/run_time_errors/suppressions.sh |
Tests Valgrind suppressions. |
tests/run_time_errors/system_then_uninitialized.c |
Tests synchronization after system(). |
tests/run_time_errors/typedef_array_declaration.c |
Tests typedef-array rendering. |
tests/run_time_errors/uncaught_exception.cpp |
Tests uncaught C++ exceptions. |
tests/run_time_errors/underscore_function_name.c |
Tests underscore-prefixed function reporting. |
tests/run_time_errors/uninitialized_after_deep_stack.c |
Tests stack reinitialization. |
tests/run_time_errors/uninitialized_after_printf_pointer.c |
Tests uninitialized detection after pointer output. |
tests/run_time_errors/uninitialized_after_recursion.c |
Tests uninitialized values after recursion. |
tests/run_time_errors/uninitialized_with_lm.c |
Tests dual sanitizers with libm. |
tests/run_time_errors/uninitialized-array-element-if.c |
Removes nondeterministic MemorySanitizer coverage. |
tests/run_time_errors/utf8_lead_byte_before_uninitialized.c |
Tests UTF-8/uninitialized-byte distinction. |
tests/run_time_errors/utf8_string_byte_not_uninitialized.c |
Tests valid UTF-8 continuation bytes. |
tests/run_time_errors/valgrind_malloc_array_index.c |
Compares ASan and Valgrind explanations. |
tests/run_time_errors/valgrind_non_utf8_path.sh |
Tests non-UTF-8 executable paths. |
tests/run_time_errors/valgrind_use_after_free.c |
Tests Valgrind use-after-free explanations. |
tests/run_time_errors/will_not_overwrite_source.sh |
Tests source overwrite protection. |
tests/expected_output/** |
Adds and refreshes regression baselines for the tests above and newer compiler diagnostics. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| terminate called without an active exception | ||
|
|
||
| Execution stopped by the C++ library, which printed the reason above. | ||
| The most common reason is an exception which was thrown and never caught. |
There was a problem hiding this comment.
Fixed in commit fef979e by making the terminate-path diagnostic neutral and updating this expected output accordingly.
| @@ -0,0 +1,8 @@ | |||
| dcc: warning: using incremental compilation (-c) is not recommended with dcc | |||
| Signficant parts of dcc error detection do not work with incremental compilation. | |||
There was a problem hiding this comment.
Fixed in commit fef979e by correcting "Signficant" to "Significant" in both the warning text generator and this expected output.
Co-authored-by: Dylan-Brotherston <22044664+Dylan-Brotherston@users.noreply.github.com>
Co-authored-by: Dylan-Brotherston <22044664+Dylan-Brotherston@users.noreply.github.com>
Co-authored-by: Dylan-Brotherston <22044664+Dylan-Brotherston@users.noreply.github.com>


Audit, issue triage and fuzzing of dcc 2.37
52 commits. Every one landed only after
make checkand the full end-to-endsuite passed. The suite is green on clang 17, 18, 19, 21 and 22.
make checkWhat this is
Three passes over the project:
the defects found fixed and a static-checking gate added.
released 2.37 and a build of this branch so "already fixed" could be told
from "fixed here".
defects. Each was independently reproduced and minimised before being fixed.
Every fix was adversarially reviewed by agents which rebuilt dcc and tried to
break the fix; that caught regressions in the fixes themselves, several of them
serious, which were repaired before landing.
Changes a user will notice
A program stopped by an error dcc detected now exits 141, whichever
sanitizer found it and whichever kind of error it is. A program dcc did not
stop keeps its own status, so a marking script can tell the two apart.
dcc says when it drops to one sanitizer. Detecting uninitialized variables
needs the second process, which is disabled for a library, threads, incremental
compilation, an object file, an unsafe system header, and on macOS. That was
silent; a student lost their best diagnostic without knowing. In C this is
uncommon. In C++ it is the usual case, so most C++ compiles gain a line.
Output checking counts everything the program writes to standard output,
not only what goes through the
stdoutstream:write(1, ...)and the outputof a command run with
system()are now checked. A program which passed bywriting through a channel the checker could not see will now be judged on what
it actually wrote — this can change an autotest result. Six accepted-output
files in this repository recorded the old behaviour and were re-recorded; one
of them ended
correct output rejected, because under MemorySanitizer thechecker's own reads were reported as uninitialized and every check failed.
Every C++ program that read to end of file used to hang. dcc's streambuf
never reported end of file and returned the last character forever, so
while (cin >> x)andwhile (getline(...))never finished.d++ prog.cpp -o prog.cppdestroyed the source. The guard against writinga program over its own source covered only
.cand.h.Issues closed
Closed by a commit trailer, so GitHub will close them on merge:
#12 #44 #56 #59 #60 #62 #72 #80 #95 #96 #97 #100 #109
Also fixed, but without a trailer because the fix predates the triage:
#107 (
--helpprinted nothing; it now prints 40 lines).Partly addressed, and left open: #24 (three wrong explanations gone, some
suggestions unimplemented), #74 (every spelling of standard input is now
refused clearly; compiling from stdin is still unsupported), #75 (the
link step now says what detection an object file costs).
#49 has been closeable since 2019: it was fixed seventeen days after it was
filed and never closed.
#58 is diagnosed, not fixed. In the default mode the valgrind process is
built without UndefinedBehaviorSanitizer, which is why
dccmissesuninitialized values that
dcc --valgrindreports — exactly what the reporterguessed in 2019. Enabling it is not the fix: a program compiled with
--use-after-returnis then killed by SIGKILL and prints nothing at all. Theexclusion now carries that explanation in the code, where it was previously a
bare condition with an identical commented-out copy above it.
#42 is a duplicate of #106 and can be closed with it.
🤖 Generated with Claude Code