Skip to content

Commit 717c538

Browse files
authored
Merge pull request #100 from Univers42/develop
Release v2.8.3: fixes #94 #95 #96 #97 + the builtin builtin
2 parents 2d09c1d + 539c34b commit 717c538

40 files changed

Lines changed: 839 additions & 127 deletions

‎.github/workflows/ci.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,12 @@ jobs:
419419
# where xmalloc and readline's free() are the same allocator.
420420
- name: Tab completion on the ft_malloc heap
421421
run: make completion-test
422+
# Rebuilds with 512-byte arena chunks so the allocator's rare states
423+
# (chunk boundaries, registry-full heap fallback) happen every few
424+
# nodes — the #94 double free could only ever fire in those states.
425+
# Runs last: it leaves the object tree fclean'd.
426+
- name: Arena chunk-boundary stress (ASan)
427+
run: make arena-stress
422428

423429
# ── Benchmarks: measured, reported, never a gate ─────────────────────────
424430
# Shared CI runners are noisy neighbours; timings there cannot decide

‎Makefile‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1124,6 +1124,15 @@ help-test: all ## The help builtin — every dispatch-table entry must have one
11241124
docs-builtins: all ## Regenerate wiki/builtins/index.md from the help builtin
11251125
@python3 tools/gen_builtins_md.py $(BIN_DIR)/$(BAPTIZE_SHELL)
11261126

1127+
# The parse arena's rare states (chunk boundaries, registry-full heap
1128+
# fallback) need hundreds of MB to reach at production chunk sizes — the
1129+
# issue #94 double free lived there for months. This rebuilds with
1130+
# 512-byte chunks so those states occur every few nodes, then runs an
1131+
# rc-shaped corpus under ASan. NOTE: rebuilds twice (stress flags in, then
1132+
# fclean — the object tree is not keyed on EXTRA_CFLAGS).
1133+
arena-stress: ## Arena chunk-boundary stress under ASan (issue #94 detector)
1134+
@sh $(TEST_DIR)/alloc_stress.sh
1135+
11271136
# The update path end to end against a LOCAL fake release server: discovery,
11281137
# download, sha256, atomic replace, and every rejection path (bad checksum,
11291138
# truncated asset, unreachable source). Nothing is installed system-wide and
@@ -1236,7 +1245,7 @@ geoman: all ## External 42 minishell tester, as an independent cross-check
12361245
readline-test anim-test git-prompt-test \
12371246
prompt-atomic-test \
12381247
bg-tty-test prompt-integrity-test update-badge-test nonblock-tty-test \
1239-
update-config-test update-test help-test test-release \
1248+
update-config-test update-test help-test test-release arena-stress \
12401249
conformance perf rss \
12411250
charts cli-opts-test net-redir-test login-test user-install-test \
12421251
geoman oracle docker-suite docker widechar-test \

‎RELEASE.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,40 @@ shows you how to drive the shell.
88

99
---
1010

11+
## v2.8.3 — *the four-bug-reports release*
12+
13+
Four field reports, four root causes, and a detector for each so none of
14+
them can quietly come back:
15+
16+
- **The big one (#94):** a double free in AST teardown could SIGSEGV the
17+
shell on large rc-shaped scripts. The parse arena's rare heap-fallback
18+
handed the reparser a shared token it then freed once per word — a
19+
state so deep it needed hundreds of MB of parse allocations to reach,
20+
which is why no normal-size test ever saw it. Fixed at the root, and
21+
`make arena-stress` now shrinks the arena chunks to 512 bytes so those
22+
rare states happen every few nodes, under ASan, on every CI run.
23+
- **`$(case ...)` parses (#95):** a case pattern's unbalanced `)` no
24+
longer ends the command substitution — so the idiomatic
25+
`n=$(case "$x" in ''|*[!0-9]*) echo no ;; *) echo yes ;; esac)` works,
26+
and one such line no longer silently disables the rest of your rc.
27+
- **Preexec hooks from functions (#96):** `trap 'my_preexec' DEBUG`
28+
inside a function now persists after the function returns, exactly as
29+
bash does — the canonical installable-hook shape works. Subshells got
30+
bash's other half too: inherited DEBUG/RETURN/ERR traps stay listable
31+
in `$( )` but never fire there. And because bash-preexec's hook could
32+
now actually run, hellish gained the **`builtin`** builtin it calls
33+
(`builtin history 1`) — 69 builtins now.
34+
- **`$COLUMNS` and `$LINES` (#97):** set at interactive startup and kept
35+
current across resizes (honouring `shopt checkwinsize`), unexported,
36+
like bash — a right prompt no longer needs to fork `tput cols`.
37+
38+
Verified green across every layer: 4323 golden cases on both the ASan
39+
and release builds, whole-script + hard corpora, the pty suite (plus a
40+
new resize test), allocator parity on both heaps, the 13-plugin corpus,
41+
and the new arena stress.
42+
43+
---
44+
1145
## v2.8.2 — *the customize-your-prompt release*
1246

1347
Paste any zsh prompt tutorial into `~/.hellishrc` and it now just works

‎incs/casescan.h‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/* ************************************************************************** */
2+
/* */
3+
/* ::: :::::::: */
4+
/* casescan.h :+: :+: :+: */
5+
/* +:+ +:+ +:+ */
6+
/* By: dlesieur <dlesieur@student.42.fr> +#+ +:+ +#+ */
7+
/* +#+#+#+#+#+ +#+ */
8+
/* Created: 2026/09/01 00:00:00 by dlesieur #+# #+# */
9+
/* Updated: 2026/09/01 00:00:00 by dlesieur ### ########.fr */
10+
/* */
11+
/* ************************************************************************** */
12+
13+
/* Case-aware $(...) span scanning (issue #95). A case pattern's closing
14+
`)` is unbalanced by design (`a*)` needs no opening paren), so every
15+
scanner that walks a command substitution looking for its matching `)`
16+
by counting parens ends the span at the first pattern. This automaton
17+
is the one shared implementation of the fix: it tracks `case ... esac`
18+
regions (only in command position, so `echo case` cannot arm it) and
19+
reports a `)` at the depth where the innermost open `case` started as
20+
a pattern terminator instead of a closer. Balanced pattern parens
21+
`(a*)`, extglob `@(a|b)`, body subshells and nested substitutions all
22+
raised the depth first, so they still close normally.
23+
Users: the lexer's tokenize_subshell, the word reparser's
24+
reparse_envvar_paren, the expander's find_cmd_sub_end. The cmdsub fast
25+
path's csf_skip_csub stays naive on purpose: a mis-scan there makes
26+
eligibility reject and fork, which is correct by construction. */
27+
28+
#ifndef CASESCAN_H
29+
# define CASESCAN_H
30+
31+
# include <stdbool.h>
32+
33+
/* Unclosed-`case` tracking depth; nesting beyond it falls back to plain
34+
paren counting (the pre-#95 behaviour). */
35+
# define CASESCAN_MAX 32
36+
37+
typedef struct s_casescan
38+
{
39+
int len; /* input length, or -1 for NUL-terminated input */
40+
int ncase; /* how many `case` are open */
41+
int at[CASESCAN_MAX]; /* paren depth each open case started at */
42+
bool cmdpos; /* the next word sits in command position */
43+
} t_casescan;
44+
45+
void casescan_init(t_casescan *cs, int len);
46+
47+
/* Feed the UNQUOTED, UNESCAPED character at s[*i]; the caller handles
48+
quotes and backslash itself (clearing cs->cmdpos when it does).
49+
Advances *i past what was consumed (one char, or a whole keyword) and
50+
returns the paren-depth delta to apply: +1, -1 or 0. */
51+
int casescan_step(t_casescan *cs, const char *s, int *i, int depth);
52+
53+
#endif

‎incs/env.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ char *env_to_str(t_env *e);
144144
t_env *env_nget(t_vec_env *env, char *key, int len);
145145
void set_shlvl(t_shell *state);
146146
void set_shell_var(t_shell *state);
147+
void update_winsize_vars(t_shell *state);
147148

148149
/* O(1) env name index (env_index.c) -- lazy hash table over the vector.
149150
Pass len<0 to use strlen(key). Returns vector position, -1 if absent. */

‎incs/executor.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ void fire_debug_trap(t_shell *state);
7878
void fire_err_trap(t_shell *state, int code);
7979
void fire_return_trap(t_shell *state, int code);
8080
void trap_save_reset(t_shell *state, char **save);
81+
void pseudo_traps_quiet(t_shell *state);
8182
void trap_restore(t_shell *state, char **save);
8283
void exe_res_set_status(t_shell *st, t_execution_state *res);
8384
int find_cmd_path(t_shell *state, char *cmd_name,

‎incs/ft_builtins.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ int builtin_true(t_shell *state, t_vec argv);
9292
int builtin_false(t_shell *state, t_vec argv);
9393
int builtin_umask(t_shell *state, t_vec argv);
9494
int builtin_command(t_shell *state, t_vec argv);
95+
int builtin_builtin(t_shell *state, t_vec argv);
9596
int builtin_return(t_shell *state, t_vec argv);
9697
int builtin_getopts(t_shell *state, t_vec argv);
9798
int builtin_exec(t_shell *state, t_vec argv);

‎incs/parena.h‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,18 @@
2828
# include <stddef.h>
2929
# include <stdbool.h>
3030

31-
# define PARENA_MAX_CHUNKS 64
32-
# define PARENA_FIRST_CHUNK 262144
33-
# define PARENA_MAX_CHUNK 8388608
31+
/* Overridable so a stress build can shrink the chunks (e.g.
32+
-DPARENA_FIRST_CHUNK=512) and turn rare chunk-boundary states into
33+
the common case — tests/alloc_stress.sh builds exactly that. */
34+
# ifndef PARENA_MAX_CHUNKS
35+
# define PARENA_MAX_CHUNKS 64
36+
# endif
37+
# ifndef PARENA_FIRST_CHUNK
38+
# define PARENA_FIRST_CHUNK 262144
39+
# endif
40+
# ifndef PARENA_MAX_CHUNK
41+
# define PARENA_MAX_CHUNK 8388608
42+
# endif
3443

3544
/* Bump-allocation granularity is 8 bytes: every parse object is
3645
pointer-aligned, and small blocks waste half as much padding as the

‎incs/shell.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,13 @@ typedef struct s_shell
397397
/* --- traps and readonly vars --- */
398398
char *traps[SH_NTRAP]; /* trap strings, by signal num */
399399
int trap_depth; /* >0 while a trap body runs */
400+
/* Bit (slot - TRAP_DEBUG) set: that pseudo trap was inherited by a
401+
subshell environment (cmdsub, ( ), coproc) and must not FIRE there,
402+
though it stays listable — bash 5.3 keeps `trap -p DEBUG` working
403+
inside $() while never running the inherited trap. Setting or
404+
resetting the slot clears its bit, so a trap installed inside the
405+
child fires normally. */
406+
int traps_quiet;
400407
/* >0 while a prompt is being rendered. An error raised in there must
401408
never end the session: a prompt redraws on every keystroke, and a
402409
shell that exits because of a typo in PS1 gives the user no way back

‎incs/version.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
/* The single source of truth for the running shell's version. Bumped in step
1717
with the git tag / GitHub release / npm package / docker image. */
18-
# define HELLISH_VERSION "2.8.2"
18+
# define HELLISH_VERSION "2.8.3"
1919

2020
/* Where releases live; used by the `update` builtin and the daily check. */
2121
# define HELLISH_REPO "Univers42/hellish"

0 commit comments

Comments
 (0)