]> Projects (at) Tadryanom (dot) Me - AdrOS.git/commitdiff
tty: add basic job control (fg pgrp) + SIGTTIN/SIGTTOU
authorTulio A M Mendes <[email protected]>
Sun, 8 Feb 2026 10:49:56 +0000 (07:49 -0300)
committerTulio A M Mendes <[email protected]>
Sun, 8 Feb 2026 10:49:56 +0000 (07:49 -0300)
include/errno.h
include/process.h
include/syscall.h
src/arch/x86/idt.c
src/kernel/scheduler.c
src/kernel/syscall.c
src/kernel/tty.c
user/init.c

index 6f455883afc6b310c4997534be9db2bbe6d13092..fb82c75083e3881b66dc913b54704f18e9ec4283 100644 (file)
@@ -4,6 +4,7 @@
 #define EPERM 1
 #define ENOENT 2
 #define EIO 5
+#define EINTR 4
 #define EBADF 9
 #define ECHILD 10
 #define EFAULT 14
index 7ce978bd6419cbd42f13d3d270407f61400a06cc..7065bd4767cf6a3ce880f7b870131c3ae8648077 100644 (file)
@@ -22,6 +22,8 @@ struct file {
 
 #define PROCESS_MAX_FILES 16
 
+#define PROCESS_MAX_SIG 32
+
 struct process {
     uint32_t pid;
     uint32_t parent_pid;
@@ -37,6 +39,14 @@ struct process {
     int has_user_regs;
     struct registers user_regs;
 
+    // Minimal signals: handler pointers, blocked mask and pending mask.
+    // handlers[i] == 0 => default
+    // handlers[i] == 1 => ignore
+    // handlers[i] >= 2 => user handler address
+    uintptr_t sig_handlers[PROCESS_MAX_SIG];
+    uint32_t sig_blocked_mask;
+    uint32_t sig_pending_mask;
+
     int waiting;
     int wait_pid;
     int wait_result_pid;
index 8903e98d716518b0b5774a24f907f3faedf070cb..8da9645d2299b6eab8d204371086a2814f6723ab 100644 (file)
@@ -33,6 +33,9 @@ enum {
     SYSCALL_SETSID = 22,
     SYSCALL_SETPGID = 23,
     SYSCALL_GETPGRP = 24,
+
+    SYSCALL_SIGACTION = 25,
+    SYSCALL_SIGPROCMASK = 26,
 };
 
 #endif
index aa0d2570aab1d6f808f1e2852e25e301a840410c..b858cf5e94e624937f2b8f43c95a7e110081cbe8 100644 (file)
@@ -3,6 +3,7 @@
 #include "uart_console.h"
 #include "process.h"
 #include "spinlock.h"
+#include "uaccess.h"
 #include <stddef.h>
 
 #define IDT_ENTRIES 256
@@ -40,6 +41,59 @@ void idt_set_gate(uint8_t num, uint32_t base, uint16_t sel, uint8_t flags) {
     idt[num].flags   = flags; // Present(0x80) | DPL(00) | Type(0xE = 32bit Int Gate) -> 0x8E
 }
 
+static void deliver_signals_to_usermode(struct registers* regs) {
+    if (!regs) return;
+    if (!current_process) return;
+    if ((regs->cs & 3U) != 3U) return;
+
+    const uint32_t pending = current_process->sig_pending_mask;
+    const uint32_t blocked = current_process->sig_blocked_mask;
+    uint32_t ready = pending & ~blocked;
+    if (!ready) return;
+
+    int sig = -1;
+    for (int i = 1; i < PROCESS_MAX_SIG; i++) {
+        if ((ready & (1U << (uint32_t)i)) != 0U) {
+            sig = i;
+            break;
+        }
+    }
+    if (sig < 0) return;
+
+    current_process->sig_pending_mask &= ~(1U << (uint32_t)sig);
+
+    const uintptr_t h = current_process->sig_handlers[sig];
+    if (h == 1) {
+        return;
+    }
+
+    if (h == 0) {
+        if (sig == 11) {
+            process_exit_notify(128 + sig);
+            __asm__ volatile("sti");
+            schedule();
+            for (;;) __asm__ volatile("hlt");
+        }
+        return;
+    }
+
+    uint32_t frame[2];
+    frame[0] = regs->eip;
+    frame[1] = (uint32_t)sig;
+
+    const uint32_t new_esp = regs->useresp - (uint32_t)sizeof(frame);
+    if (copy_to_user((void*)(uintptr_t)new_esp, frame, sizeof(frame)) < 0) {
+        const int SIG_SEGV = 11;
+        process_exit_notify(128 + SIG_SEGV);
+        __asm__ volatile("sti");
+        schedule();
+        for (;;) __asm__ volatile("hlt");
+    }
+
+    regs->useresp = new_esp;
+    regs->eip = (uint32_t)h;
+}
+
 /* Reconfigure the PIC to remap IRQs from 0-15 to 32-47 */
 void pic_remap(void) {
     uint8_t a1, a2;
@@ -223,4 +277,6 @@ void isr_handler(struct registers* regs) {
         }
         outb(0x20, 0x20); // Master EOI
     }
+
+    deliver_signals_to_usermode(regs);
 }
index 1934dfcd7112e3f36e0e56e1edc7a9193da02d7e..827d5cfb51d20bc8526ba051f57317a1704f6a9a 100644 (file)
@@ -91,13 +91,12 @@ static void process_close_all_files_locked(struct process* p) {
 }
 
 int process_kill(uint32_t pid, int sig) {
-    // Minimal: support SIGKILL only.
     const int SIG_KILL = 9;
     if (pid == 0) return -EINVAL;
-    if (sig != SIG_KILL) return -EINVAL;
 
-    // Killing self: just exit via existing path.
-    if (current_process && current_process->pid == pid) {
+    if (sig <= 0 || sig >= PROCESS_MAX_SIG) return -EINVAL;
+
+    if (current_process && current_process->pid == pid && sig == SIG_KILL) {
         process_exit_notify(128 + sig);
         hal_cpu_enable_interrupts();
         schedule();
@@ -116,19 +115,26 @@ int process_kill(uint32_t pid, int sig) {
         return 0;
     }
 
-    process_close_all_files_locked(p);
-    p->exit_status = 128 + sig;
-    p->state = PROCESS_ZOMBIE;
+    if (sig == SIG_KILL) {
+        process_close_all_files_locked(p);
+        p->exit_status = 128 + sig;
+        p->state = PROCESS_ZOMBIE;
 
-    if (p->pid != 0) {
-        struct process* parent = process_find_locked(p->parent_pid);
-        if (parent && parent->state == PROCESS_BLOCKED && parent->waiting) {
-            if (parent->wait_pid == -1 || parent->wait_pid == (int)p->pid) {
-                parent->wait_result_pid = (int)p->pid;
-                parent->wait_result_status = p->exit_status;
-                parent->state = PROCESS_READY;
+        if (p->pid != 0) {
+            struct process* parent = process_find_locked(p->parent_pid);
+            if (parent && parent->state == PROCESS_BLOCKED && parent->waiting) {
+                if (parent->wait_pid == -1 || parent->wait_pid == (int)p->pid) {
+                    parent->wait_result_pid = (int)p->pid;
+                    parent->wait_result_status = p->exit_status;
+                    parent->state = PROCESS_READY;
+                }
             }
         }
+    } else {
+        p->sig_pending_mask |= (1U << (uint32_t)sig);
+        if (p->state == PROCESS_BLOCKED || p->state == PROCESS_SLEEPING) {
+            p->state = PROCESS_READY;
+        }
     }
 
     spin_unlock_irqrestore(&sched_lock, flags);
index 49dab830561f0dc64325185fbbe8ce827138b8a6..4db8412cc41fdd70c0bf07dd8aa4fb366da65989 100644 (file)
@@ -730,7 +730,6 @@ static int syscall_open_impl(const char* user_path, uint32_t flags) {
 
     fs_node_t* node = vfs_lookup(path);
     if (!node) return -ENOENT;
-    if (node->flags != FS_FILE && node->flags != FS_CHARDEVICE) return -EINVAL;
 
     struct file* f = (struct file*)kmalloc(sizeof(*f));
     if (!f) return -ENOMEM;
@@ -781,7 +780,6 @@ static int syscall_read_impl(int fd, void* user_buf, uint32_t len) {
         return (int)total;
     }
 
-    if (f->node->flags != FS_FILE) return -ESPIPE;
     if (!f->node->read) return -ESPIPE;
 
     uint8_t kbuf[256];
@@ -817,8 +815,8 @@ static int syscall_write_impl(int fd, const void* user_buf, uint32_t len) {
 
     struct file* f = fd_get(fd);
     if (!f || !f->node) return -EBADF;
-    if (f->node->flags != FS_FILE && f->node->flags != FS_CHARDEVICE) return -ESPIPE;
     if (!f->node->write) return -ESPIPE;
+    if (((f->node->flags & FS_FILE) == 0) && f->node->flags != FS_CHARDEVICE) return -ESPIPE;
 
     uint8_t kbuf[256];
     uint32_t total = 0;
@@ -830,9 +828,9 @@ static int syscall_write_impl(int fd, const void* user_buf, uint32_t len) {
             return -EFAULT;
         }
 
-        uint32_t wr = vfs_write(f->node, (f->node->flags == FS_FILE) ? f->offset : 0, chunk, kbuf);
+        uint32_t wr = vfs_write(f->node, ((f->node->flags & FS_FILE) != 0) ? f->offset : 0, chunk, kbuf);
         if (wr == 0) break;
-        if (f->node->flags == FS_FILE) f->offset += wr;
+        if ((f->node->flags & FS_FILE) != 0) f->offset += wr;
         total += wr;
         if (wr < chunk) break;
     }
@@ -872,6 +870,44 @@ static int syscall_getpgrp_impl(void) {
     return (int)current_process->pgrp_id;
 }
 
+static int syscall_sigaction_impl(int sig, uintptr_t handler, uintptr_t* old_out) {
+    if (!current_process) return -EINVAL;
+    if (sig <= 0 || sig >= PROCESS_MAX_SIG) return -EINVAL;
+
+    if (old_out) {
+        if (user_range_ok(old_out, sizeof(*old_out)) == 0) return -EFAULT;
+        uintptr_t oldh = current_process->sig_handlers[sig];
+        if (copy_to_user(old_out, &oldh, sizeof(oldh)) < 0) return -EFAULT;
+    }
+
+    current_process->sig_handlers[sig] = handler;
+    return 0;
+}
+
+static int syscall_sigprocmask_impl(uint32_t how, uint32_t mask, uint32_t* old_out) {
+    if (!current_process) return -EINVAL;
+
+    if (old_out) {
+        if (user_range_ok(old_out, sizeof(*old_out)) == 0) return -EFAULT;
+        uint32_t old = current_process->sig_blocked_mask;
+        if (copy_to_user(old_out, &old, sizeof(old)) < 0) return -EFAULT;
+    }
+
+    if (how == 0U) {
+        current_process->sig_blocked_mask = mask;
+        return 0;
+    }
+    if (how == 1U) {
+        current_process->sig_blocked_mask |= mask;
+        return 0;
+    }
+    if (how == 2U) {
+        current_process->sig_blocked_mask &= ~mask;
+        return 0;
+    }
+    return -EINVAL;
+}
+
 static void syscall_handler(struct registers* regs) {
     uint32_t syscall_no = regs->eax;
 
@@ -1070,6 +1106,22 @@ static void syscall_handler(struct registers* regs) {
         return;
     }
 
+    if (syscall_no == SYSCALL_SIGACTION) {
+        int sig = (int)regs->ebx;
+        uintptr_t handler = (uintptr_t)regs->ecx;
+        uintptr_t* old_out = (uintptr_t*)regs->edx;
+        regs->eax = (uint32_t)syscall_sigaction_impl(sig, handler, old_out);
+        return;
+    }
+
+    if (syscall_no == SYSCALL_SIGPROCMASK) {
+        uint32_t how = regs->ebx;
+        uint32_t mask = regs->ecx;
+        uint32_t* old_out = (uint32_t*)regs->edx;
+        regs->eax = (uint32_t)syscall_sigprocmask_impl(how, mask, old_out);
+        return;
+    }
+
     regs->eax = (uint32_t)-ENOSYS;
 }
 
index 44622169894d8d07d49e5d23ffa62ffad5b5c44d..a917e9e1db45efd71a8efd645e70bf2d50b95057 100644 (file)
@@ -28,6 +28,15 @@ static uint32_t waitq_tail = 0;
 
 static uint32_t tty_lflag = TTY_ICANON | TTY_ECHO;
 
+static uint32_t tty_session_id = 0;
+static uint32_t tty_fg_pgrp = 0;
+
+enum {
+    SIGTSTP = 20,
+    SIGTTIN = 21,
+    SIGTTOU = 22,
+};
+
 static int canon_empty(void) {
     return canon_head == canon_tail;
 }
@@ -39,6 +48,13 @@ int tty_write_kbuf(const void* kbuf, uint32_t len) {
     if (!kbuf) return -EFAULT;
     if (len > 1024 * 1024) return -EINVAL;
 
+    // Job control: background writes to controlling TTY generate SIGTTOU.
+    if (current_process && tty_session_id != 0 && current_process->session_id == tty_session_id &&
+        tty_fg_pgrp != 0 && current_process->pgrp_id != tty_fg_pgrp) {
+        (void)process_kill(current_process->pid, SIGTTOU);
+        return -EINTR;
+    }
+
     const char* p = (const char*)kbuf;
     for (uint32_t i = 0; i < len; i++) {
         uart_put_char(p[i]);
@@ -51,6 +67,13 @@ int tty_read_kbuf(void* kbuf, uint32_t len) {
     if (len > 1024 * 1024) return -EINVAL;
     if (!current_process) return -ECHILD;
 
+    // Job control: background reads from controlling TTY generate SIGTTIN.
+    if (tty_session_id != 0 && current_process->session_id == tty_session_id &&
+        tty_fg_pgrp != 0 && current_process->pgrp_id != tty_fg_pgrp) {
+        (void)process_kill(current_process->pid, SIGTTIN);
+        return -EINTR;
+    }
+
     while (1) {
         uintptr_t flags = spin_lock_irqsave(&tty_lock);
 
@@ -147,9 +170,14 @@ enum {
 int tty_ioctl(uint32_t cmd, void* user_arg) {
     if (!user_arg) return -EFAULT;
 
+    if (current_process && tty_session_id == 0 && current_process->session_id != 0) {
+        tty_session_id = current_process->session_id;
+        tty_fg_pgrp = current_process->pgrp_id;
+    }
+
     if (cmd == TTY_TIOCGPGRP) {
         if (user_range_ok(user_arg, sizeof(int)) == 0) return -EFAULT;
-        int fg = 0;
+        int fg = (int)tty_fg_pgrp;
         if (copy_to_user(user_arg, &fg, sizeof(fg)) < 0) return -EFAULT;
         return 0;
     }
@@ -158,7 +186,19 @@ int tty_ioctl(uint32_t cmd, void* user_arg) {
         if (user_range_ok(user_arg, sizeof(int)) == 0) return -EFAULT;
         int fg = 0;
         if (copy_from_user(&fg, user_arg, sizeof(fg)) < 0) return -EFAULT;
-        if (fg != 0) return -EINVAL;
+        if (!current_process) return -EINVAL;
+
+        // If there is no controlling session yet, only allow setting fg=0.
+        // This matches early-boot semantics used by userland smoke tests.
+        if (tty_session_id == 0) {
+            if (fg != 0) return -EPERM;
+            tty_fg_pgrp = 0;
+            return 0;
+        }
+
+        if (current_process->session_id != tty_session_id) return -EPERM;
+        if (fg < 0) return -EINVAL;
+        tty_fg_pgrp = (uint32_t)fg;
         return 0;
     }
 
@@ -250,6 +290,8 @@ void tty_init(void) {
     line_len = 0;
     canon_head = canon_tail = 0;
     waitq_head = waitq_tail = 0;
+    tty_session_id = 0;
+    tty_fg_pgrp = 0;
 
     keyboard_set_callback(tty_keyboard_cb);
 }
@@ -259,6 +301,13 @@ int tty_write(const void* user_buf, uint32_t len) {
     if (len > 1024 * 1024) return -EINVAL;
     if (user_range_ok(user_buf, (size_t)len) == 0) return -EFAULT;
 
+    // Job control: background writes to controlling TTY generate SIGTTOU.
+    if (current_process && tty_session_id != 0 && current_process->session_id == tty_session_id &&
+        tty_fg_pgrp != 0 && current_process->pgrp_id != tty_fg_pgrp) {
+        (void)process_kill(current_process->pid, SIGTTOU);
+        return -EINTR;
+    }
+
     char kbuf[256];
     uint32_t remaining = len;
     uintptr_t up = (uintptr_t)user_buf;
@@ -286,6 +335,13 @@ int tty_read(void* user_buf, uint32_t len) {
     if (user_range_ok(user_buf, (size_t)len) == 0) return -EFAULT;
     if (!current_process) return -ECHILD;
 
+    // Job control: background reads from controlling TTY generate SIGTTIN.
+    if (tty_session_id != 0 && current_process->session_id == tty_session_id &&
+        tty_fg_pgrp != 0 && current_process->pgrp_id != tty_fg_pgrp) {
+        (void)process_kill(current_process->pid, SIGTTIN);
+        return -EINTR;
+    }
+
     while (1) {
         uintptr_t flags = spin_lock_irqsave(&tty_lock);
 
index 429c94cbb09a0c7ef89e01adbad1253870f267a2..5f798cc501110d39e42068642394790b3825dbd9 100644 (file)
@@ -25,6 +25,9 @@ enum {
     SYSCALL_SETSID = 22,
     SYSCALL_SETPGID = 23,
     SYSCALL_GETPGRP = 24,
+
+    SYSCALL_SIGACTION = 25,
+    SYSCALL_SIGPROCMASK = 26,
 };
 
 enum {
@@ -56,7 +59,10 @@ enum {
 
 enum {
     SIGKILL = 9,
+    SIGUSR1 = 10,
     SIGSEGV = 11,
+    SIGTTIN = 21,
+    SIGTTOU = 22,
 };
 
 enum {
@@ -90,6 +96,62 @@ static int sys_write(int fd, const void* buf, uint32_t len) {
     return ret;
 }
 
+static void write_int_dec(int v) {
+    char buf[16];
+    int i = 0;
+    if (v == 0) {
+        buf[i++] = '0';
+    } else {
+        int neg = 0;
+        if (v < 0) {
+            neg = 1;
+            v = -v;
+        }
+        while (v > 0 && i < (int)sizeof(buf)) {
+            buf[i++] = (char)('0' + (v % 10));
+            v /= 10;
+        }
+        if (neg && i < (int)sizeof(buf)) {
+            buf[i++] = '-';
+        }
+        for (int j = 0; j < i / 2; j++) {
+            char t = buf[j];
+            buf[j] = buf[i - 1 - j];
+            buf[i - 1 - j] = t;
+        }
+    }
+    (void)sys_write(1, buf, (uint32_t)i);
+}
+
+static void write_hex8(uint8_t v) {
+    static const char hex[] = "0123456789ABCDEF";
+    char b[2];
+    b[0] = hex[(v >> 4) & 0xF];
+    b[1] = hex[v & 0xF];
+    (void)sys_write(1, b, 2);
+}
+
+static void write_hex32(uint32_t v) {
+    static const char hex[] = "0123456789ABCDEF";
+    char b[8];
+    for (int i = 0; i < 8; i++) {
+        uint32_t shift = (uint32_t)(28 - 4 * i);
+        b[i] = hex[(v >> shift) & 0xFU];
+    }
+    (void)sys_write(1, b, 8);
+}
+
+static int sys_sigaction(int sig, void (*handler)(int), uintptr_t* old_out) {
+    int ret;
+    __asm__ volatile(
+        "int $0x80"
+        : "=a"(ret)
+        : "a"(SYSCALL_SIGACTION), "b"(sig), "c"(handler), "d"(old_out)
+        : "memory"
+    );
+    return ret;
+}
+
 static int sys_select(uint32_t nfds, uint64_t* readfds, uint64_t* writefds, uint64_t* exceptfds, int32_t timeout) {
     int ret;
     __asm__ volatile(
@@ -332,6 +394,27 @@ __attribute__((noreturn)) static void sys_exit(int code) {
     __builtin_unreachable();
 }
 
+static volatile int got_usr1 = 0;
+static volatile int got_ttin = 0;
+static volatile int got_ttou = 0;
+
+static void usr1_handler(int sig) {
+    (void)sig;
+    got_usr1 = 1;
+    sys_write(1, "[init] SIGUSR1 handler OK\n",
+              (uint32_t)(sizeof("[init] SIGUSR1 handler OK\n") - 1));
+}
+
+static void ttin_handler(int sig) {
+    (void)sig;
+    got_ttin = 1;
+}
+
+static void ttou_handler(int sig) {
+    (void)sig;
+    got_ttou = 1;
+}
+
 void _start(void) {
     __asm__ volatile(
         "mov $0x23, %ax\n"
@@ -345,25 +428,29 @@ void _start(void) {
     (void)sys_write(1, msg, (uint32_t)(sizeof(msg) - 1));
 
     static const char path[] = "/bin/init.elf";
+
     int fd = sys_open(path, 0);
     if (fd < 0) {
-        sys_write(1, "[init] open failed\n", (uint32_t)(sizeof("[init] open failed\n") - 1));
+        sys_write(1, "[init] open failed fd=", (uint32_t)(sizeof("[init] open failed fd=") - 1));
+        write_int_dec(fd);
+        sys_write(1, "\n", 1);
         sys_exit(1);
     }
 
     uint8_t hdr[4];
-    int rd = sys_read(fd, hdr, (uint32_t)sizeof(hdr));
-    if (sys_close(fd) < 0) {
-        sys_write(1, "[init] close failed\n", (uint32_t)(sizeof("[init] close failed\n") - 1));
-        sys_exit(1);
-    }
-
+    int rd = sys_read(fd, hdr, 4);
+    (void)sys_close(fd);
     if (rd == 4 && hdr[0] == 0x7F && hdr[1] == 'E' && hdr[2] == 'L' && hdr[3] == 'F') {
         sys_write(1, "[init] open/read/close OK (ELF magic)\n",
                   (uint32_t)(sizeof("[init] open/read/close OK (ELF magic)\n") - 1));
     } else {
-        sys_write(1, "[init] read failed or bad header\n",
-                  (uint32_t)(sizeof("[init] read failed or bad header\n") - 1));
+        sys_write(1, "[init] read failed or bad header rd=", (uint32_t)(sizeof("[init] read failed or bad header rd=") - 1));
+        write_int_dec(rd);
+        sys_write(1, " hdr=", (uint32_t)(sizeof(" hdr=") - 1));
+        for (int i = 0; i < 4; i++) {
+            write_hex8(hdr[i]);
+        }
+        sys_write(1, "\n", 1);
         sys_exit(1);
     }
 
@@ -572,6 +659,8 @@ void _start(void) {
         }
         (void)sys_close(tfd);
 
+    }
+
     {
         int pid = sys_fork();
         if (pid < 0) {
@@ -748,6 +837,102 @@ void _start(void) {
                   (uint32_t)(sizeof("[init] ioctl(/dev/tty) OK\n") - 1));
     }
 
+    // A2: basic job control. A background pgrp read/write on controlling TTY should raise SIGTTIN/SIGTTOU.
+    {
+        int leader = sys_fork();
+        if (leader < 0) {
+            sys_write(1, "[init] fork(job control leader) failed\n",
+                      (uint32_t)(sizeof("[init] fork(job control leader) failed\n") - 1));
+            sys_exit(1);
+        }
+        if (leader == 0) {
+            int me = sys_getpid();
+            int sid = sys_setsid();
+            if (sid != me) {
+                sys_write(1, "[init] setsid(job control) failed\n",
+                          (uint32_t)(sizeof("[init] setsid(job control) failed\n") - 1));
+                sys_exit(1);
+            }
+
+            int tfd = sys_open("/dev/tty", 0);
+            if (tfd < 0) {
+                sys_write(1, "[init] open(/dev/tty) for job control failed\n",
+                          (uint32_t)(sizeof("[init] open(/dev/tty) for job control failed\n") - 1));
+                sys_exit(1);
+            }
+
+            // Touch ioctl to make kernel acquire controlling session/pgrp.
+            int fg = 0;
+            (void)sys_ioctl(tfd, TIOCGPGRP, &fg);
+
+            fg = me;
+            if (sys_ioctl(tfd, TIOCSPGRP, &fg) < 0) {
+                sys_write(1, "[init] ioctl TIOCSPGRP(job control) failed\n",
+                          (uint32_t)(sizeof("[init] ioctl TIOCSPGRP(job control) failed\n") - 1));
+                sys_exit(1);
+            }
+
+            int bg = sys_fork();
+            if (bg < 0) {
+                sys_write(1, "[init] fork(job control bg) failed\n",
+                          (uint32_t)(sizeof("[init] fork(job control bg) failed\n") - 1));
+                sys_exit(1);
+            }
+            if (bg == 0) {
+                (void)sys_setpgid(0, me + 1);
+
+                (void)sys_sigaction(SIGTTIN, ttin_handler, 0);
+                (void)sys_sigaction(SIGTTOU, ttou_handler, 0);
+
+                uint8_t b = 0;
+                (void)sys_read(tfd, &b, 1);
+                if (!got_ttin) {
+                    sys_write(1, "[init] SIGTTIN job control failed\n",
+                              (uint32_t)(sizeof("[init] SIGTTIN job control failed\n") - 1));
+                    sys_exit(1);
+                }
+
+                const char msg2[] = "x";
+                (void)sys_write(tfd, msg2, 1);
+                if (!got_ttou) {
+                    sys_write(1, "[init] SIGTTOU job control failed\n",
+                              (uint32_t)(sizeof("[init] SIGTTOU job control failed\n") - 1));
+                    sys_exit(1);
+                }
+
+                sys_exit(0);
+            }
+
+            int st2 = 0;
+            int wp2 = sys_waitpid(bg, &st2, 0);
+            if (wp2 != bg || st2 != 0) {
+                sys_write(1, "[init] waitpid(job control bg) failed wp=", (uint32_t)(sizeof("[init] waitpid(job control bg) failed wp=") - 1));
+                write_int_dec(wp2);
+                sys_write(1, " st=", (uint32_t)(sizeof(" st=") - 1));
+                write_int_dec(st2);
+                sys_write(1, "\n", 1);
+                sys_exit(1);
+            }
+
+            (void)sys_close(tfd);
+            sys_exit(0);
+        }
+
+        int stL = 0;
+        int wpL = sys_waitpid(leader, &stL, 0);
+        if (wpL != leader || stL != 0) {
+            sys_write(1, "[init] waitpid(job control leader) failed wp=", (uint32_t)(sizeof("[init] waitpid(job control leader) failed wp=") - 1));
+            write_int_dec(wpL);
+            sys_write(1, " st=", (uint32_t)(sizeof(" st=") - 1));
+            write_int_dec(stL);
+            sys_write(1, "\n", 1);
+            sys_exit(1);
+        }
+
+        sys_write(1, "[init] job control (SIGTTIN/SIGTTOU) OK\n",
+                  (uint32_t)(sizeof("[init] job control (SIGTTIN/SIGTTOU) OK\n") - 1));
+    }
+
     {
         int fd = sys_open("/dev/null", 0);
         if (fd < 0) {
@@ -810,6 +995,34 @@ void _start(void) {
         sys_write(1, "[init] setsid/setpgid/getpgrp OK\n",
                   (uint32_t)(sizeof("[init] setsid/setpgid/getpgrp OK\n") - 1));
     }
+
+    {
+        uintptr_t oldh = 0;
+        if (sys_sigaction(SIGUSR1, usr1_handler, &oldh) < 0) {
+            sys_write(1, "[init] sigaction failed\n",
+                      (uint32_t)(sizeof("[init] sigaction failed\n") - 1));
+            sys_exit(1);
+        }
+
+        int me = sys_getpid();
+        if (sys_kill(me, SIGUSR1) < 0) {
+            sys_write(1, "[init] kill(SIGUSR1) failed\n",
+                      (uint32_t)(sizeof("[init] kill(SIGUSR1) failed\n") - 1));
+            sys_exit(1);
+        }
+
+        for (uint32_t i = 0; i < 2000000U; i++) {
+            if (got_usr1) break;
+        }
+
+        if (!got_usr1) {
+            sys_write(1, "[init] SIGUSR1 not delivered\n",
+                      (uint32_t)(sizeof("[init] SIGUSR1 not delivered\n") - 1));
+            sys_exit(1);
+        }
+
+        sys_write(1, "[init] sigaction/kill(SIGUSR1) OK\n",
+                  (uint32_t)(sizeof("[init] sigaction/kill(SIGUSR1) OK\n") - 1));
     }
 
     fd = sys_open("/tmp/hello.txt", 0);
@@ -965,8 +1178,7 @@ void _start(void) {
             sys_exit(1);
         }
         static const char z[] = "discard me";
-        int wr = sys_write(fd, z, (uint32_t)(sizeof(z) - 1));
-        if (wr != (int)(sizeof(z) - 1)) {
+        if (sys_write(fd, z, (uint32_t)(sizeof(z) - 1)) != (int)(sizeof(z) - 1)) {
             sys_write(1, "[init] /dev/null write failed\n",
                       (uint32_t)(sizeof("[init] /dev/null write failed\n") - 1));
             sys_exit(1);