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#include "modules/system/vfs/VFS.h"
16
17namespace {
18Mutex terminalPolicy;
19
20class TerminalBinding final : public Process::ControllingTerminal {
21 public:
22 TerminalBinding(ConsoleFile* console, const SharedPointer<ConsoleControlState>& control,
23 VFS::FilesystemPin&& storage)
24 : m_Console(console), m_Control(control), m_Storage(pedigree_std::move(storage)) {}
25 ~TerminalBinding() override {
26 m_Console->releaseVfsReference();
27 }
28 File* file() const override {
29 return static_cast<TerminalControl*>(m_Control.get())->active() ? m_Console : nullptr;
30 }
31
32 private:
33 ConsoleFile* m_Console;
35 VFS::FilesystemPin m_Storage;
36};
37
38PosixProcess* currentProcess() {
39 Process* process = Processor::information().getCurrentThread()->getParent();
40 return process->getType() == Process::Posix ? static_cast<PosixProcess*>(process) : nullptr;
41}
42
43bool currentTerminal(PosixProcess& process, ConsoleFile& console) {
44 auto context = process.acquireCttyContext();
45 return context && context->file() == &console;
46}
47} // namespace
48
49TerminalControl::TerminalControl(size_t session, size_t foreground)
50 : m_Active(true), m_Session(session), m_Foreground(foreground) {}
51
52bool TerminalControl::active() const {
53 return __atomic_load_n(&m_Active, __ATOMIC_ACQUIRE);
54}
55
56void TerminalControl::controlCharacter(Character character) {
57 if (!active())
58 return;
59 const size_t foreground = __atomic_load_n(&m_Foreground, __ATOMIC_ACQUIRE);
60 const int signal = character == Character::Interrupt ? SIGINT
61 : character == Character::Quit ? SIGQUIT
62 : SIGTSTP;
63 for (size_t i = 0; i < Scheduler::instance().getNumProcesses(); ++i) {
65 if (!Scheduler::instance().acquireProcess(recipient, i) ||
66 recipient->getType() != Process::Posix)
67 continue;
68 auto* process = static_cast<PosixProcess*>(recipient.get());
69 {
70 RecursingLockGuard<Spinlock> guard(ProcessGroupManager::instance().lock());
71 size_t group = 0;
72 if (process->getSessionId() != m_Session || !process->getProcessGroupId(group) ||
73 group != foreground)
74 continue;
75 }
77 auto* subsystem = static_cast<PosixSubsystem*>(process->getSubsystem());
78 if (subsystem && process->acquireProcessSignalThread(thread) && active())
79 subsystem->sendSignal(thread.get(), signal, false, true);
80 }
81}
82
83void TerminalControl::invalidate() {
84 __atomic_store_n(&m_Active, false, __ATOMIC_RELEASE);
85}
86
87Mutex& TerminalControl::lock() {
88 return terminalPolicy;
89}
90
91int TerminalControl::attach(ConsoleFile& console, bool steal, bool automatic,
92 const SharedPointer<ConsoleIoState>& opened) {
93 TerminationDeferral deferral;
94 // Opening can wait for an earlier revocation, so it precedes policy admission.
95 auto epoch = opened ? opened : console.captureOpenEpoch();
96 if (!epoch) {
97 SYSCALL_ERROR(OutOfMemory);
98 return -1;
99 }
100 LockGuard<Mutex> guard(terminalPolicy);
101 if (epoch->revoked()) {
102 SYSCALL_ERROR(IoError);
103 return -1;
104 }
105 PosixProcess* process = currentProcess();
106 if (!process || console.isMaster()) {
107 SYSCALL_ERROR(NotAConsole);
108 return automatic ? 0 : -1;
109 }
110 auto context = process->acquireCttyContext();
111 File* existing = context ? context->file() : nullptr;
112 const size_t session = process->getSessionId();
113 if (session != process->getUserspaceId(Process::rootPidNamespace().get()) ||
114 (existing && existing != &console)) {
115 if (automatic)
116 return 0;
117 NOTICE("TIOCSCTTY EPERM - session doesn't match");
118 SYSCALL_ERROR(NotEnoughPermissions);
119 return -1;
120 }
121 auto previous = console.controlState();
122 auto* old = static_cast<TerminalControl*>(previous.get());
123 if (old && old->active() && old->m_Session == session && existing == &console)
124 return 0;
125 if (old && old->active() && old->m_Session != session &&
126 (!steal || process->getEffectiveUserId() != 0)) {
127 if (automatic)
128 return 0;
129 NOTICE("TIOCSCTTY EPERM - you are not root and a previous session is active");
130 SYSCALL_ERROR(NotEnoughPermissions);
131 return -1;
132 }
133 size_t group = 0;
134 if (!process->getProcessGroupId(group)) {
135 SYSCALL_ERROR(NoSuchProcess);
136 return -1;
137 }
138 auto control = SharedPointer<ConsoleControlState>::tryAdopt(new TerminalControl(session, group));
139 VFS::FilesystemPin storage;
140 if (!control || !VFS::instance().pinFilesystem(console.getFilesystem(), storage) ||
141 !console.retainVfsReference()) {
142 SYSCALL_ERROR(OutOfMemory);
143 return -1;
144 }
145 auto* rawBinding = new TerminalBinding(&console, control, pedigree_std::move(storage));
146 if (!rawBinding) {
147 console.releaseVfsReference();
148 SYSCALL_ERROR(OutOfMemory);
149 return -1;
150 }
152 if (!binding) {
153 SYSCALL_ERROR(OutOfMemory);
154 return -1;
155 }
156 if (old)
157 old->invalidate();
158 console.setControlState(control);
159 process->setCttyContext(binding);
160 return 0;
161}
162
163int TerminalControl::setForeground(ConsoleFile& console, int group,
164 const SharedPointer<ConsoleIoState>& opened) {
165 TerminationDeferral deferral;
166 LockGuard<Mutex> guard(terminalPolicy);
167 if (opened && opened->revoked()) {
168 SYSCALL_ERROR(NotAConsole);
169 return -1;
170 }
171 PosixProcess* process = currentProcess();
172 auto slot = console.controlState();
173 auto* control = static_cast<TerminalControl*>(slot.get());
174 if (!process || !control || !control->active() || !currentTerminal(*process, console) ||
175 control->m_Session != process->getSessionId()) {
176 SYSCALL_ERROR(NotAConsole);
177 return -1;
178 }
179 if (group < 0) {
180 SYSCALL_ERROR(InvalidArgument);
181 return -1;
182 }
183 {
184 RecursingLockGuard<Spinlock> groupGuard(ProcessGroupManager::instance().lock());
185 ProcessGroup* target =
186 ProcessGroupManager::instance().findGroup(group, process->pidNamespace().get());
187 if (!target || target->sessionId != control->m_Session) {
188 SYSCALL_ERROR(NotEnoughPermissions);
189 return -1;
190 }
191 group = target->processGroupId;
192 }
193 __atomic_store_n(&control->m_Foreground, static_cast<size_t>(group), __ATOMIC_RELEASE);
194 return 0;
195}
196
197int TerminalControl::foreground(ConsoleFile& console, const SharedPointer<ConsoleIoState>& opened) {
198 LockGuard<Mutex> guard(terminalPolicy);
199 if (opened && opened->revoked()) {
200 SYSCALL_ERROR(IoError);
201 return -1;
202 }
203 PosixProcess* process = currentProcess();
204 auto slot = console.controlState();
205 auto* control = static_cast<TerminalControl*>(slot.get());
206 if (!process || !control || !control->active() || !currentTerminal(*process, console) ||
207 control->m_Session != process->getSessionId()) {
208 SYSCALL_ERROR(NotAConsole);
209 return -1;
210 }
211 RecursingLockGuard<Spinlock> groupGuard(ProcessGroupManager::instance().lock());
212 auto* group = ProcessGroupManager::instance().findGroup(
213 __atomic_load_n(&control->m_Foreground, __ATOMIC_ACQUIRE));
214 return group && group->identity ? group->identity->id(process->pidNamespace().get()) : 0;
215}
216
217int TerminalControl::session(ConsoleFile& console, const SharedPointer<ConsoleIoState>& opened) {
218 LockGuard<Mutex> guard(terminalPolicy);
219 if (opened && opened->revoked()) {
220 SYSCALL_ERROR(IoError);
221 return -1;
222 }
223 auto slot = console.controlState();
224 auto* control = static_cast<TerminalControl*>(slot.get());
225 if (!control || !control->active()) {
226 SYSCALL_ERROR(NotAConsole);
227 return -1;
228 }
229 // A PTY master can query its slave's session without controlling that terminal.
230 if (!console.isMaster()) {
231 PosixProcess* process = currentProcess();
232 if (!process || !currentTerminal(*process, console) ||
233 control->m_Session != process->getSessionId()) {
234 SYSCALL_ERROR(NotAConsole);
235 return -1;
236 }
237 }
238 auto* process = currentProcess();
240 if (!process || !Scheduler::instance().acquireProcessByUserspaceId(
241 leader, control->m_Session, Process::rootPidNamespace().get())) {
242 return 0;
243 }
244 return leader->getUserspaceId(process->pidNamespace().get());
245}
246
247int TerminalControl::hangup() {
248 TerminationDeferral deferral;
249 Process* caller = Processor::information().getCurrentThread()->getParent();
250 if (caller->getEffectiveUserId() != 0) {
251 SYSCALL_ERROR(NotEnoughPermissions);
252 return -1;
253 }
256 SharedPointer<ConsoleIoState> retired, replacement;
257 ConsoleFile* console = nullptr;
258 {
259 LockGuard<Mutex> guard(terminalPolicy);
260 context = caller->acquireCttyContext();
261 File* file = context ? context->file() : nullptr;
262 if (!file)
263 return 0;
264 if (!ConsoleManager::instance().isConsole(file)) {
265 SYSCALL_ERROR(NotAConsole);
266 return -1;
267 }
268 console = static_cast<ConsoleFile*>(file);
270 if (!replacement) {
271 SYSCALL_ERROR(OutOfMemory);
272 return -1;
273 }
274 auto slot = console->controlState();
275 auto* control = static_cast<TerminalControl*>(slot.get());
276 if (!console->beginRevocation(retired)) {
277 SYSCALL_ERROR(DeviceBusy);
278 return -1;
279 }
280 if (control && control->active()) {
281 if (Scheduler::instance().acquireProcessByUserspaceId(leader, control->m_Session,
282 Process::rootPidNamespace().get()) &&
283 (leader->getType() != Process::Posix ||
284 static_cast<PosixProcess*>(leader.get())->getSessionId() != control->m_Session)) {
285 leader.reset();
286 }
287 control->invalidate();
288 }
289 console->setControlState(SharedPointer<ConsoleControlState>());
290 caller->setCttyContext(SharedPointer<Process::ControllingTerminal>());
291 }
292 console->finishRevocation(retired, replacement);
293 if (leader) {
295 auto* subsystem = static_cast<PosixSubsystem*>(leader->getSubsystem());
296 if (subsystem && leader->acquireProcessSignalThread(thread)) {
297 subsystem->sendSignal(thread.get(), SIGHUP, false, true);
298 subsystem->sendSignal(thread.get(), SIGCONT, false, true);
299 }
300 }
301 return 0;
302}
303
304void TerminalControl::processTerminated(PosixProcess& process) {
305 TerminationDeferral deferral;
306 size_t session = 0, foreground = 0;
309 ConsoleFile* revoking = nullptr;
310 {
311 LockGuard<Mutex> guard(terminalPolicy);
312 context = process.acquireCttyContext();
313 File* file = context ? context->file() : nullptr;
314 if (file && ConsoleManager::instance().isConsole(file)) {
315 auto* console = static_cast<ConsoleFile*>(file);
316 auto slot = console->controlState();
317 auto* control = static_cast<TerminalControl*>(slot.get());
318 // A PTY master must be able to drain output after its session leader exits.
319 const bool preservePtyData = console->isPtySlave();
320 if (control && control->active() &&
321 control->m_Session == process.getUserspaceId(Process::rootPidNamespace().get()) &&
322 (preservePtyData || console->beginRevocation(retired))) {
323 session = control->m_Session;
324 foreground = __atomic_load_n(&control->m_Foreground, __ATOMIC_ACQUIRE);
325 control->invalidate();
326 console->setControlState(SharedPointer<ConsoleControlState>());
327 if (!preservePtyData)
328 revoking = console;
329 }
330 }
331 process.setCttyContext(SharedPointer<Process::ControllingTerminal>());
332 }
333 // Exit cannot fail allocation. A later open prepares its own fresh state.
334 if (revoking)
335 revoking->finishRevocation(retired, SharedPointer<ConsoleIoState>());
336 if (!session || !foreground)
337 return;
338 for (size_t i = 0; i < Scheduler::instance().getNumProcesses(); ++i) {
339 Scheduler::ProcessLease recipient;
340 if (!Scheduler::instance().acquireProcess(recipient, i) ||
341 recipient->getType() != Process::Posix || recipient.get() == &process)
342 continue;
343 auto* target = static_cast<PosixProcess*>(recipient.get());
344 size_t group = 0;
345 if (target->getSessionId() != session || !target->getProcessGroupId(group) ||
346 group != foreground)
347 continue;
349 auto* subsystem = static_cast<PosixSubsystem*>(target->getSubsystem());
350 if (subsystem && target->acquireProcessSignalThread(thread)) {
351 subsystem->sendSignal(thread.get(), SIGHUP, false, true);
352 subsystem->sendSignal(thread.get(), SIGCONT, false, true);
353 }
354 }
355}
Definition File.h:75
virtual bool retainVfsReference()
Definition File.cc:910
virtual void releaseVfsReference()
Definition File.cc:914
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:504
MUST_USE_RESULT bool acquireProcessSignalThread(ThreadLease &lease)
Definition Process.cc:1407
Process * getParent()
Definition Process.h:620
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...)
T * get() const
static VFS & instance()
Definition VFS.cc:311