Refactor + security hardening (closes #18)#23
Merged
Conversation
Phase 1: Build hardening - Add trigger_core static library, link all targets against it - Add -Wall -Wextra -Werror to all compilation - Add TRIGGER_SANITIZE option (address+undefined sanitizers) - Add sanitize CI job - Re-enable bugprone-branch-clone in .clang-tidy Phase 2a: Process/signal safety (HIGH severity) - Initialize status=0, check waitpid return with EINTR retry - Children use _exit(127) on exec failure - Parent ignores SIGINT/SIGQUIT; children reset to SIG_DFL - trigger_read_line retries getline on EINTR - Drop WUNTRACED from waitpid calls Phase 2b: Tokenizer integer hardening - All tokenizer buffer sizes use size_t with overflow-checked doubling - Token array growth changed from linear to geometric (doubling) - Check strdup return in finalize_token - Make is_whitespace/add_char_to_token/finalize_token static - process_character made static (internal to utils.c) Phase 2c: fd/glob/syntax error fixes - Syntax errors in pipeline parse now abort the command (return NULL) - Check every dup/dup2 return value in single-stage builtin path - Check dup/dup2 in forked pipeline children, call _exit on failure - Glob: treat any nonzero return as 'keep literal', don't read glob_t Phase 3: Builtin dispatch table - Replace parallel builtin_str[]/builtin_func[] arrays with Builtin struct - Add find_builtin() returning const Builtin* or NULL - Replace all 4 dispatch loops with find_builtin - Add pointer-equality test for builtin name->function mapping - trigger_pwd uses getcwd(NULL, 0) instead of fixed cwd[4096] Phase 4: Pipeline parser/executor dedup - Add classify_operator() enum as single source of truth for operators - Fix bug #5: pass glob_eligible to trigger_parse_pipeline so quoted operators are never treated as operators - Collapse two-pass parsing into single dynamic-realloc pass - Extract apply_redirection() shared by in-process and forked paths - Rename free_pipeline -> trigger_free_pipeline, export in header - Remove duplicated free_pipeline from test_pipeline.c Phase 5: TokenList + ownership + API hygiene - Introduce TokenList {argv, glob_eligible, count} struct - parse_line_with_quotes returns TokenList* (NULL on unclosed quote) - expand_globs(TokenList*) mutates in place - trigger_execute(TokenList*) - no more char***/int** params - Add xmalloc/xrealloc/xstrdup OOM helpers, use throughout - Replace #define true/false with <stdbool.h> - Remove unused TRIGGER_TOK_DELIM constant - Use FILE_MODE constant (0644) for consistency - Flip ASan detect_leaks=1 in CI Phase 6: Exit-status model - ExecuteResult struct: int status + bool should_exit - Builtins return 0/1 POSIX-style (success/failure) - trigger_exit supports exit [n] with numeric argument - trigger_launch returns WEXITSTATUS or 128+signal - Pipeline returns last stage's exit status - main exits with last recorded status - New test_builtins tests for exit 3 and find_builtin New tests: - Parse syntax error: cmd > returns NULL (abort) - Parse syntax error: cmd > | next returns NULL - Quoted pipe bug fix: echo "|" | cat -> 2 stages, literal pipe arg - exit 3 returns 3, exit nonsensical returns 255 - Builtin name->function pointer equality test Closes #18
- execute.h, glob_expand.h: use forward decl of TokenList instead of #include "utils/utils.h" which is not on the include/ search path - Builtin array reformatted to standard LLVM style - Whitespace and continuation indent fixes - Cast spacing fixes ((int) n -> (int)n)
…d param - test_execute.c: fix broken ../include/utils/utils.h -> ../src/utils/utils.h - utils.h: add struct tag to TokenList for forward-decl compatibility - pipeline.c: add missing <stdint.h> for SIZE_MAX - utils.c: suppress -Wunused-parameter on token_size in finalize_token All 12 source + test files now compile with -std=gnu11 -Wall -Wextra -Werror
trigger_parse_pipeline now only NULL-sets consumed token pointers but never frees the string memory. The caller (token_list_free or execute path) owns the TokenList and is responsible for cleanup. This was causing heap-use-after-free in all parse tests.
- test_input.c: fix heap-buffer-overflow in long_token test
(line buffer was 2 bytes too small for strcat)
- test_glob.c: fix memory leak in free_tl - use free_array_of_strings
instead of free() to free individual string entries
- pipeline.c: fix memory leaks in parse error path:
- NULL-out operator tokens in original tokens array
- free operator tokens when consumed (pipe/redirect ops)
- free consumed argv strings in error handler
- NULL infile/outfile pointers in original tokens to prevent
double-free with token_list_free
- utils.c: fix token_list_free to iterate by count instead of
using free_array_of_strings (which stops at first NULL gap)
All 5 test suites now pass with -fsanitize=address,detect_leaks=1
- builtins.c: one line per array entry, __attribute__((unused))
- pipeline.h: enum on single line (fits under 100 cols)
- utils.c: add_char_to_token return type on its own line,
__attribute__((unused)) on token_size param
- execute.c: break after '=' not '(' for parse_pipeline call
- pipeline.c: apply_redirection break after '(' not arg
- test_pipeline.c: make_tokenlist on one line
Formatted with clang-format-18 (LLVM base, 4-space indent, 100 cols). All 489 tests pass. Dry-run check passes with no violations.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A comprehensive, 6-phase refactoring and security hardening of the trigger shell. Every confirmed bug from the #18 analysis is fixed, every design smell is addressed, and the codebase is substantially cleaner, safer, and easier to develop on.
🛡️ Bugs Fixed
statusuninitialized +waitpidunchecked → UB / busy-loopstatus=0, EINTR retry, check returnSIG_DFLintsizes + O(n²) reallocs → overflow UBsize_twith overflow-checked geometric doublingdup/dup2unchecked → fd corruption_exitin childglob_eligiblepassed totrigger_parse_pipelineglob()error falls through to uninitializedglob_texit()flushes inherited stdio; exec failure should be 127_exit(127)in childrenWUNTRACED+ noWIFSTOPPED→ Ctrl-Z hangs shellWUNTRACEDNULL, command aborted🧹 Design Improvements
find_builtin()— singleBuiltinstruct table, 4→0 dispatch loopstokens[]/glob_eligible[]lockstep by handTokenList {argv, glob_eligible, count}structclassify_operator()enum, single-pass dynamic-realloc parseapply_redirection()shared across in-process and forked pathsfree_pipelineduplicated verbatim in teststrigger_free_pipeline, exported, single definition#define true 1 / false 0<stdbool.h>fprintf+exiton OOMxmalloc/xrealloc/xstrdupcentralized helperscwd[4096]magic buffer, bare0644, unusedTRIGGER_TOK_DELIMgetcwd(NULL,0),FILE_MODE, removedtrigger_corestatic library, all targets link against it-Wall -Wextra -Werror, no sanitizers🏗️ Build & CI
trigger_corestatic library — shell + all 5 test binaries link against it-Wall -Wextra -Werroron all compilation unitsTRIGGER_SANITIZE=ONoption (Address + Undefined Behavior sanitizers)sanitizeCI job;detect_leaks=1InputTests,BuiltinsTests,ExecuteTests,PipelineTests,GlobTests🧪 New Tests
cmd >→ parse failurecmd > | next→ parse failureecho "|" | cat→ 2 stages,|is literal argexit 3→ returns 3exit [n]numeric argument supportfind_builtin("cd")->fn == trigger_cd✅ Preserved Invariants
echo "*.c"/echo \*.cliteral,echo *.cexpandsparse_line_with_quotesreturns NULLtrigger_split_linepreserved as thin wrapper (tests depend on it)📊 Code Metrics
pipeline.c: ~354 → ~378 lines (refactored, features added)+1765 / -1542across 20 filesfind_builtin()functionclassify_operator()enumx*helpersCloses #18