Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ and versions are tracked in the repo-root `VERSION` file.
- Stopped emitting the caller's unredacted argument vector when wrapper
diagnostics are enabled, preventing sensitive option and positional values
from entering terminal or persistent logs.
- Hardened the primary diagnostic sink to reject unusable and non-regular
targets, normalize new and existing log files to mode `0600`, suppress
best-effort write errors, and report only eligible sinks through
`log_is_enabled`.

### Deprecated

Expand Down
12 changes: 10 additions & 2 deletions lib/bash/std/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,16 @@ Terminal verbosity and category gates answer different questions:
- `set_log_category_level` controls whether a component may emit a record at
all. Categories inherit from the nearest explicitly configured dotted parent,
then from `default`.
- `BASE_CLI_PRIMARY_LOG`, when set, receives accepted records through DEBUG
even when the terminal remains at INFO.
- `BASE_CLI_PRIMARY_LOG`, when it names an eligible path, receives accepted
records through DEBUG even when the terminal remains at INFO.

The primary sink is best-effort and never changes application status. An
existing target must be an owned, writable, regular non-symlink file; a missing
target needs an existing writable and searchable parent directory. The library
does not create parent directories. `log_is_enabled` checks this eligibility
without creating or changing the target. Before appending, the library creates
or normalizes the primary log to mode `0600`; setup and write failures are
suppressed and disable that path for the remainder of the process.

The global default category gate is permissive for compatibility. Applications
can keep their own DEBUG output while limiting a reusable component:
Expand Down
120 changes: 105 additions & 15 deletions lib/bash/std/lib_std.sh
Original file line number Diff line number Diff line change
Expand Up @@ -460,8 +460,9 @@ print_path() {
__log_init__() {
# Map log level strings (FATAL, ERROR, etc.) to numeric values.
# Note the '-g' option passed to declare is essential for global scope.
unset _log_levels _loggers_level_map _log_category_level_map
declare -gA _log_levels _loggers_level_map _log_category_level_map
unset _log_levels _loggers_level_map _log_category_level_map _log_primary_sink_failed_paths
declare -gA _log_levels _loggers_level_map _log_category_level_map _log_primary_sink_failed_paths
_log_primary_sink_failed_paths=()
# VERBOSE is deprecated compatibility surface; new callers should use DEBUG.
_log_levels=([FATAL]=0 [ERROR]=1 [WARN]=2 [INFO]=3 [DEBUG]=4 [VERBOSE]=5)

Expand Down Expand Up @@ -521,14 +522,110 @@ __log_source_location__() {
printf -v "$result_name" '%s:%s' "$source_path" "$source_line"
}

#
# __log_primary_sink_is_usable__ - Check the primary sink without modifying it.
#
__log_primary_sink_is_usable__() {
local primary_log="${1-}" parent_dir

[[ -n "$primary_log" && "$primary_log" != */ ]] || return 1
[[ -z "${_log_primary_sink_failed_paths[$primary_log]+set}" ]] || return 1
[[ ! -L "$primary_log" ]] || return 1

if [[ -e "$primary_log" ]]; then
if [[ -f "$primary_log" && -O "$primary_log" && -w "$primary_log" ]]; then
return 0
fi
return 1
fi

if [[ "$primary_log" == */* ]]; then
parent_dir="${primary_log%/*}"
[[ -n "$parent_dir" ]] || parent_dir=/
else
parent_dir=.
fi

[[ -d "$parent_dir" && -w "$parent_dir" && -x "$parent_dir" ]]
}

#
# __log_primary_sink_prepare__ - Create or privately harden a usable sink.
#
__log_primary_sink_prepare__() {
local primary_log="$1" chmod_path

__log_primary_sink_is_usable__ "$primary_log" || return 1

if [[ ! -e "$primary_log" ]]; then
# noclobber avoids truncating a target that appears after the
# non-mutating eligibility check.
if ! (umask 077; set -o noclobber; : >"$primary_log") 2>/dev/null; then
[[ -e "$primary_log" && ! -L "$primary_log" ]] || return 1
fi
fi

[[ -f "$primary_log" && ! -L "$primary_log" &&
-O "$primary_log" && -w "$primary_log" ]] || return 1

# macOS chmod does not accept "--"; prefix a bare option-like path instead.
chmod_path="$primary_log"
[[ "$chmod_path" == -* ]] && chmod_path="./$chmod_path"
command chmod 600 "$chmod_path" 2>/dev/null || return 1

[[ -f "$primary_log" && ! -L "$primary_log" &&
-O "$primary_log" && -w "$primary_log" ]]
}

#
# __log_primary_sink_append__ - Append one record or file payload.
#
__log_primary_sink_append__() {
local payload_kind="${1-}" payload="${2-}"
local primary_log="${BASE_CLI_PRIMARY_LOG:-}"

__log_primary_sink_is_usable__ "$primary_log" || return 1

(
umask 077
__log_primary_sink_prepare__ "$primary_log" || exit 1

case "$payload_kind" in
record)
printf '%s\n' "$payload"
;;
file)
command cat -- "$payload" || exit 1
printf '\n'
;;
*)
exit 1
;;
esac >>"$primary_log"
) 2>/dev/null
}

#
# __log_primary_sink_write__ - Keep sink failures best-effort and disable them.
#
__log_primary_sink_write__() {
local payload_kind="$1" payload="$2"
local primary_log="${BASE_CLI_PRIMARY_LOG:-}"

if ! __log_primary_sink_append__ "$payload_kind" "$payload"; then
_log_primary_sink_failed_paths["$primary_log"]=1
fi
return 0
}

#
# __print_log_record__ - Compose and write a structured log record.
#
__print_log_record__() {
local color="$1" in_level="$2" source_location="$3"
local terminal_enabled="${4:-1}" persist_enabled="${5:-0}"
shift 5
local message timestamp log_line primary_log
local message timestamp log_line

message="$(__join_message__ "$@")"
__log_timestamp__ timestamp
Expand All @@ -537,12 +634,7 @@ __print_log_record__() {
printf '%b%s%b\n' "$color" "$log_line" "$COLOR_OFF" >&2
fi
if ((persist_enabled)); then
primary_log="${BASE_CLI_PRIMARY_LOG:-}"
if [[ -n "$primary_log" ]]; then
# The launcher creates the primary log with mode 0600. Direct
# library users may not, so keep the same private default here.
(umask 077; printf '%s\n' "$log_line" >>"$primary_log") || :
fi
__log_primary_sink_write__ record "$log_line"
fi
}

Expand Down Expand Up @@ -677,7 +769,8 @@ __log_sink_state__() {
if ((category_level >= event_level)); then
terminal_level="${_loggers_level_map[$category]:-${_loggers_level_map[default]}}"
((terminal_level >= event_level)) && terminal_state=1
if [[ -n "${BASE_CLI_PRIMARY_LOG:-}" ]] && ((event_level <= _log_levels[DEBUG])); then
if ((event_level <= _log_levels[DEBUG])) &&
__log_primary_sink_is_usable__ "${BASE_CLI_PRIMARY_LOG:-}"; then
persist_state=1
fi
fi
Expand Down Expand Up @@ -767,7 +860,7 @@ __print_log_file__() {
local in_level="${1-}"
[[ -n "$in_level" ]] || return 1
shift
local logger=default file primary_log
local logger=default file
local terminal_enabled persist_enabled
if [[ "${1-}" == "-l" ]]; then
if [[ -z "${2-}" ]]; then
Expand All @@ -788,10 +881,7 @@ __print_log_file__() {
printf '\n' >&2
fi
if ((persist_enabled)); then
primary_log="${BASE_CLI_PRIMARY_LOG:-}"
if [[ -n "$primary_log" ]]; then
(umask 077; { cat -- "$file"; printf '\n'; } >>"$primary_log") || :
fi
__log_primary_sink_write__ file "$file"
fi
fi
}
Expand Down
Loading
Loading