The Pedigree Project 0.1
TerminalControl.cc
1/* Copyright (c) 2026, Pedigree Developers. */
2#include "TerminalControl.h"
3#include "pedigree/kernel/LockGuard.h"
4#include "pedigree/kernel/process/Process.h"
5#include "pedigree/kernel/process/Scheduler.h"
6#include "pedigree/kernel/process/TerminationDeferral.h"
7#include "pedigree/kernel/processor/Processor.h"
8#include "pedigree/kernel/processor/ProcessorInformation.h"
9#include "pedigree/kernel/syscallError.h"
10
11#include <signal.h>
12
13#include "PosixProcess.h"
14#include "PosixSubsystem.h"
15
16namespace {
17Mutex terminalPolicy;
18
19class TerminalBinding final : public Process::ControllingTerminal {
20 public:
21 TerminalBinding(ConsoleFile* console, const SharedPointer<ConsoleControlState>& control)
22 : m_Console(console), m_Control(control) {}
23 ~TerminalBinding() override {
24 m_Console->releaseVfsReference();
25 }
26 File* file() const override {
27 return static_cast<TerminalControl*>(m_Control.get())->active() ? m_Console : nullptr;
28 }
29
30 private:
31 ConsoleFile* m_Console;
33};
34
35PosixProcess* currentProcess() {
36 Process* process = Processor::information().getCurrentThread()->getParent();
37 return process->getType() == Process::Posix ? static_cast<PosixProcess*>(process) : nullptr;
38}
39
40bool currentTerminal(PosixProcess& process, ConsoleFile& console) {
41 auto context = process.acquireCttyContext();
42 return context && context->file() == &console;
43}
44} // namespace
45
46TerminalControl::TerminalControl(size_t session, size_t foreground)
47 : m_Active(true), m_Session(session), m_Foreground(foreground) {}
48
49bool TerminalControl::active() const {
50 return __atomic_load_n(&m_Active, __ATOMIC_ACQUIRE);
51}
52
53void TerminalControl::controlCharacter(Character character) {
54 if (!active())
55 return;
56 const size_t foreground = __atomic_load_n(&m_Foreground, __ATOMIC_ACQUIRE);
57 const int signal = character == Character::Interrupt ? SIGINT
58 : character == Character::Quit ? SIGQUIT
59 : SIGTSTP;
60 for (size_t i = 0; i < Scheduler::instance().getNumProcesses(); ++i) {
62 if (!Scheduler::instance().acquireProcess(recipient, i) ||
63 recipient->getType() != Process::Posix)
64 continue;
65 auto* process = static_cast<PosixProcess*>(recipient.get());
66 {
67 RecursingLockGuard<Spinlock> guard(ProcessGroupManager::instance().lock());
68 size_t group = 0;
69 if (process->getSessionId() != m_Session || !process->getProcessGroupId(group) ||
70 group != foreground)
71 continue;
72 }
74 auto* subsystem = static_cast<PosixSubsystem*>(process->getSubsystem());
75 if (subsystem && process->acquireProcessSignalThread(thread) && active())
76 subsystem->sendSignal(thread.get(), signal, false, true);
77 }
78}
79
80void TerminalControl::invalidate() {
81 __atomic_store_n(&m_Active, false, __ATOMIC_RELEASE);
82}
83
84Mutex& TerminalControl::lock() {
85 return terminalPolicy;
86}
87
88int TerminalControl::attach(ConsoleFile& console, bool steal, bool automatic,
89 const SharedPointer<ConsoleIoState>& opened) {
90 TerminationDeferral deferral;
91 // Opening can wait for an earlier revocation, so it precedes policy admission.
92 auto epoch = opened ? opened : console.captureOpenEpoch();
93 if (!epoch) {
94 SYSCALL_ERROR(OutOfMemory);
95 return -1;
96 }
97 LockGuard<Mutex> guard(terminalPolicy);
98 if (epoch->revoked()) {
99 SYSCALL_ERROR(IoError);
100 return -1;
101 }
102 PosixProcess* process = currentProcess();
103 if (!process || console.isMaster()) {
104 SYSCALL_ERROR(NotAConsole);
105 return automatic ? 0 : -1;
106 }
107 auto context = process->acquireCttyContext();
108 File* existing = context ? context->file() : nullptr;
109 const size_t session = process->getSessionId();
110 if (session != process->getUserspaceId() || (existing && existing != &console)) {
111 if (automatic)
112 return 0;
113 NOTICE("TIOCSCTTY EPERM - session doesn't match");
114 SYSCALL_ERROR(NotEnoughPermissions);
115 return -1;
116 }
117 auto previous = console.controlState();
118 auto* old = static_cast<TerminalControl*>(previous.get());
119 if (old && old->active() && old->m_Session == session && existing == &console)
120 return 0;
121 if (old && old->active() && old->m_Session != session &&
122 (!steal || process->getEffectiveUserId() != 0)) {
123 if (automatic)
124 return 0;
125 NOTICE("TIOCSCTTY EPERM - you are not root and a previous session is active");
126 SYSCALL_ERROR(NotEnoughPermissions);
127 return -1;
128 }
129 size_t group = 0;
130 if (!process->getProcessGroupId(group)) {
131 SYSCALL_ERROR(NoSuchProcess);
132 return -1;
133 }
134 auto control = SharedPointer<ConsoleControlState>::tryAdopt(new TerminalControl(session, group));
135 if (!control || !console.retainVfsReference()) {
136 SYSCALL_ERROR(OutOfMemory);
137 return -1;
138 }
139 auto* rawBinding = new TerminalBinding(&console, control);
140 if (!rawBinding) {
141 console.releaseVfsReference();
142 SYSCALL_ERROR(OutOfMemory);
143 return -1;
144 }
146 if (!binding) {
147 SYSCALL_ERROR(OutOfMemory);
148 return -1;
149 }
150 if (old)
151 old->invalidate();
152 console.setControlState(control);
153 process->setCttyContext(binding);
154 return 0;
155}
156
157int TerminalControl::setForeground(ConsoleFile& console, int group,
158 const SharedPointer<ConsoleIoState>& opened) {
159 TerminationDeferral deferral;
160 LockGuard<Mutex> guard(terminalPolicy);
161 if (opened && opened->revoked()) {
162 SYSCALL_ERROR(NotAConsole);
163 return -1;
164 }
165 PosixProcess* process = currentProcess();
166 auto slot = console.controlState();
167 auto* control = static_cast<TerminalControl*>(slot.get());
168 if (!process || !control || !control->active() || !currentTerminal(*process, console) ||
169 control->m_Session != process->getSessionId()) {
170 SYSCALL_ERROR(NotAConsole);
171 return -1;
172 }
173 if (group < 0) {
174 SYSCALL_ERROR(InvalidArgument);
175 return -1;
176 }
177 {
178 RecursingLockGuard<Spinlock> groupGuard(ProcessGroupManager::instance().lock());
179 ProcessGroup* target = ProcessGroupManager::instance().findGroup(group);
180 if (!target || target->sessionId != control->m_Session) {
181 SYSCALL_ERROR(NotEnoughPermissions);
182 return -1;
183 }
184 }
185 __atomic_store_n(&control->m_Foreground, static_cast<size_t>(group), __ATOMIC_RELEASE);
186 return 0;
187}
188
189int TerminalControl::foreground(ConsoleFile& console, const SharedPointer<ConsoleIoState>& opened) {
190 LockGuard<Mutex> guard(terminalPolicy);
191 if (opened && opened->revoked()) {
192 SYSCALL_ERROR(IoError);
193 return -1;
194 }
195 PosixProcess* process = currentProcess();
196 auto slot = console.controlState();
197 auto* control = static_cast<TerminalControl*>(slot.get());
198 if (!process || !control || !control->active() || !currentTerminal(*process, console) ||
199 control->m_Session != process->getSessionId()) {
200 SYSCALL_ERROR(NotAConsole);
201 return -1;
202 }
203 return static_cast<int>(__atomic_load_n(&control->m_Foreground, __ATOMIC_ACQUIRE));
204}
205
206int TerminalControl::hangup() {
207 TerminationDeferral deferral;
208 Process* caller = Processor::information().getCurrentThread()->getParent();
209 if (caller->getEffectiveUserId() != 0) {
210 SYSCALL_ERROR(NotEnoughPermissions);
211 return -1;
212 }
215 SharedPointer<ConsoleIoState> retired, replacement;
216 ConsoleFile* console = nullptr;
217 {
218 LockGuard<Mutex> guard(terminalPolicy);
219 context = caller->acquireCttyContext();
220 File* file = context ? context->file() : nullptr;
221 if (!file)
222 return 0;
223 if (!ConsoleManager::instance().isConsole(file)) {
224 SYSCALL_ERROR(NotAConsole);
225 return -1;
226 }
227 console = static_cast<ConsoleFile*>(file);
229 if (!replacement) {
230 SYSCALL_ERROR(OutOfMemory);
231 return -1;
232 }
233 auto slot = console->controlState();
234 auto* control = static_cast<TerminalControl*>(slot.get());
235 if (!console->beginRevocation(retired)) {
236 SYSCALL_ERROR(DeviceBusy);
237 return -1;
238 }
239 if (control && control->active()) {
240 if (Scheduler::instance().acquireProcessByUserspaceId(leader, control->m_Session) &&
241 (leader->getType() != Process::Posix ||
242 static_cast<PosixProcess*>(leader.get())->getSessionId() != control->m_Session))
243 leader.reset();
244 control->invalidate();
245 }
246 console->setControlState(SharedPointer<ConsoleControlState>());
247 caller->setCttyContext(SharedPointer<Process::ControllingTerminal>());
248 }
249 console->finishRevocation(retired, replacement);
250 if (leader) {
252 auto* subsystem = static_cast<PosixSubsystem*>(leader->getSubsystem());
253 if (subsystem && leader->acquireProcessSignalThread(thread)) {
254 subsystem->sendSignal(thread.get(), SIGHUP, false, true);
255 subsystem->sendSignal(thread.get(), SIGCONT, false, true);
256 }
257 }
258 return 0;
259}
260
261void TerminalControl::processTerminated(PosixProcess& process) {
262 TerminationDeferral deferral;
263 size_t session = 0, foreground = 0;
266 ConsoleFile* revoking = nullptr;
267 {
268 LockGuard<Mutex> guard(terminalPolicy);
269 context = process.acquireCttyContext();
270 File* file = context ? context->file() : nullptr;
271 if (file && ConsoleManager::instance().isConsole(file)) {
272 auto* console = static_cast<ConsoleFile*>(file);
273 auto slot = console->controlState();
274 auto* control = static_cast<TerminalControl*>(slot.get());
275 // A PTY master must be able to drain output after its session leader exits.
276 const bool preservePtyData = console->isPtySlave();
277 if (control && control->active() && control->m_Session == process.getUserspaceId() &&
278 (preservePtyData || console->beginRevocation(retired))) {
279 session = control->m_Session;
280 foreground = __atomic_load_n(&control->m_Foreground, __ATOMIC_ACQUIRE);
281 control->invalidate();
282 console->setControlState(SharedPointer<ConsoleControlState>());
283 if (!preservePtyData)
284 revoking = console;
285 }
286 }
287 process.setCttyContext(SharedPointer<Process::ControllingTerminal>());
288 }
289 // Exit cannot fail allocation. A later open prepares its own fresh state.
290 if (revoking)
291 revoking->finishRevocation(retired, SharedPointer<ConsoleIoState>());
292 if (!session || !foreground)
293 return;
294 for (size_t i = 0; i < Scheduler::instance().getNumProcesses(); ++i) {
295 Scheduler::ProcessLease recipient;
296 if (!Scheduler::instance().acquireProcess(recipient, i) ||
297 recipient->getType() != Process::Posix || recipient.get() == &process)
298 continue;
299 auto* target = static_cast<PosixProcess*>(recipient.get());
300 size_t group = 0;
301 if (target->getSessionId() != session || !target->getProcessGroupId(group) ||
302 group != foreground)
303 continue;
305 auto* subsystem = static_cast<PosixSubsystem*>(target->getSubsystem());
306 if (subsystem && target->acquireProcessSignalThread(thread)) {
307 subsystem->sendSignal(thread.get(), SIGHUP, false, true);
308 subsystem->sendSignal(thread.get(), SIGCONT, false, true);
309 }
310 }
311}
Definition File.h:74
virtual bool retainVfsReference()
Definition File.cc:842
virtual void releaseVfsReference()
Definition File.cc:846
Definition Mutex.h:56
virtual void sendSignal(Thread *pThread, int signal, bool yield=true, bool processDirected=false)
ProcessGroup * findGroup(size_t gid) const
size_t getUserspaceId() const
Definition Process.h:467
MUST_USE_RESULT bool acquireProcessSignalThread(ThreadLease &lease)
Definition Process.cc:1303
Process * getParent()
Definition Process.h:567
static ProcessorInformation & information()
static Scheduler & instance()
Definition Scheduler.h:96
size_t getNumProcesses()
Definition Scheduler.cc:268
MUST_USE_RESULT bool acquireProcess(ProcessLease &lease, size_t n)
Definition Scheduler.cc:275
static SharedPointer< T > tryAdopt(T *ptr)
static SharedPointer< T > tryAllocate(Args...)