1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
//! Per-thread storage for the shell's `errflag`.
//!
//! **zshrs-original infrastructure — no `Src/*.c` counterpart.** It exists
//! because zshrs replaced a `fork(2)` with a thread.
//!
//! In C zsh, `errflag` (`Src/init.c`, declared `zsh.h:2833`) is a plain
//! `int` owned by exactly one thread: the shell is single-threaded, and
//! every piece of background work — a completion run, a command
//! substitution, a `compinit` rebuild — is a forked CHILD. The child gets
//! a *copy* of `errflag`; whatever it sets there can never be observed by
//! the parent's line editor.
//!
//! zshrs runs that same work on the worker pool ([`crate::worker`]) instead
//! of forking. With one process-global `AtomicI32`, the copy semantics were
//! lost and `errflag` became shared mutable state between the line editor
//! and every background parse. That is a correctness bug, not a style one:
//! `errflag` is the ENTIRE mechanism by which `send-break` (^G,
//! `Src/Zle/zle_misc.c:1144`) aborts a line —
//!
//! ```c
//! int sendbreak(UNUSED(char **args)) { errflag |= ERRFLAG_ERROR|ERRFLAG_INT; return 1; }
//! ```
//!
//! — and by which `zleread` (`Src/Zle/zle_main.c:1387`) then decides to
//! throw the line away instead of running it:
//!
//! ```c
//! if (eofsent || errflag || exit_pending) { s = NULL; }
//! ```
//!
//! The `compinit` bytecode backfill (`ext_builtins.rs`) parses thousands of
//! autoload bodies on a pool thread, and each body is bracketed by C's own
//! `errflag &= ~ERRFLAG_ERROR; … errflag = saved;` save/clear/restore
//! pattern. Landing one of those stores inside the ^G window cleared
//! `ERRFLAG_ERROR` under the editor, and the aborted line EXECUTED — `ls
//! /us` + ^G ran `ls`. A late restore re-injected a stale flag into the
//! NEXT `zleread`, aborting a line the user never typed, and a body that
//! failed to parse on the worker surfaced as `zsh: parse error` against
//! whatever the main thread happened to be parsing.
//!
//! Restoring the C model is enough to fix all of that: the shell thread
//! keeps the process-wide flag, every other thread gets its own. A thread
//! that runs shell code off the main thread IS a child in the C design, so
//! its errors stay with it.
//!
//! The four accessors mirror the `AtomicI32` API this type replaced, so
//! every `errflag.load(…)` / `.store(…)` / `.fetch_or(…)` / `.fetch_and(…)`
//! call site reads exactly as it did before.
use std::cell::Cell;
use std::sync::atomic::{AtomicI32, Ordering};
thread_local! {
/// This thread's private `errflag`, plus whether this thread is the
/// shell thread (in which case the private copy is never consulted).
/// The `is_shell` flag is computed once per thread, so the hot
/// `errflag.load()` path is a TLS read and a branch.
static SLOT: Slot = Slot {
is_shell: std::thread::current().name() == Some("main"),
val: Cell::new(0),
};
}
struct Slot {
is_shell: bool,
val: Cell<i32>,
}
/// `errflag` storage: process-global on the shell thread, thread-local
/// everywhere else. See the module docs for why.
pub struct ErrflagCell {
shell: AtomicI32,
}
impl ErrflagCell {
/// A cleared flag. `const` so `errflag` stays a plain `static`.
pub const fn new() -> Self {
ErrflagCell {
shell: AtomicI32::new(0),
}
}
/// Mirrors `AtomicI32::load`.
#[inline]
pub fn load(&self, order: Ordering) -> i32 {
SLOT.with(|s| {
if s.is_shell {
self.shell.load(order)
} else {
s.val.get()
}
})
}
/// Mirrors `AtomicI32::store`.
#[inline]
pub fn store(&self, val: i32, order: Ordering) {
SLOT.with(|s| {
if s.is_shell {
self.shell.store(val, order);
} else {
s.val.set(val);
}
})
}
/// Mirrors `AtomicI32::fetch_or`, returning the previous value.
#[inline]
pub fn fetch_or(&self, val: i32, order: Ordering) -> i32 {
SLOT.with(|s| {
if s.is_shell {
self.shell.fetch_or(val, order)
} else {
let prev = s.val.get();
s.val.set(prev | val);
prev
}
})
}
/// Mirrors `AtomicI32::fetch_and`, returning the previous value.
#[inline]
pub fn fetch_and(&self, val: i32, order: Ordering) -> i32 {
SLOT.with(|s| {
if s.is_shell {
self.shell.fetch_and(val, order)
} else {
let prev = s.val.get();
s.val.set(prev & val);
prev
}
})
}
}
impl Default for ErrflagCell {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ported::zsh_h::{ERRFLAG_ERROR, ERRFLAG_INT};
/// The regression this type exists for: a background parse's
/// `errflag &= ~ERRFLAG_ERROR; … errflag = saved;` bracket must not be
/// able to clear the flag `sendbreak()` just raised on the shell
/// thread. Before the split, the worker's restore landed on the same
/// word and the aborted line executed.
#[test]
fn off_thread_writes_do_not_disturb_the_shell_flag() {
static FLAG: ErrflagCell = ErrflagCell::new();
FLAG.store(0, Ordering::Relaxed);
// The shell thread aborts a line (zle_misc.c:1144 sendbreak).
FLAG.fetch_or(ERRFLAG_ERROR | ERRFLAG_INT, Ordering::Relaxed);
// A pool thread runs the backfill's save/clear/parse/restore.
std::thread::spawn(|| {
let saved = FLAG.load(Ordering::Relaxed);
assert_eq!(saved, 0, "a non-shell thread starts with its own flag");
FLAG.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
FLAG.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // its parse failed
FLAG.store(saved, Ordering::Relaxed);
})
.join()
.unwrap();
// zleread's epilogue (zle_main.c:1387) must still see the abort.
assert_eq!(
FLAG.load(Ordering::Relaxed) & ERRFLAG_ERROR,
ERRFLAG_ERROR,
"the worker's restore cleared the editor's abort flag"
);
}
}