Skip to content

Refactor + security hardening (closes #18)#23

Merged
melogtm merged 10 commits into
mainfrom
refactor/issue-18-cleanup-hardening
Jul 11, 2026
Merged

Refactor + security hardening (closes #18)#23
melogtm merged 10 commits into
mainfrom
refactor/issue-18-cleanup-hardening

Conversation

@melogtm

@melogtm melogtm commented Jul 11, 2026

Copy link
Copy Markdown
Owner

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

# Severity Bug Fix
1 HIGH status uninitialized + waitpid unchecked → UB / busy-loop Init status=0, EINTR retry, check return
2 HIGH No SIGINT handling — Ctrl-C kills the shell Parent ignores SIGINT/SIGQUIT, children reset to SIG_DFL
3 MED Signed int sizes + O(n²) reallocs → overflow UB All sizes → size_t with overflow-checked geometric doubling
4 MED dup/dup2 unchecked → fd corruption Check every call; abort command in parent, _exit in child
5 MED Quoted operators leak into pipeline parser glob_eligible passed to trigger_parse_pipeline
6 LOW/MED glob() error falls through to uninitialized glob_t Any nonzero return → keep literal
7 LOW exit() flushes inherited stdio; exec failure should be 127 _exit(127) in children
8 LOW WUNTRACED + no WIFSTOPPED → Ctrl-Z hangs shell Drop WUNTRACED
9 LOW Syntax error prints error but still runs the command Parse returns NULL, command aborted

🧹 Design Improvements

Smell Solution
Builtin dispatch copy-pasted at 4 sites find_builtin() — single Builtin struct table, 4→0 dispatch loops
tokens[]/glob_eligible[] lockstep by hand TokenList {argv, glob_eligible, count} struct
Pipeline: two near-identical passes, operator checks written 6× classify_operator() enum, single-pass dynamic-realloc parse
Redirection open/dup2 duplicated apply_redirection() shared across in-process and forked paths
free_pipeline duplicated verbatim in tests Renamed trigger_free_pipeline, exported, single definition
#define true 1 / false 0 <stdbool.h>
9 scattered fprintf+exit on OOM xmalloc / xrealloc / xstrdup centralized helpers
cwd[4096] magic buffer, bare 0644, unused TRIGGER_TOK_DELIM getcwd(NULL,0), FILE_MODE, removed
No library target — 5 test binaries recompile sources by hand trigger_core static library, all targets link against it
No -Wall -Wextra -Werror, no sanitizers Enabled everywhere + sanitizer CI matrix

🏗️ Build & CI

  • trigger_core static library — shell + all 5 test binaries link against it
  • -Wall -Wextra -Werror on all compilation units
  • TRIGGER_SANITIZE=ON option (Address + Undefined Behavior sanitizers)
  • New sanitize CI job; detect_leaks=1
  • All CTest names preserved: InputTests, BuiltinsTests, ExecuteTests, PipelineTests, GlobTests

🧪 New Tests

Test What it verifies
cmd > → parse failure Syntax error aborts command
cmd > | next → parse failure Syntax error with pipe aborts
echo "|" | cat → 2 stages, | is literal arg Bug #5: quoted operators not parsed
exit 3 → returns 3 exit [n] numeric argument support
Builtin function pointer equality find_builtin("cd")->fn == trigger_cd
10k-char token + 200-token line Realloc paths under sanitizers

✅ Preserved Invariants

  • Glob eligibility: echo "*.c" / echo \*.c literal, echo *.c expands
  • Unclosed quote → parse_line_with_quotes returns NULL
  • Single-stage builtins run in-process; multi-stage builtins fork
  • CTest names unchanged; LLVM/4-space/100col formatting
  • trigger_split_line preserved as thin wrapper (tests depend on it)

📊 Code Metrics

  • pipeline.c: ~354 → ~378 lines (refactored, features added)
  • Net diff: +1765 / -1542 across 20 files
  • 4 dispatch loops → 1 find_builtin() function
  • 6 operator strcmp guards → 1 classify_operator() enum
  • 9 OOM error paths → 3 x* helpers

Closes #18

melogtm added 2 commits July 11, 2026 16:24
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
Copilot AI review requested due to automatic review settings July 11, 2026 19:36

This comment was marked as off-topic.

melogtm added 8 commits July 11, 2026 16:46
- 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.
@melogtm melogtm merged commit 3f47d49 into main Jul 11, 2026
3 checks passed
@melogtm melogtm deleted the refactor/issue-18-cleanup-hardening branch July 11, 2026 20:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor + Analysis for Potential Issues in the Codebase

2 participants