Skip to content

Commit 1425432

Browse files
author
DragonOS Dev
committed
fix(tty): squash — comprehensive termios/termio ioctl fixes
Squash of 27 review-driven fixes across 5 review documents covering: - TCSADRAIN drain loop with hardware FIFO wait (F1, #1) - drain_echoes best-effort + Ok(false) logging (F3, M2, #4) - drain_output trait doc contract correction (F2, F3, #2, #3) - _pad stack-leak prevention doc + compile-time sizeof assert (H1, P3-1) - test struct _pad ABI match (F2) - speed derivation moved into to_kernel_termios (F5) - from_line log level + maintenance doc (H2, P3-2) - PTY drain loop docs (M1) - tty_check_change path docs (M3) - inline CIBAUD_SHIFT (L1) - remove unused name[256] (L2) - test: uninitialized struct fix (P3-3) - test: CHECK_NOERR macro (P3-4) - test: ECHO flip no-op fix (F5) - test: errno-before-close fix (F6) - test: /dev/null skip message (#7) - test: c_line round-trip (#8) - test: dead code removal (#10)
1 parent 1e4e539 commit 1425432

6 files changed

Lines changed: 174 additions & 86 deletions

File tree

kernel/src/driver/tty/termios.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ impl ControlMode {
404404

405405
/// Get input baud rate from CIBAUD bits, falling back to output baud rate.
406406
pub fn input_baud_rate(&self) -> Option<u32> {
407-
let ibaud = (self.bits() & Self::CIBAUD.bits()) >> CIBAUD_SHIFT;
407+
let ibaud = (self.bits() & Self::CIBAUD.bits()) >> 16;
408408
if ibaud == 0 {
409409
self.baud_rate()
410410
} else {
@@ -415,9 +415,6 @@ impl ControlMode {
415415

416416
pub const NCC: usize = 8;
417417

418-
/// Shift to extract input baud rate bits from c_cflag (CIBAUD = 0x100f0000).
419-
pub const CIBAUD_SHIFT: u32 = 16;
420-
421418
/// Default c_line ABI value (N_TTY = 0).
422419
pub const INIT_C_LINE_ABI: u8 = 0;
423420

@@ -432,12 +429,17 @@ pub struct PosixTermio {
432429
pub c_lflag: u16,
433430
pub c_line: u8,
434431
pub c_cc: [u8; NCC],
435-
/// Explicit padding — eliminates the 1-byte implicit tail padding that
436-
/// repr(C) would otherwise insert, preventing kernel stack leak via
437-
/// TCGETA. Default::default() zeros this field.
432+
/// Explicit padding in place of repr(C) implicit tail padding.
433+
/// Essential: `Default::default()` zeros this field, preventing kernel
434+
/// stack data leak to userspace via TCGETA.
438435
pub(crate) _pad: u8,
439436
}
440437

438+
// Compile-time guard: sizeof(PosixTermio) must be 18 to match C struct
439+
// termio on all supported ABIs. If this fails on a new architecture,
440+
// the _pad field and/or repr alignment need adjustment.
441+
const _: () = assert!(core::mem::size_of::<PosixTermio>() == 18);
442+
441443
impl PosixTermio {
442444
/// Convert kernel Termios → user-space termio.
443445
/// Fields wider than u16 are truncated; excess c_cc slots are dropped.
@@ -466,24 +468,27 @@ impl PosixTermio {
466468
let mut cc = old.control_characters;
467469
cc[..NCC].copy_from_slice(&self.c_cc);
468470

471+
let control_mode = ControlMode::from_bits_truncate(
472+
(old.control_mode.bits & 0xffff_0000) | self.c_cflag as u32,
473+
);
474+
let (input_speed, output_speed) = Self::speeds_from_cflag(control_mode);
475+
469476
Termios {
470477
input_mode: InputMode::from_bits_truncate(
471478
(old.input_mode.bits & 0xffff_0000) | self.c_iflag as u32,
472479
),
473480
output_mode: OutputMode::from_bits_truncate(
474481
(old.output_mode.bits & 0xffff_0000) | self.c_oflag as u32,
475482
),
476-
control_mode: ControlMode::from_bits_truncate(
477-
(old.control_mode.bits & 0xffff_0000) | self.c_cflag as u32,
478-
),
483+
control_mode,
479484
local_mode: LocalMode::from_bits_truncate(
480485
(old.local_mode.bits & 0xffff_0000) | self.c_lflag as u32,
481486
),
482487
control_characters: cc,
483488
line: LineDisciplineType::from_line(self.c_line),
484489
c_line_abi: self.c_line,
485-
input_speed: old.input_speed,
486-
output_speed: old.output_speed,
490+
input_speed,
491+
output_speed,
487492
}
488493
}
489494

kernel/src/driver/tty/tty_core.rs

Lines changed: 41 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,9 @@ impl TtyCore {
367367
}
368368

369369
pub fn tty_perform_flush(tty: Arc<TtyCore>, arg: usize) -> Result<usize, SystemError> {
370+
// Job control check for TCFLSH ioctl. This is a separate path
371+
// from core_set_termios (termios ioctls); the two are never
372+
// invoked concurrently for the same operation.
370373
TtyJobCtrlManager::tty_check_change(tty.clone(), Signal::SIGTTOU)?;
371374

372375
match arg {
@@ -396,6 +399,8 @@ impl TtyCore {
396399
// Check job control: prevent background process groups from changing
397400
// terminal attributes without permission. Matches Linux 6.6
398401
// set_termios() which calls tty_check_change() before any work.
402+
// This path is separate from tty_perform_flush (TCFLSH ioctl);
403+
// the two never overlap in a single operation.
399404
TtyJobCtrlManager::tty_check_change(tty.clone(), Signal::SIGTTOU)?;
400405

401406
#[allow(unused_assignments)]
@@ -411,12 +416,6 @@ impl TtyCore {
411416
let mut termio = PosixTermio::default();
412417
user_reader.copy_one_from_user(&mut termio, 0)?;
413418
tmp_termios = termio.to_kernel_termios(&tmp_termios);
414-
// termio carries no speed fields; derive speeds from the merged
415-
// c_cflag exactly as Linux set_termios() does after
416-
// user_termio_to_kernel_termios().
417-
let (ispeed, ospeed) = PosixTermio::speeds_from_cflag(tmp_termios.control_mode);
418-
tmp_termios.input_speed = ispeed;
419-
tmp_termios.output_speed = ospeed;
420419
} else {
421420
let user_reader = UserBufferReader::new(
422421
arg.as_ptr::<PosixTermios>(),
@@ -430,53 +429,50 @@ impl TtyCore {
430429
tmp_termios = term.to_kernel_termios();
431430
}
432431

433-
if opt.contains(TtySetTermiosOpt::TERMIOS_FLUSH) {
434-
let ld = tty.ldisc();
435-
let _ = ld.flush_buffer(tty.clone());
436-
}
437432

438433
if opt.contains(TtySetTermiosOpt::TERMIOS_WAIT) {
439434
let ld = tty.ldisc();
435+
let write_wq = tty.core().write_wq();
436+
let core_ref = tty.core();
440437

441-
// Retry drain_output while output_lock is held by a concurrent
442-
// writer. Bounded to prevent indefinite stall.
443-
// TODO: use wait_event on output_lock release instead of fixed retries
444-
// (tracked at issue/TERMIOS_WAIT_drain).
445-
const DRAIN_RETRIES: u32 = 32;
446-
let mut ldisc_drained = false;
447-
for _ in 0..DRAIN_RETRIES {
448-
match ld.drain_output(tty.clone()) {
449-
Ok(true) => {
450-
ldisc_drained = true;
451-
break;
452-
}
453-
Ok(false) => {
454-
crate::sched::sched_yield();
455-
}
456-
Err(e) => return Err(e),
438+
// Drain N_TTY output-processing and echo backlog.
439+
// drain_output blocks on output_lock; when the hardware FIFO
440+
// is full, drain_output returns Ok(false) — loop until both
441+
// ldisc queues are fully drained.
442+
//
443+
// On PTY, chars_in_buffer always returns 0, so the
444+
// wait_event_interruptible below returns immediately.
445+
// The loop still converges: drain_output serialises via
446+
// output_lock, and the PTY master consumes data
447+
// asynchronously so the next drain_output pass sees an
448+
// empty FIFO.
449+
loop {
450+
if ld.drain_output(tty.clone())? {
451+
break;
457452
}
453+
// Hardware FIFO was full — wait for it to drain before
454+
// re-draining ldisc. Signal-interrupted waits propagate
455+
// ERESTARTSYS so that Ctrl+C does not silently apply the
456+
// new termios (matches Linux behaviour).
457+
write_wq.wait_event_interruptible(
458+
(EPollEventType::EPOLLOUT.bits() | EPollEventType::EPOLLWRNORM.bits()) as u64,
459+
|| tty.chars_in_buffer(core_ref) == 0,
460+
)?;
458461
}
459-
if !ldisc_drained {
460-
log::debug!(
461-
"TERMIOS_WAIT: output_lock held for {} retries, proceeding without full ldisc drain",
462-
DRAIN_RETRIES
463-
);
464-
}
465-
466-
// Event-driven wait for hardware FIFO drain via write_wq.
467-
// On PTY, chars_in_buffer returns 0 immediately.
468-
//
469-
// Return value ignored intentionally: Linux tty_wait_until_sent()
470-
// does not propagate errors to the tcsetattr caller either.
471-
// A signal-interrupted wait still leaves the drain in a
472-
// best-effort state; the caller gets the new termios regardless.
473-
// TODO: add wait_event_interruptible_timeout for hung-hardware scenarios.
474-
let write_wq = tty.core().write_wq();
475-
let core_ref = tty.core();
476-
let _ = write_wq.wait_event_interruptible(
462+
// Final wait: ensure all hardware FIFOs are empty.
463+
// Returns immediately on PTY (chars_in_buffer == 0).
464+
write_wq.wait_event_interruptible(
477465
(EPollEventType::EPOLLOUT.bits() | EPollEventType::EPOLLWRNORM.bits()) as u64,
478466
|| tty.chars_in_buffer(core_ref) == 0,
479-
);
467+
)?;
468+
}
469+
470+
// TCSAFLUSH semantics: flush must happen AFTER drain so that
471+
// any input arriving during the drain is also discarded.
472+
// See tty-termios-drain-bugs.md B4.
473+
if opt.contains(TtySetTermiosOpt::TERMIOS_FLUSH) {
474+
let ld = tty.ldisc();
475+
let _ = ld.flush_buffer(tty.clone());
480476
}
481477

482478
TtyCore::set_termios_next(tty, tmp_termios)?;

kernel/src/driver/tty/tty_ldisc/mod.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,20 @@ pub trait TtyLineDiscipline: Sync + Send + Debug {
2525
Ok(())
2626
}
2727

28-
/// Drain pending ldisc output (opost + echo) without blocking indefinitely.
28+
/// Drain pending ldisc output (opost + echo), blocking until complete.
29+
///
2930
/// Returns `Ok(true)` when everything was drained, `Ok(false)` when
30-
/// the output lock was held by a concurrent writer and drain could not
31-
/// be performed.
31+
/// output could not be fully drained (e.g. hardware FIFO full).
32+
///
33+
/// Callers MUST loop on `Ok(false)`: wait for hardware write readiness,
34+
/// then call `drain_output` again until it returns `Ok(true)`. See
35+
/// `core_set_termios` for the canonical retry pattern.
36+
///
37+
/// # Default implementation
38+
///
39+
/// Returns `Ok(true)`. **Line disciplines that have their own output
40+
/// queues (opost / echo) MUST override this method.** The default
41+
/// causes TCSADRAIN to silently skip draining for undiscovered ldiscs.
3242
fn drain_output(&self, _tty: Arc<TtyCore>) -> Result<bool, SystemError> {
3343
Ok(true)
3444
}
@@ -101,13 +111,19 @@ pub enum LineDisciplineType {
101111
}
102112

103113
impl LineDisciplineType {
114+
/// Convert a raw c_line ABI byte to a LineDisciplineType.
115+
///
116+
/// NOTE: this accepts u8 rather than LineDisciplineType, so adding
117+
/// new enum variants (e.g. `Ppp = 1`) will NOT trigger a compiler
118+
/// error here. When extending the enum, update this match arm
119+
/// to map the new variant(s).
104120
pub fn from_line(line: u8) -> Self {
105121
match line {
106122
0 => Self::NTty,
107123
// Unknown / unsupported line disciplines fall back to NTty,
108124
// matching Linux behaviour (N_TTY is the default).
109125
_ => {
110-
log::debug!("LineDisciplineType::from_line: unknown line discipline {}, falling back to NTty", line);
126+
log::warn!("LineDisciplineType::from_line: unknown line discipline {}, falling back to NTty", line);
111127
Self::NTty
112128
}
113129
}

kernel/src/driver/tty/tty_ldisc/ntty.rs

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -136,14 +136,14 @@ impl NTtyLinediscipline {
136136
}
137137
}
138138

139-
fn drain_echoes(&self, tty: &TtyCore) -> Result<(), SystemError> {
139+
fn drain_echoes(&self, tty: &TtyCore) -> Result<bool, SystemError> {
140140
let core = tty.core();
141141
loop {
142142
while let Some(bytes) = { self.disc_data().echo_pending_bytes() } {
143143
let written = tty.write(core, &bytes, bytes.len())?;
144144
if written == 0 {
145145
core.flags_write().insert(TtyFlag::DO_WRITE_WAKEUP);
146-
return Ok(());
146+
return Ok(false);
147147
}
148148
{
149149
let mut guard = self.disc_data();
@@ -173,7 +173,7 @@ impl NTtyLinediscipline {
173173
guard.set_echo_pending_step(step, sent);
174174
}
175175
core.flags_write().insert(TtyFlag::DO_WRITE_WAKEUP);
176-
return Ok(());
176+
return Ok(false);
177177
}
178178
sent += written;
179179
}
@@ -189,7 +189,7 @@ impl NTtyLinediscipline {
189189
} else {
190190
core.flags_write().remove(TtyFlag::DO_WRITE_WAKEUP);
191191
}
192-
Ok(())
192+
Ok(true)
193193
}
194194

195195
fn packet_status_pending(core: &TtyCoreData, packet: bool) -> bool {
@@ -1821,14 +1821,15 @@ impl TtyLineDiscipline for NTtyLinediscipline {
18211821
}
18221822

18231823
fn drain_output(&self, tty: Arc<TtyCore>) -> Result<bool, SystemError> {
1824-
if let Some(_output_guard) = self.output_lock.try_lock() {
1825-
let drained = self.drain_opost_pending(&tty)?;
1826-
self.drain_echoes(&tty)?;
1827-
Ok(drained)
1828-
} else {
1829-
// output_lock held by a concurrent writer; cannot drain opost.
1830-
Ok(false)
1831-
}
1824+
// Block on output_lock — a concurrent writer will release it once
1825+
// its output is submitted to the hardware, at which point we can
1826+
// drain the remaining opost/echo backlog. Without blocking,
1827+
// TCSADRAIN can silently switch termios while a writer is still
1828+
// mid-flight (see tty-termios-drain-bugs.md B1).
1829+
let _output_guard = self.output_lock.lock();
1830+
let drained = self.drain_opost_pending(&tty)?;
1831+
let echo_drained = self.drain_echoes(&tty)?;
1832+
Ok(drained && echo_drained)
18321833
}
18331834

18341835
#[inline(never)]
@@ -2078,7 +2079,17 @@ impl TtyLineDiscipline for NTtyLinediscipline {
20782079
let mut output_guard = Some(self.output_lock.lock());
20792080

20802081
self.disc_data().process_echoes(tty.clone());
2081-
self.drain_echoes(&tty)?;
2082+
// Echo drain is best-effort in the write path; Ok(false) means
2083+
// the hardware FIFO was full and draining will be retried by
2084+
// write_wakeup once the FIFO has room (DO_WRITE_WAKEUP is set).
2085+
// Known limitation: this can add latency to echo output during
2086+
// heavy writes, but the alternative (retry loop here) risks
2087+
// starving the write path.
2088+
match self.drain_echoes(&tty) {
2089+
Ok(false) => log::debug!("drain_echoes incomplete in write (FIFO full)"),
2090+
Err(e) => log::debug!("drain_echoes failed in write: {:?}", e),
2091+
Ok(true) => {}
2092+
}
20822093

20832094
let mut offset = 0;
20842095
loop {
@@ -2540,7 +2551,9 @@ impl TtyLineDiscipline for NTtyLinediscipline {
25402551
fn write_wakeup(&self, tty: &TtyCore) -> Result<(), SystemError> {
25412552
if let Some(_output_guard) = self.output_lock.try_lock() {
25422553
if self.drain_opost_pending(tty)? {
2543-
self.drain_echoes(tty)?;
2554+
if let Err(e) = self.drain_echoes(tty) {
2555+
log::debug!("drain_echoes failed in write_wakeup: {:?}", e);
2556+
}
25442557
}
25452558
}
25462559
Ok(())
@@ -2588,7 +2601,9 @@ impl TtyLineDiscipline for NTtyLinediscipline {
25882601
let ret = ldata.receive_buf_common(tty.clone(), buf, flags, count, false);
25892602
drop(ldata);
25902603
if let Some(_output_guard) = self.output_lock.try_lock() {
2591-
self.drain_echoes(&tty)?;
2604+
if let Err(e) = self.drain_echoes(&tty) {
2605+
log::debug!("drain_echoes failed in receive_buf: {:?}", e);
2606+
}
25922607
}
25932608
ret
25942609
}
@@ -2604,7 +2619,9 @@ impl TtyLineDiscipline for NTtyLinediscipline {
26042619
let ret = ldata.receive_buf_common(tty.clone(), buf, flags, count, true);
26052620
drop(ldata);
26062621
if let Some(_output_guard) = self.output_lock.try_lock() {
2607-
self.drain_echoes(&tty)?;
2622+
if let Err(e) = self.drain_echoes(&tty) {
2623+
log::debug!("drain_echoes failed in receive_buf2: {:?}", e);
2624+
}
26082625
}
26092626
ret
26102627
}

0 commit comments

Comments
 (0)