11use std:: {
22 collections:: VecDeque ,
33 io:: { Read , Write } ,
4- mem:: ManuallyDrop ,
54 sync:: { Arc , Mutex , OnceLock } ,
65 thread,
76} ;
@@ -15,18 +14,38 @@ type ChildWaitResult = Result<ExitStatus, Arc<std::io::Error>>;
1514
1615// On musl libc (Alpine Linux), concurrent PTY operations trigger
1716// SIGSEGV/SIGBUS in musl internals (sysconf, fcntl). This affects
18- // openpty+fork, FD cleanup (close), and FD drops from any thread.
19- // Serialize all PTY lifecycle operations that touch musl internals.
17+ // openpty+fork, FD operations (read/write/close), and FD drops.
18+ // Ensure only one Terminal exists at a time by blocking spawn until
19+ // all PTY FDs from the previous Terminal are closed.
2020#[ cfg( target_env = "musl" ) ]
21- static PTY_LOCK : std:: sync:: Mutex < ( ) > = std:: sync:: Mutex :: new ( ( ) ) ;
21+ static PTY_GATE : ( std:: sync:: Mutex < bool > , std:: sync:: Condvar ) =
22+ ( std:: sync:: Mutex :: new ( false ) , std:: sync:: Condvar :: new ( ) ) ;
23+
24+ /// RAII guard that releases the PTY gate when dropped.
25+ /// Shared via `Arc` so the gate stays held while any part of the
26+ /// Terminal (reader, writer, background thread) is still alive.
27+ #[ cfg( target_env = "musl" ) ]
28+ struct PtyPermit ;
29+
30+ #[ cfg( target_env = "musl" ) ]
31+ impl Drop for PtyPermit {
32+ fn drop ( & mut self ) {
33+ let ( lock, cvar) = & PTY_GATE ;
34+ * lock. lock ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) = false ;
35+ cvar. notify_one ( ) ;
36+ }
37+ }
2238
2339/// The read half of a PTY connection. Implements [`Read`].
2440///
2541/// Reading feeds data through an internal vt100 parser (shared with [`PtyWriter`]),
2642/// keeping `screen_contents()` up-to-date with parsed terminal output.
2743pub struct PtyReader {
28- reader : ManuallyDrop < Box < dyn Read + Send > > ,
44+ reader : Box < dyn Read + Send > ,
2945 parser : Arc < Mutex < vt100:: Parser < Vt100Callbacks > > > ,
46+ /// Prevent concurrent PTY sessions on musl; released when all parts are dropped.
47+ #[ cfg( target_env = "musl" ) ]
48+ _permit : Arc < PtyPermit > ,
3049}
3150
3251/// The write half of a PTY connection. Implements [`Write`].
@@ -36,25 +55,10 @@ pub struct PtyReader {
3655pub struct PtyWriter {
3756 writer : Arc < Mutex < Option < Box < dyn Write + Send > > > > ,
3857 parser : Arc < Mutex < vt100:: Parser < Vt100Callbacks > > > ,
39- master : ManuallyDrop < Box < dyn MasterPty + Send > > ,
40- }
41-
42- impl Drop for PtyReader {
43- fn drop ( & mut self ) {
44- #[ cfg( target_env = "musl" ) ]
45- let _guard = PTY_LOCK . lock ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
46- // SAFETY: called exactly once, from drop.
47- unsafe { ManuallyDrop :: drop ( & mut self . reader ) } ;
48- }
49- }
50-
51- impl Drop for PtyWriter {
52- fn drop ( & mut self ) {
53- #[ cfg( target_env = "musl" ) ]
54- let _guard = PTY_LOCK . lock ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
55- // SAFETY: called exactly once, from drop.
56- unsafe { ManuallyDrop :: drop ( & mut self . master ) } ;
57- }
58+ master : Box < dyn MasterPty + Send > ,
59+ /// Prevent concurrent PTY sessions on musl; released when all parts are dropped.
60+ #[ cfg( target_env = "musl" ) ]
61+ _permit : Arc < PtyPermit > ,
5862}
5963
6064/// A cloneable handle to a child process spawned in a PTY.
@@ -282,8 +286,17 @@ impl Terminal {
282286 ///
283287 /// Panics if the writer lock is poisoned when the background thread closes it.
284288 pub fn spawn ( size : ScreenSize , cmd : CommandBuilder ) -> anyhow:: Result < Self > {
289+ // On musl, block until no other Terminal is alive.
285290 #[ cfg( target_env = "musl" ) ]
286- let _spawn_guard = PTY_LOCK . lock ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
291+ let permit = {
292+ let ( lock, cvar) = & PTY_GATE ;
293+ let mut busy = lock. lock ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
294+ while * busy {
295+ busy = cvar. wait ( busy) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
296+ }
297+ * busy = true ;
298+ Arc :: new ( PtyPermit )
299+ } ;
287300
288301 let pty_pair = portable_pty:: native_pty_system ( ) . openpty ( portable_pty:: PtySize {
289302 rows : size. rows ,
@@ -313,15 +326,16 @@ impl Terminal {
313326 let writer = Arc :: clone ( & writer) ;
314327 let exit_status = Arc :: clone ( & exit_status) ;
315328 let slave = pty_pair. slave ;
329+ // Hold a permit clone so the gate stays held while FDs are being cleaned up.
330+ #[ cfg( target_env = "musl" ) ]
331+ let _permit = Arc :: clone ( & permit) ;
316332 move || {
317333 let _ = exit_status. set ( child. wait ( ) . map_err ( Arc :: new) ) ;
318- // On musl, serialize FD cleanup (close) with PTY spawn to
319- // prevent racing on musl-internal state.
320- #[ cfg( target_env = "musl" ) ]
321- let _cleanup_guard = PTY_LOCK . lock ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
322334 // Close writer first, then drop slave to trigger EOF on the reader.
323335 * writer. lock ( ) . unwrap ( ) = None ;
324336 drop ( slave) ;
337+ // _permit is dropped here (after FD cleanup), releasing the gate
338+ // once all other permit clones are also dropped.
325339 }
326340 } ) ;
327341
@@ -337,10 +351,18 @@ impl Terminal {
337351
338352 Ok ( Self {
339353 pty_reader : PtyReader {
340- reader : ManuallyDrop :: new ( reader ) ,
354+ reader,
341355 parser : Arc :: clone ( & parser) ,
356+ #[ cfg( target_env = "musl" ) ]
357+ _permit : Arc :: clone ( & permit) ,
358+ } ,
359+ pty_writer : PtyWriter {
360+ writer,
361+ parser,
362+ master,
363+ #[ cfg( target_env = "musl" ) ]
364+ _permit : permit,
342365 } ,
343- pty_writer : PtyWriter { writer, parser, master : ManuallyDrop :: new ( master) } ,
344366 child_handle : ChildHandle { child_killer, exit_status } ,
345367 } )
346368 }
0 commit comments