diff --git a/kernel/pty/pty.cpp b/kernel/pty/pty.cpp index d36fe95d..9fede3ab 100644 --- a/kernel/pty/pty.cpp +++ b/kernel/pty/pty.cpp @@ -5,6 +5,7 @@ #include "mm/heap.h" #include "mm/uaccess.h" #include "dynpriv/dynpriv.h" +#include "signals/signal.h" #include "sync/poll.h" #include "sync/wait_queue.h" #include "terminal/terminal.h" @@ -18,6 +19,7 @@ constexpr uint32_t TCSETSF = 0x5404; constexpr uint32_t TIOCGWINSZ = 0x5413; constexpr uint32_t TIOCSWINSZ = 0x5414; +constexpr uint32_t LINUX_ISIG = 0x0001; constexpr uint32_t LINUX_ECHO = 0x0008; constexpr uint32_t LINUX_ICANON = 0x0002; @@ -46,6 +48,14 @@ __PRIVILEGED_CODE static void pty_echo_fn(void* ctx, const uint8_t* buf, size_t (void)ring_buffer_write(chan->m_output_rb, buf, len, true); } +__PRIVILEGED_CODE static void pty_signal_fn(void* ctx, uint32_t sig) { + auto* chan = static_cast(ctx); + uint32_t fg = __atomic_load_n(&chan->m_fg_group, __ATOMIC_ACQUIRE); + if (fg) { + (void)signals::send_to_group_id(fg, sig); + } +} + // Master ops static ssize_t pty_master_read( @@ -80,6 +90,7 @@ static ssize_t pty_master_write( result = resource::ERR_PIPE; } else { terminal::ld_input_buf(&chan->m_ld, chan->m_input_rb, &chan->m_echo, + &chan->m_sig, static_cast(ksrc), count); result = static_cast(count); } @@ -206,6 +217,9 @@ static int32_t do_tcgets(pty_channel* chan, uint64_t arg) { if (chan->m_ld.mode != terminal::LD_MODE_RAW) { t.c_lflag = LINUX_ICANON | LINUX_ECHO; } + if (chan->m_ld.isig) { + t.c_lflag |= LINUX_ISIG; + } int32_t rc = mm::uaccess::copy_to_user( reinterpret_cast(arg), &t, sizeof(t)); @@ -226,7 +240,38 @@ static int32_t do_tcsets(pty_channel* chan, uint64_t arg) { ? terminal::STLX_TCSETS_COOKED : terminal::STLX_TCSETS_RAW; + // The mode shortcut pairs ISIG with it, the termios bit then decides terminal::ld_set_mode(&chan->m_ld, mode); + terminal::ld_set_isig(&chan->m_ld, (t.c_lflag & LINUX_ISIG) != 0); + return resource::OK; +} + +static int32_t do_tiocgpgrp(pty_channel* chan, uint64_t arg) { + int32_t g = static_cast( + __atomic_load_n(&chan->m_fg_group, __ATOMIC_ACQUIRE)); + + int32_t rc = mm::uaccess::copy_to_user( + reinterpret_cast(arg), &g, sizeof(g)); + + return (rc == mm::uaccess::OK) ? resource::OK : resource::ERR_INVAL; +} + +static int32_t do_tiocspgrp(pty_channel* chan, uint64_t arg) { + int32_t g = 0; + int32_t rc = mm::uaccess::copy_from_user( + &g, reinterpret_cast(arg), sizeof(g)); + if (rc != mm::uaccess::OK || g < 0) { + return resource::ERR_INVAL; + } + + // POSIX requires an existing process group, 0 clears the foreground + if (g > 0 && + signals::send_to_group_id(static_cast(g), 0) != signals::OK) { + return resource::ERR_INVAL; + } + + __atomic_store_n(&chan->m_fg_group, static_cast(g), + __ATOMIC_RELEASE); return resource::OK; } @@ -257,6 +302,8 @@ static int32_t pty_termios_ioctl(pty_channel* chan, uint32_t cmd, uint64_t arg) case TCSETSF: return do_tcsets(chan, arg); case TIOCGWINSZ: return do_tiocgwinsz(chan, arg); case TIOCSWINSZ: return do_tiocswinsz(chan, arg); + case terminal::TIOCGPGRP: return do_tiocgpgrp(chan, arg); + case terminal::TIOCSPGRP: return do_tiocspgrp(chan, arg); case terminal::STLX_TCSETS_RAW: case terminal::STLX_TCSETS_COOKED: return terminal::ld_set_mode(&chan->m_ld, cmd); default: return resource::ERR_INVAL; @@ -367,8 +414,10 @@ __PRIVILEGED_CODE int32_t create_pair( terminal::ld_init(&chan->m_ld); chan->m_echo = { pty_echo_fn, chan.ptr() }; + chan->m_sig = { pty_signal_fn, chan.ptr() }; chan->m_id = __atomic_fetch_add(&g_next_pty_id, 1, __ATOMIC_RELAXED); chan->m_oflags = PTY_OFLAG_ONLCR; + chan->m_fg_group = 0; chan->m_winsize = { 24, 80, 0, 0 }; auto* ep_master = heap::kalloc_new(); diff --git a/kernel/pty/pty.h b/kernel/pty/pty.h index 89fb8be1..b85361f9 100644 --- a/kernel/pty/pty.h +++ b/kernel/pty/pty.h @@ -31,8 +31,10 @@ struct pty_channel : rc::ref_counted { ring_buffer* m_output_rb; // slave write -> master read terminal::line_discipline m_ld; terminal::echo_target m_echo; + terminal::signal_target m_sig; uint32_t m_id; uint32_t m_oflags; // output processing flags + uint32_t m_fg_group; // foreground process group, 0 = none pty_winsize m_winsize; // set via TIOCSWINSZ from either end /** @note Privilege: **required** */ diff --git a/kernel/signals/signal.cpp b/kernel/signals/signal.cpp index b352785f..f9aa716c 100644 --- a/kernel/signals/signal.cpp +++ b/kernel/signals/signal.cpp @@ -1,6 +1,7 @@ #include "signals/signal.h" #include "sched/sched.h" #include "sched/task.h" +#include "sched/task_registry.h" #include "timer/timer.h" #include "common/logging.h" @@ -13,6 +14,10 @@ enum class send_verdict : uint8_t { HANDLED, // a user handler is installed, wake the target to deliver }; +// Distinct groups remembered during one group-id send. Signals coalesce, +// so duplicate sends past this window are harmless extra wakes. +constexpr uint32_t MAX_GROUP_SEND_GROUPS = 64; + /** * Clear pending instances of sig from the shared set and every thread. * @note Privilege: **required** @@ -265,6 +270,37 @@ __PRIVILEGED_CODE int32_t send_to_group(sched::thread_group* tg, uint32_t sig) { return OK; } +__PRIVILEGED_CODE int32_t send_to_group_id(uint32_t group_id, uint32_t sig) { + sched::thread_group* seen[MAX_GROUP_SEND_GROUPS]; + uint32_t seen_count = 0; + bool found = false; + + sync::irq_state irq = sched::g_task_registry.lock(); + sched::g_task_registry.for_each_locked([&](sched::task& t) { + sched::thread_group* tg = t.group; + if (!tg || __atomic_load_n(&tg->group_id, __ATOMIC_ACQUIRE) != group_id) { + return; + } + + for (uint32_t i = 0; i < seen_count; i++) { + if (seen[i] == tg) { + return; + } + } + if (seen_count < MAX_GROUP_SEND_GROUPS) { + seen[seen_count++] = tg; + } + + found = true; + if (sig != 0) { + send_to_group(tg, sig); + } + }); + sched::g_task_registry.unlock(irq); + + return found ? OK : ERR_INVAL; +} + __PRIVILEGED_CODE uint32_t fatal_pending(sched::task* t) { sig_set_t pending = __atomic_load_n(&t->sig.pending, __ATOMIC_ACQUIRE); sig_set_t shared = t->group diff --git a/kernel/signals/signal.h b/kernel/signals/signal.h index 1da72e97..372cac29 100644 --- a/kernel/signals/signal.h +++ b/kernel/signals/signal.h @@ -70,6 +70,14 @@ __PRIVILEGED_CODE int32_t send_to_task(sched::task* t, uint32_t sig); */ __PRIVILEGED_CODE int32_t send_to_group(sched::thread_group* tg, uint32_t sig); +/** + * @brief Send sig to every process in the process group group_id, where + * sig 0 only probes for existence. Safe from ISR context. + * @return OK when at least one process matched, ERR_INVAL otherwise. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE int32_t send_to_group_id(uint32_t group_id, uint32_t sig); + /** * @brief Fatal signal the task must die from, or 0. * A set kill flag reports the group's recorded exit signal (SIGKILL if diff --git a/kernel/syscall/handlers/sys_signal.cpp b/kernel/syscall/handlers/sys_signal.cpp index de8e632a..bfeb9ea8 100644 --- a/kernel/syscall/handlers/sys_signal.cpp +++ b/kernel/syscall/handlers/sys_signal.cpp @@ -12,10 +12,6 @@ static constexpr uint64_t SIGSET_SIZE = 8; // Highest value representable as a task or group id static constexpr int64_t TASK_ID_LIMIT = 0xFFFFFFFF; -// Distinct groups remembered during one group kill. Signals coalesce, so -// duplicate sends past this window are harmless extra wakes, never misses. -static constexpr uint32_t MAX_KILL_GROUPS = 64; - static int64_t map_send_error(int32_t rc) { switch (rc) { case signals::OK: return 0; @@ -24,37 +20,10 @@ static int64_t map_send_error(int32_t rc) { } } -// Send sig to every process in the group, where sig 0 only probes existence. -// Group pointers stay valid because registered tasks pin their groups. +// Send sig to every process in the group, where sig 0 only probes existence static int64_t kill_process_group(uint32_t group_id, uint32_t sig) { - sched::thread_group* seen[MAX_KILL_GROUPS]; - uint32_t seen_count = 0; - bool found = false; - - sync::irq_state irq = sched::g_task_registry.lock(); - sched::g_task_registry.for_each_locked([&](sched::task& t) { - sched::thread_group* tg = t.group; - if (!tg || __atomic_load_n(&tg->group_id, __ATOMIC_ACQUIRE) != group_id) { - return; - } - - for (uint32_t i = 0; i < seen_count; i++) { - if (seen[i] == tg) { - return; - } - } - if (seen_count < MAX_KILL_GROUPS) { - seen[seen_count++] = tg; - } - - found = true; - if (sig != 0) { - signals::send_to_group(tg, sig); - } - }); - sched::g_task_registry.unlock(irq); - - return found ? 0 : syscall::ESRCH; + return signals::send_to_group_id(group_id, sig) == signals::OK + ? 0 : syscall::ESRCH; } // Thread-directed send shared by tkill and tgkill, tgid 0 skips the pair check diff --git a/kernel/terminal/console_node.cpp b/kernel/terminal/console_node.cpp index f7dc313b..f8b87aac 100644 --- a/kernel/terminal/console_node.cpp +++ b/kernel/terminal/console_node.cpp @@ -22,8 +22,8 @@ ssize_t console_node::write(fs::file*, const void* buf, size_t count) { return static_cast(count); } -int32_t console_node::ioctl(fs::file*, uint32_t cmd, uint64_t) { - return terminal::set_mode(cmd); +int32_t console_node::ioctl(fs::file*, uint32_t cmd, uint64_t arg) { + return terminal::console_ioctl(cmd, arg); } int32_t console_node::getattr(fs::vattr* attr) { diff --git a/kernel/terminal/line_discipline.cpp b/kernel/terminal/line_discipline.cpp index 12913be4..58a2e6eb 100644 --- a/kernel/terminal/line_discipline.cpp +++ b/kernel/terminal/line_discipline.cpp @@ -1,20 +1,61 @@ #include "terminal/line_discipline.h" #include "terminal/terminal.h" +#include "signals/signal_types.h" #include "common/ring_buffer.h" #include "dynpriv/dynpriv.h" namespace terminal { +// Signal-generating control bytes (termios VINTR/VQUIT/VSUSP defaults) +constexpr char LD_CH_INTR = 0x03; // ^C +constexpr char LD_CH_QUIT = 0x1C; // ^backslash +constexpr char LD_CH_SUSP = 0x1A; // ^Z + void ld_init(line_discipline* ld) { ld->mode = 0; + ld->isig = 1; ld->line_len = 0; ld->prev_char = 0; ld->lock = sync::SPINLOCK_INIT; } +static uint32_t signal_for_char(char c) { + switch (c) { + case LD_CH_INTR: return signals::SIGINT; + case LD_CH_QUIT: return signals::SIGQUIT; + case LD_CH_SUSP: return signals::SIGTSTP; + default: return 0; + } +} + +// Echo "^X" plus CRLF so the next output starts on a fresh line +static void echo_signal_char(const echo_target* echo, char c) { + if (echo && echo->write) { + const uint8_t seq[] = {'^', static_cast(c + 0x40), '\r', '\n'}; + echo->write(echo->ctx, seq, sizeof(seq)); + } +} + static void ld_process_byte(line_discipline* ld, ring_buffer* sink, - const echo_target* echo, char c, - bool hold_lock, sync::irq_state& irq) { + const echo_target* echo, const signal_target* sig, + char c, bool hold_lock, sync::irq_state& irq) { + uint32_t signum = ld->isig ? signal_for_char(c) : 0; + if (signum) { + // The byte becomes a signal instead of input, and the pending + // line is flushed (Linux ISIG semantics) + bool cooked = ld->mode != LD_MODE_RAW; + ld->line_len = 0; + ld->prev_char = c; + if (!hold_lock) sync::spin_unlock_irqrestore(ld->lock, irq); + if (cooked) { + echo_signal_char(echo, c); + } + if (sig && sig->send) { + sig->send(sig->ctx, signum); + } + return; + } + if (ld->mode == LD_MODE_RAW) { ld->prev_char = c; if (!hold_lock) sync::spin_unlock_irqrestore(ld->lock, irq); @@ -70,24 +111,28 @@ static void ld_process_byte(line_discipline* ld, ring_buffer* sink, } __PRIVILEGED_CODE void ld_input(line_discipline* ld, ring_buffer* sink, - const echo_target* echo, char c) { + const echo_target* echo, + const signal_target* sig, char c) { sync::irq_state irq = sync::spin_lock_irqsave(ld->lock); - ld_process_byte(ld, sink, echo, c, false, irq); + ld_process_byte(ld, sink, echo, sig, c, false, irq); } __PRIVILEGED_CODE void ld_input_buf(line_discipline* ld, ring_buffer* sink, const echo_target* echo, + const signal_target* sig, const char* buf, size_t len) { sync::irq_state irq = sync::spin_lock_irqsave(ld->lock); - if (ld->mode == LD_MODE_RAW) { + // The raw bulk path only applies with ISIG off, interception needs + // the per-byte scan + if (ld->mode == LD_MODE_RAW && !ld->isig) { sync::spin_unlock_irqrestore(ld->lock, irq); (void)ring_buffer_write(sink, reinterpret_cast(buf), len, true); return; } for (size_t i = 0; i < len; i++) { - ld_process_byte(ld, sink, echo, buf[i], true, irq); + ld_process_byte(ld, sink, echo, sig, buf[i], true, irq); } sync::spin_unlock_irqrestore(ld->lock, irq); @@ -109,9 +154,20 @@ int32_t ld_set_mode(line_discipline* ld, uint32_t cmd) { ld->line_len = 0; ld->mode = new_mode; } + // The shortcuts pair ISIG with the mode, raw callers expect + // control bytes verbatim (cfmakeraw clears ISIG the same way) + ld->isig = (new_mode == LD_MODE_RAW) ? 0u : 1u; sync::spin_unlock_irqrestore(ld->lock, irq); }); return OK; } +void ld_set_isig(line_discipline* ld, bool on) { + RUN_ELEVATED({ + sync::irq_state irq = sync::spin_lock_irqsave(ld->lock); + ld->isig = on ? 1u : 0u; + sync::spin_unlock_irqrestore(ld->lock, irq); + }); +} + } // namespace terminal diff --git a/kernel/terminal/line_discipline.h b/kernel/terminal/line_discipline.h index fe31e743..1ef35b75 100644 --- a/kernel/terminal/line_discipline.h +++ b/kernel/terminal/line_discipline.h @@ -16,8 +16,16 @@ struct echo_target { void* ctx; }; +// Receiver for signals generated by intercepted control bytes (ISIG). +// The send callback must be nonblocking and safe from ISR context. +struct signal_target { + void (*send)(void* ctx, uint32_t sig); + void* ctx; +}; + struct line_discipline { uint32_t mode; + uint32_t isig; // 1 = intercept INTR/QUIT/SUSP control bytes char line_buf[LD_LINE_BUF_MAX + 1]; size_t line_len; char prev_char; @@ -25,7 +33,7 @@ struct line_discipline { }; /** - * @brief Initialize a line discipline to cooked mode with empty state. + * @brief Initialize a line discipline to cooked mode with ISIG enabled. */ void ld_init(line_discipline* ld); @@ -35,32 +43,43 @@ void ld_init(line_discipline* ld); * @param ld Line discipline state. * @param sink Ring buffer where processed input is delivered. * @param echo Where echo bytes are sent (cooked mode only). + * @param sig Where intercepted control bytes deliver signals (ISIG). * @param c The input byte. * @note Privilege: **required** */ __PRIVILEGED_CODE void ld_input(line_discipline* ld, ring_buffer* sink, - const echo_target* echo, char c); + const echo_target* echo, + const signal_target* sig, char c); /** * @brief Process a buffer of input bytes through the line discipline. * Acquires ld->lock once for the entire buffer. More efficient than * per-byte ld_input for process-context callers (PTY master write). - * Echo and ring_buffer_write are called while holding ld->lock - * and must be nonblocking. + * Echo, ring_buffer_write, and signal sends are called while holding + * ld->lock and must be nonblocking. * @note Privilege: **required** */ __PRIVILEGED_CODE void ld_input_buf(line_discipline* ld, ring_buffer* sink, const echo_target* echo, + const signal_target* sig, const char* buf, size_t len); /** * @brief Switch between raw and cooked mode. Resets line buffer. - * Elevates internally for the spinlock critical section. + * ISIG follows the shortcut: raw disables it, cooked enables it, and a + * later TCSETS can set it independently. Elevates internally for the + * spinlock critical section. * @param cmd STLX_TCSETS_RAW or STLX_TCSETS_COOKED. * @return OK on success, ERR on invalid cmd. */ int32_t ld_set_mode(line_discipline* ld, uint32_t cmd); +/** + * @brief Enable or disable signal-character interception (termios ISIG). + * Elevates internally for the spinlock critical section. + */ +void ld_set_isig(line_discipline* ld, bool on); + } // namespace terminal #endif // STELLUX_TERMINAL_LINE_DISCIPLINE_H diff --git a/kernel/terminal/terminal.cpp b/kernel/terminal/terminal.cpp index cc2440f9..3a0f0e52 100644 --- a/kernel/terminal/terminal.cpp +++ b/kernel/terminal/terminal.cpp @@ -4,10 +4,14 @@ #include "common/ring_buffer.h" #include "io/serial.h" #include "resource/resource.h" +#include "signals/signal.h" #include "common/logging.h" +#include "dynpriv/dynpriv.h" +#include "fs/fs.h" #include "fs/fstypes.h" #include "fs/devfs/devfs.h" #include "mm/heap.h" +#include "mm/uaccess.h" #include "sync/poll.h" namespace terminal { @@ -17,6 +21,7 @@ constexpr size_t INPUT_RING_CAPACITY = 4096; __PRIVILEGED_BSS static struct { ring_buffer* input_rb; line_discipline ld; + uint32_t fg_group; // foreground process group, 0 = none } g_console; __PRIVILEGED_CODE static void serial_echo(void* ctx, const uint8_t* buf, size_t len) { @@ -29,6 +34,19 @@ __PRIVILEGED_DATA static const echo_target g_serial_echo = { nullptr, }; +__PRIVILEGED_CODE static void console_signal_fn(void* ctx, uint32_t sig) { + (void)ctx; + uint32_t fg = __atomic_load_n(&g_console.fg_group, __ATOMIC_ACQUIRE); + if (fg) { + (void)signals::send_to_group_id(fg, sig); + } +} + +__PRIVILEGED_DATA static const signal_target g_console_sig = { + console_signal_fn, + nullptr, +}; + __PRIVILEGED_CODE int32_t init() { g_console.input_rb = ring_buffer_create(INPUT_RING_CAPACITY); if (!g_console.input_rb) { @@ -60,7 +78,8 @@ __PRIVILEGED_CODE int32_t init() { } __PRIVILEGED_CODE void input_char(char c) { - ld_input(&g_console.ld, g_console.input_rb, &g_serial_echo, c); + ld_input(&g_console.ld, g_console.input_rb, &g_serial_echo, + &g_console_sig, c); } __PRIVILEGED_CODE ring_buffer* console_input_rb() { @@ -122,4 +141,43 @@ int32_t set_mode(uint32_t cmd) { return ld_set_mode(&g_console.ld, cmd); } +int32_t console_ioctl(uint32_t cmd, uint64_t arg) { + // Failures use fs error codes: this sits on the /dev/console node + // path, where the terminal ERR value would read as fs::ERR_NOENT + if (cmd == TIOCGPGRP) { + int32_t g = 0; + RUN_ELEVATED({ + g = static_cast( + __atomic_load_n(&g_console.fg_group, __ATOMIC_ACQUIRE)); + }); + return mm::uaccess::copy_to_user( + reinterpret_cast(arg), &g, sizeof(g)) == mm::uaccess::OK + ? OK : fs::ERR_INVAL; + } + + if (cmd == TIOCSPGRP) { + int32_t g = 0; + if (mm::uaccess::copy_from_user( + &g, reinterpret_cast(arg), sizeof(g)) != mm::uaccess::OK + || g < 0) { + return fs::ERR_INVAL; + } + + int32_t result = OK; + RUN_ELEVATED({ + // POSIX requires an existing process group, 0 clears the foreground + if (g > 0 && signals::send_to_group_id( + static_cast(g), 0) != signals::OK) { + result = fs::ERR_INVAL; + } else { + __atomic_store_n(&g_console.fg_group, static_cast(g), + __ATOMIC_RELEASE); + } + }); + return result; + } + + return set_mode(cmd); +} + } // namespace terminal diff --git a/kernel/terminal/terminal.h b/kernel/terminal/terminal.h index ece20600..6729082c 100644 --- a/kernel/terminal/terminal.h +++ b/kernel/terminal/terminal.h @@ -16,6 +16,10 @@ constexpr int32_t ERR = -1; constexpr uint32_t STLX_TCSETS_RAW = 0x7301; constexpr uint32_t STLX_TCSETS_COOKED = 0x7302; +// Linux tty ioctls for the terminal foreground process group +constexpr uint32_t TIOCGPGRP = 0x540F; +constexpr uint32_t TIOCSPGRP = 0x5410; + /** * @brief Initialize the global console terminal. Creates the input ring * buffer, registers as the serial RX callback, enables serial RX @@ -47,6 +51,14 @@ __PRIVILEGED_CODE ring_buffer* console_input_rb(); */ int32_t set_mode(uint32_t cmd); +/** + * @brief Console terminal ioctl handling: the foreground process group + * ioctls plus the mode-switch commands set_mode accepts. + * @return OK on success, fs::ERR_INVAL on a bad foreground argument, + * ERR on an unsupported cmd. + */ +int32_t console_ioctl(uint32_t cmd, uint64_t arg); + /** * @brief Get the terminal resource ops table for creating resource_objects. */ diff --git a/kernel/tests/pty/pty.test.cpp b/kernel/tests/pty/pty.test.cpp index 98c1905d..e8e8487f 100644 --- a/kernel/tests/pty/pty.test.cpp +++ b/kernel/tests/pty/pty.test.cpp @@ -7,6 +7,7 @@ #include "sched/task.h" #include "terminal/terminal.h" #include "terminal/line_discipline.h" +#include "signals/signal_types.h" #include "common/ring_buffer.h" #include "fs/fstypes.h" @@ -202,3 +203,154 @@ TEST(pty_test, raw_mode_no_echo) { EXPECT_EQ(resource::close(task, hm), resource::OK); EXPECT_EQ(resource::close(task, hs), resource::OK); } + +// Captures the last signal an ISIG interception delivered +static uint32_t g_isig_caught; + +static void isig_capture(void*, uint32_t sig) { + g_isig_caught = sig; +} + +TEST(pty_test, isig_intercepts_interrupt_byte) { + sched::task* task = sched::current(); + ASSERT_NOT_NULL(task); + + resource::resource_object* master = nullptr; + resource::resource_object* slave = nullptr; + ASSERT_EQ(pty::create_pair(&master, &slave), resource::OK); + + resource::handle_t hm = -1; + resource::handle_t hs = -1; + ASSERT_EQ(resource::alloc_handle(&task->handles, master, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hm), resource::HANDLE_OK); + resource::resource_release(master); + ASSERT_EQ(resource::alloc_handle(&task->handles, slave, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hs), resource::HANDLE_OK); + resource::resource_release(slave); + + auto* ep = static_cast(slave->impl); + ep->channel->m_sig = { isig_capture, nullptr }; + g_isig_caught = 0; + + // ^C mid-line: the pending line is flushed and the byte swallowed + ASSERT_EQ(resource::write(task, hm, "ab\x03", 3), static_cast(3)); + EXPECT_EQ(g_isig_caught, signals::SIGINT); + + // The echo carries "ab" then the "^C" caret sequence + char echo_buf[16] = {}; + ASSERT_EQ(resource::read(task, hm, echo_buf, 16), static_cast(6)); + EXPECT_STREQ(echo_buf, "ab^C\r\n"); + + // Only the next complete line reaches the slave + ASSERT_EQ(resource::write(task, hm, "cd\r", 3), static_cast(3)); + char buf[16] = {}; + ASSERT_EQ(resource::read(task, hs, buf, 16), static_cast(3)); + EXPECT_STREQ(buf, "cd\n"); + + EXPECT_EQ(resource::close(task, hm), resource::OK); + EXPECT_EQ(resource::close(task, hs), resource::OK); +} + +TEST(pty_test, isig_quit_and_susp_bytes) { + sched::task* task = sched::current(); + ASSERT_NOT_NULL(task); + + resource::resource_object* master = nullptr; + resource::resource_object* slave = nullptr; + ASSERT_EQ(pty::create_pair(&master, &slave), resource::OK); + + resource::handle_t hm = -1; + resource::handle_t hs = -1; + ASSERT_EQ(resource::alloc_handle(&task->handles, master, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hm), resource::HANDLE_OK); + resource::resource_release(master); + ASSERT_EQ(resource::alloc_handle(&task->handles, slave, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hs), resource::HANDLE_OK); + resource::resource_release(slave); + + auto* ep = static_cast(slave->impl); + ep->channel->m_sig = { isig_capture, nullptr }; + + g_isig_caught = 0; + ASSERT_EQ(resource::write(task, hm, "\x1C", 1), static_cast(1)); + EXPECT_EQ(g_isig_caught, signals::SIGQUIT); + + g_isig_caught = 0; + ASSERT_EQ(resource::write(task, hm, "\x1A", 1), static_cast(1)); + EXPECT_EQ(g_isig_caught, signals::SIGTSTP); + + EXPECT_EQ(resource::close(task, hm), resource::OK); + EXPECT_EQ(resource::close(task, hs), resource::OK); +} + +TEST(pty_test, isig_raw_mode_passes_bytes) { + sched::task* task = sched::current(); + ASSERT_NOT_NULL(task); + + resource::resource_object* master = nullptr; + resource::resource_object* slave = nullptr; + ASSERT_EQ(pty::create_pair(&master, &slave), resource::OK); + + resource::handle_t hm = -1; + resource::handle_t hs = -1; + ASSERT_EQ(resource::alloc_handle(&task->handles, master, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hm), resource::HANDLE_OK); + resource::resource_release(master); + ASSERT_EQ(resource::alloc_handle(&task->handles, slave, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hs), resource::HANDLE_OK); + resource::resource_release(slave); + + auto* ep = static_cast(slave->impl); + ep->channel->m_sig = { isig_capture, nullptr }; + + // The raw shortcut clears ISIG, control bytes are plain input + terminal::ld_set_mode(&ep->channel->m_ld, terminal::STLX_TCSETS_RAW); + g_isig_caught = 0; + ASSERT_EQ(resource::write(task, hm, "\x03", 1), static_cast(1)); + EXPECT_EQ(g_isig_caught, 0U); + + char buf[4] = {}; + ASSERT_EQ(resource::read(task, hs, buf, 4), static_cast(1)); + EXPECT_EQ(buf[0], '\x03'); + + EXPECT_EQ(resource::close(task, hm), resource::OK); + EXPECT_EQ(resource::close(task, hs), resource::OK); +} + +TEST(pty_test, isig_reenabled_in_raw_mode) { + sched::task* task = sched::current(); + ASSERT_NOT_NULL(task); + + resource::resource_object* master = nullptr; + resource::resource_object* slave = nullptr; + ASSERT_EQ(pty::create_pair(&master, &slave), resource::OK); + + resource::handle_t hm = -1; + resource::handle_t hs = -1; + ASSERT_EQ(resource::alloc_handle(&task->handles, master, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hm), resource::HANDLE_OK); + resource::resource_release(master); + ASSERT_EQ(resource::alloc_handle(&task->handles, slave, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hs), resource::HANDLE_OK); + resource::resource_release(slave); + + auto* ep = static_cast(slave->impl); + ep->channel->m_sig = { isig_capture, nullptr }; + + // TCSETS can turn ISIG back on independently of raw mode + terminal::ld_set_mode(&ep->channel->m_ld, terminal::STLX_TCSETS_RAW); + terminal::ld_set_isig(&ep->channel->m_ld, true); + + g_isig_caught = 0; + ASSERT_EQ(resource::write(task, hm, "x\x03y", 3), static_cast(3)); + EXPECT_EQ(g_isig_caught, signals::SIGINT); + + // The surrounding bytes still pass through, the ^C does not + char buf[4] = {}; + ASSERT_EQ(resource::read(task, hs, buf, 4), static_cast(2)); + EXPECT_EQ(buf[0], 'x'); + EXPECT_EQ(buf[1], 'y'); + + EXPECT_EQ(resource::close(task, hm), resource::OK); + EXPECT_EQ(resource::close(task, hs), resource::OK); +} diff --git a/userland/apps/ptytest/src/ptytest.c b/userland/apps/ptytest/src/ptytest.c index 9bbcd2cd..20fc0d11 100644 --- a/userland/apps/ptytest/src/ptytest.c +++ b/userland/apps/ptytest/src/ptytest.c @@ -1,13 +1,112 @@ +#define _GNU_SOURCE #include #include #include #include #include +#include +#include #include #define STLX_TCSETS_RAW 0x7301 -int main(void) { +static void catch_int(int sig) { + (void)sig; + char marker = 'C'; + write(STDOUT_FILENO, &marker, 1); + _exit(42); +} + +/* Child mode for the ISIG test: report readiness, then block on stdin + * until the terminal's ^C delivers SIGINT into the handler. */ +static int catch_int_child(void) { + signal(SIGINT, catch_int); + write(STDOUT_FILENO, "R", 1); + char b; + read(STDIN_FILENO, &b, 1); + return 1; +} + +/* Spawn victim on the slave as its foreground group and press ^C */ +static int isig_run_victim(const char* path, const char** args, + int wait_ready, int* status) { + int master_fd, slave_fd; + if (pty_create(&master_fd, &slave_fd) < 0) { + printf("ptytest: isig pty_create failed\n"); + return -1; + } + + int proc = proc_create(path, args); + if (proc < 0) { + printf("ptytest: isig proc_create failed\n"); + close(master_fd); + close(slave_fd); + return -1; + } + proc_set_handle(proc, 0, slave_fd); + proc_set_handle(proc, 1, slave_fd); + proc_set_handle(proc, 2, slave_fd); + + process_info info; + if (proc_info(proc, &info) != 0 || + setpgid(info.pid, info.pid) != 0 || + tcsetpgrp(master_fd, info.pid) != 0) { + printf("ptytest: isig foreground setup failed\n"); + proc_detach(proc); + close(master_fd); + close(slave_fd); + return -1; + } + proc_start(proc); + + /* Wait for the readiness marker, or give the victim time to block */ + if (wait_ready) { + char b = 0; + while (read(master_fd, &b, 1) == 1 && b != 'R') {} + } else { + usleep(200 * 1000); + } + + char intr = 0x03; + write(master_fd, &intr, 1); + + proc_wait(proc, status); + close(master_fd); + close(slave_fd); + return 0; +} + +static int isig_test(void) { + /* Default disposition: ^C must kill the foreground child */ + static const char* sleep_args[] = { "60", NULL }; + int status = 0; + if (isig_run_victim("/bin/sleep", sleep_args, 0, &status) != 0) { + return -1; + } + if (!STLX_WIFSIGNALED(status) || STLX_WTERMSIG(status) != SIGINT) { + printf("ptytest: ISIG kill FAILED (status=%d)\n", status); + return -1; + } + printf("ptytest: ISIG ^C killed foreground child\n"); + + /* Installed handler: ^C must run it instead of killing */ + static const char* catch_args[] = { "--catch-int", NULL }; + status = 0; + if (isig_run_victim("/bin/ptytest", catch_args, 1, &status) != 0) { + return -1; + } + if (!STLX_WIFEXITED(status) || STLX_WEXITSTATUS(status) != 42) { + printf("ptytest: ISIG handler FAILED (status=%d)\n", status); + return -1; + } + printf("ptytest: ISIG ^C ran the child's handler\n"); + return 0; +} + +int main(int argc, char** argv) { + if (argc >= 2 && strcmp(argv[1], "--catch-int") == 0) { + return catch_int_child(); + } setvbuf(stdout, NULL, _IONBF, 0); int master_fd, slave_fd; @@ -54,6 +153,11 @@ int main(void) { close(slave_fd); close(master_fd); + + if (isig_test() != 0) { + return 1; + } + printf("ptytest: all tests passed\n"); return 0; } diff --git a/userland/apps/shell/src/shell.c b/userland/apps/shell/src/shell.c index bc3a8fd6..73cff7f7 100644 --- a/userland/apps/shell/src/shell.c +++ b/userland/apps/shell/src/shell.c @@ -6,6 +6,7 @@ #include #include #include +#include #include "line_edit.h" #include "parse.h" @@ -14,10 +15,36 @@ #define STLX_TCSETS_RAW 0x7301 #define STLX_TCSETS_COOKED 0x7302 +/* The shell's own process group, restored as foreground after each job */ +static int g_shell_pgrp; + static void shell_err(const char* s) { write(1, s, strlen(s)); } +static void set_foreground(int pgrp) { + if (pgrp > 0) tcsetpgrp(STDIN_FILENO, pgrp); +} + +/* Put a created (not yet started) child in a process group so terminal + * signals reach it and not the shell. With pgrp 0 the child leads a new + * group and becomes the foreground. Returns the group id, or -1. */ +static int foreground_child(int handle, int pgrp) { + process_info info; + if (proc_info(handle, &info) != 0 || + setpgid(info.pid, pgrp > 0 ? pgrp : info.pid) != 0) { + /* A group-less leader stays in the shell's group: clear the + * foreground so ^C drops instead of hitting the shell too */ + if (pgrp <= 0) tcsetpgrp(STDIN_FILENO, 0); + return -1; + } + if (pgrp <= 0) { + pgrp = info.pid; + set_foreground(pgrp); + } + return pgrp; +} + static int reap_status(int status) { if (STLX_WIFEXITED(status)) return STLX_WEXITSTATUS(status); if (STLX_WIFSIGNALED(status)) { @@ -134,9 +161,12 @@ static int run_single(const char* argv[], char* path_buf, if (redir_out >= 0) proc_set_handle(handle, STDOUT_FILENO, redir_out); + foreground_child(handle, 0); + if (proc_start(handle) < 0) { close(handle); close_redirect_fds(redir_in, redir_out); + set_foreground(g_shell_pgrp); shell_err("shell: failed to start process\r\n"); return 126; } @@ -147,6 +177,7 @@ static int run_single(const char* argv[], char* path_buf, int status = 0; proc_wait(handle, &status); ioctl(0, STLX_TCSETS_RAW, 0); + set_foreground(g_shell_pgrp); return reap_status(status); } @@ -154,6 +185,7 @@ static int run_single(const char* argv[], char* path_buf, static int run_pipeline(char* stages[], int nstages, char* path_buf) { int handles[MAX_PIPE_STAGES]; int prev_read_fd = -1; + int fg_pgrp = -1; for (int i = 0; i < nstages; i++) { /* Parse redirections first — modifies stage string in-place */ @@ -162,6 +194,7 @@ static int run_pipeline(char* stages[], int nstages, char* path_buf) { shell_err("shell: syntax error in redirection\r\n"); if (prev_read_fd >= 0) close(prev_read_fd); for (int j = 0; j < i; j++) proc_detach(handles[j]); + if (fg_pgrp > 0) set_foreground(g_shell_pgrp); return 1; } @@ -171,6 +204,7 @@ static int run_pipeline(char* stages[], int nstages, char* path_buf) { shell_err("shell: empty pipeline stage\r\n"); if (prev_read_fd >= 0) close(prev_read_fd); for (int j = 0; j < i; j++) proc_detach(handles[j]); + if (fg_pgrp > 0) set_foreground(g_shell_pgrp); return 1; } @@ -183,6 +217,7 @@ static int run_pipeline(char* stages[], int nstages, char* path_buf) { shell_err("shell: pipe failed\r\n"); if (prev_read_fd >= 0) close(prev_read_fd); for (int j = 0; j < i; j++) proc_detach(handles[j]); + if (fg_pgrp > 0) set_foreground(g_shell_pgrp); return 1; } } @@ -194,6 +229,7 @@ static int run_pipeline(char* stages[], int nstages, char* path_buf) { if (pipe_fds[0] >= 0) close(pipe_fds[0]); if (pipe_fds[1] >= 0) close(pipe_fds[1]); for (int j = 0; j < i; j++) proc_detach(handles[j]); + if (fg_pgrp > 0) set_foreground(g_shell_pgrp); return 1; } @@ -206,6 +242,7 @@ static int run_pipeline(char* stages[], int nstages, char* path_buf) { if (pipe_fds[0] >= 0) close(pipe_fds[0]); if (pipe_fds[1] >= 0) close(pipe_fds[1]); for (int j = 0; j < i; j++) proc_detach(handles[j]); + if (fg_pgrp > 0) set_foreground(g_shell_pgrp); return 127; } @@ -225,6 +262,15 @@ static int run_pipeline(char* stages[], int nstages, char* path_buf) { proc_set_handle(handle, STDOUT_FILENO, pipe_fds[1]); } + /* First stage leads the foreground group, later stages join it. + * If the leader setup failed, all stages stay in the shell's + * group with the foreground cleared. */ + if (i == 0) { + fg_pgrp = foreground_child(handle, 0); + } else if (fg_pgrp > 0) { + foreground_child(handle, fg_pgrp); + } + if (proc_start(handle) < 0) { shell_err("shell: failed to start process\r\n"); close(handle); @@ -233,6 +279,7 @@ static int run_pipeline(char* stages[], int nstages, char* path_buf) { if (pipe_fds[0] >= 0) close(pipe_fds[0]); if (pipe_fds[1] >= 0) close(pipe_fds[1]); for (int j = 0; j < i; j++) proc_detach(handles[j]); + set_foreground(g_shell_pgrp); return 126; } handles[i] = handle; @@ -253,6 +300,7 @@ static int run_pipeline(char* stages[], int nstages, char* path_buf) { int status = 0; proc_wait(handles[nstages - 1], &status); ioctl(0, STLX_TCSETS_RAW, 0); + set_foreground(g_shell_pgrp); return reap_status(status); } @@ -298,6 +346,8 @@ static int execute_line(char* line, char* path_buf, line_edit_state* editor, } int main(int argc, char** argv) { + g_shell_pgrp = getpgid(0); + /* Non-interactive command mode: `shell -c "command line"` (ssh exec). */ if (argc >= 3 && strcmp(argv[1], "-c") == 0) { char* cpath = malloc(256);