Skip to content

TIRITH_BASH_PREEXEC_ENFORCE=1 permanently degrades on any shell function call or PROMPT_COMMAND (extdebug fires DEBUG trap inside function bodies) #176

Description

@RTRowe

Summary

TIRITH_BASH_PREEXEC_ENFORCE=1 (the blocking fallback recommended by tirith doctor when enter mode is broken) permanently degrades to warn-only — with a per-session banner — the moment any shell function is called, or when the shell has any non-trivial PROMPT_COMMAND. In practice this makes preexec enforcement unusable in any real interactive environment (prompt frameworks like oh-my-posh/starship, terminal integrations, bash-completion — all run functions on every prompt).

Enforcement only survives in a bare shell that never calls a function. Verified on a machine where enforcement demonstrably works otherwise (a curl http://… | bash-style line is correctly tirith: BLOCKED in a bare shell).

Root cause

Enforcement enables extdebug, which makes the DEBUG trap fire inside function bodies (functrace semantics). For those fires, BASH_COMMAND is an inner simple command of the function, which can never match the typed line returned by history 1. The whole-line drift check in _tirith_preexec then treats this as tampering and calls _tirith_session_degrade_to_warn_only:

tirith: bash history no longer matches BASH_COMMAND (likely HISTCONTROL/HISTIGNORE
filtering, an alias, or a shell transformation outside the whole-line drift check);
cannot enforce whole-line semantics; falling back to warn-only.

The degrade is session-permanent, so a single function call disables blocking for the rest of the shell.

Two common triggers:

  1. Any function call — even one defined interactively (see repro).
  2. PROMPT_COMMAND content — its commands fire the trap before the first prompt with history 1 pointing at the previous session's last entry (or an empty buffer if the hook loads mid-.bashrc, which yields the sibling degrade "bash history is unavailable in this shell"). Enforcement is therefore dead before the user types anything.

Reproduction

Minimal rcfile:

export TIRITH_BASH_PREEXEC_ENFORCE=1
source "$HOME/.local/share/tirith/shell/lib/bash-hook.bash"

In a PTY (HISTCONTROL/HISTIGNORE unset, history enabled):

$ curl -s http://x.com/a.sh | true
tirith: BLOCKED                     # enforcement works — great
$ myfn() { echo inside; }
$ myfn
tirith: protection downgraded to warn-only (does not block) — run 'tirith doctor' for details
  tirith: bash history no longer matches BASH_COMMAND (…)
$ curl -s http://x.com/b.sh | true
tirith: DETECTED (shell hook cannot block in preexec mode — command will still run)

With a realistic .bashrc (oh-my-posh + history -a; history -n in PROMPT_COMMAND), the degrade banner appears before the first prompt on every new shell.

Suggested direction

The drift check can't distinguish "attacker manipulated history" from "extdebug fired inside a function/PROMPT_COMMAND". Possible approaches:

  • Skip enforcement (rather than degrading the session) for trap fires whose call stack (FUNCNAME) shows they're inside a function that was itself already validated as part of the typed line — the typed line was checked at its first top-level fire, and _tirith_last_key already caches that verdict per line.
  • Treat PROMPT_COMMAND-originated fires as internal (e.g., compare BASH_COMMAND against $PROMPT_COMMAND, plus stack inspection for functions it calls).
  • At minimum, document that TIRITH_BASH_PREEXEC_ENFORCE=1 is incompatible with prompt frameworks and shell functions — tirith doctor's "set TIRITH_BASH_PREEXEC_ENFORCE=1 in a clean-history shell" hint currently sends users down a path that can't work in their environment, ending in a confusing per-session "downgraded" banner.

Environment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions