KDESu

ptyprocess.cpp
1/*
2 This file is part of the KDE project, module kdesu.
3 SPDX-FileCopyrightText: 1999, 2000 Geert Jansen <jansen@kde.org>
4
5 This file contains code from TEShell.C of the KDE konsole.
6 SPDX-FileCopyrightText: 1997, 1998 Lars Doelle <lars.doelle@on-line.de>
7
8 SPDX-License-Identifier: GPL-2.0-only
9
10 process.cpp: Functionality to build a front end to password asking terminal programs.
11*/
12
13#include "ptyprocess.h"
14#include "kcookie_p.h"
15#include "ptyprocess_p.h"
16
17#include <config-kdesu.h>
18#include <ksu_debug.h>
19
20#include <cerrno>
21#include <fcntl.h>
22#include <signal.h>
23#include <stdlib.h>
24#include <termios.h>
25#include <unistd.h>
26
27#include <sys/resource.h>
28#include <sys/stat.h>
29#include <sys/time.h>
30#include <sys/wait.h>
31
32#if HAVE_SYS_SELECT_H
33#include <sys/select.h> // Needed on some systems.
34#endif
35
36#include <QFile>
37#include <QStandardPaths>
38
39#include <KConfigGroup>
40#include <KSharedConfig>
41
42extern int kdesuDebugArea();
43
44namespace KDESu
45{
46using namespace KDESuPrivate;
47
48/*
49** Wait for @p ms milliseconds
50** @param fd file descriptor
51** @param ms time to wait in milliseconds
52** @return
53*/
54int PtyProcess::waitMS(int fd, int ms)
55{
56 struct timeval tv;
57 tv.tv_sec = 0;
58 tv.tv_usec = 1000 * ms;
59
60 fd_set fds;
61 FD_ZERO(&fds);
62 FD_SET(fd, &fds);
63 return select(fd + 1, &fds, nullptr, nullptr, &tv);
64}
65
66// XXX this function is nonsense:
67// - for our child, we could use waitpid().
68// - the configurability at this place it *complete* braindamage
69/*
70** Basic check for the existence of @p pid.
71** Returns true iff @p pid is an extant process.
72*/
73bool PtyProcess::checkPid(pid_t pid)
74{
75 KSharedConfig::Ptr config = KSharedConfig::openConfig();
76 KConfigGroup cg(config, QStringLiteral("super-user-command"));
77 QString superUserCommand = cg.readEntry("super-user-command", "sudo");
78 // sudo does not accept signals from user so we except it
79 if (superUserCommand == QLatin1String("sudo")) {
80 return true;
81 } else {
82 return kill(pid, 0) == 0;
83 }
84}
85
86/*
87** Check process exit status for process @p pid.
88** On error (no child, no exit), return Error (-1).
89** If child @p pid has exited, return its exit status,
90** (which may be zero).
91** If child @p has not exited, return NotExited (-2).
92*/
94{
95 int state;
96 int ret;
97 ret = waitpid(pid, &state, WNOHANG);
98
99 if (ret < 0) {
100 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
101 << "waitpid():" << strerror(errno);
102 return Error;
103 }
104 if (ret == pid) {
105 if (WIFEXITED(state)) {
106 return WEXITSTATUS(state);
107 }
108
109 return Killed;
110 }
111
112 return NotExited;
113}
114
115PtyProcess::PtyProcess()
116 : PtyProcess(*new PtyProcessPrivate)
117{
118}
119
120PtyProcess::PtyProcess(PtyProcessPrivate &dd)
121 : d_ptr(&dd)
122{
123 m_terminal = false;
124 m_erase = false;
125}
126
127PtyProcess::~PtyProcess() = default;
128
129int PtyProcess::init()
130{
131 Q_D(PtyProcess);
132
133 delete d->pty;
134 d->pty = new KPty();
135 if (!d->pty->open()) {
136 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
137 << "Failed to open PTY.";
138 return -1;
139 }
140 if (!d->wantLocalEcho) {
141 enableLocalEcho(false);
142 }
143 d->inputBuffer.resize(0);
144 return 0;
145}
146
147/** Set additional environment variables. */
149{
151
152 d->env = env;
153}
154
155int PtyProcess::fd() const
156{
157 Q_D(const PtyProcess);
158
159 return d->pty ? d->pty->masterFd() : -1;
160}
161
163{
164 return m_pid;
165}
166
167/** Returns the additional environment variables set by setEnvironment() */
169{
170 Q_D(const PtyProcess);
171
172 return d->env;
173}
174
176{
178
179 QByteArray ret;
180 if (!d->inputBuffer.isEmpty()) {
181 // if there is still something in the buffer, we need not block.
182 // we should still try to read any further output, from the fd, though.
183 block = false;
184 ret = d->inputBuffer;
185 d->inputBuffer.resize(0);
186 }
187
188 int flags = fcntl(fd(), F_GETFL);
189 if (flags < 0) {
190 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
191 << "fcntl(F_GETFL):" << strerror(errno);
192 return ret;
193 }
194 int oflags = flags;
195 if (block) {
196 flags &= ~O_NONBLOCK;
197 } else {
198 flags |= O_NONBLOCK;
199 }
200
201 if ((flags != oflags) && (fcntl(fd(), F_SETFL, flags) < 0)) {
202 // We get an error here when the child process has closed
203 // the file descriptor already.
204 return ret;
205 }
206
207 while (1) {
208 ret.reserve(ret.size() + 0x8000);
209 int nbytes = read(fd(), ret.data() + ret.size(), 0x8000);
210 if (nbytes == -1) {
211 if (errno == EINTR) {
212 continue;
213 } else {
214 break;
215 }
216 }
217 if (nbytes == 0) {
218 break; // nothing available / eof
219 }
220
221 ret.resize(ret.size() + nbytes);
222 break;
223 }
224
225 return ret;
226}
227
229{
231
232 d->inputBuffer = readAll(block);
233
234 int pos;
235 QByteArray ret;
236 if (!d->inputBuffer.isEmpty()) {
237 pos = d->inputBuffer.indexOf('\n');
238 if (pos == -1) {
239 // NOTE: this means we return something even if there in no full line!
240 ret = d->inputBuffer;
241 d->inputBuffer.resize(0);
242 } else {
243 ret = d->inputBuffer.left(pos);
244 d->inputBuffer.remove(0, pos + 1);
245 }
246 }
247
248 return ret;
249}
250
251void PtyProcess::writeLine(const QByteArray &line, bool addnl)
252{
253 if (!line.isEmpty()) {
254 write(fd(), line.constData(), line.length());
255 }
256 if (addnl) {
257 write(fd(), "\n", 1);
258 }
259}
260
261void PtyProcess::unreadLine(const QByteArray &line, bool addnl)
262{
264
265 QByteArray tmp = line;
266 if (addnl) {
267 tmp += '\n';
268 }
269 if (!tmp.isEmpty()) {
270 d->inputBuffer.prepend(tmp);
271 }
272}
273
275{
276 m_exitString = exit;
277}
278
279/*
280 * Fork and execute the command. This returns in the parent.
281 */
282int PtyProcess::exec(const QByteArray &command, const QList<QByteArray> &args)
283{
285
286 int i;
287
288 if (init() < 0) {
289 return -1;
290 }
291
292 if ((m_pid = fork()) == -1) {
293 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
294 << "fork():" << strerror(errno);
295 return -1;
296 }
297
298 // Parent
299 if (m_pid) {
300 d->pty->closeSlave();
301 return 0;
302 }
303
304 // Child
305 if (setupTTY() < 0) {
306 _exit(1);
307 }
308
309 for (const QByteArray &var : std::as_const(d->env)) {
310 putenv(const_cast<char *>(var.constData()));
311 }
312 unsetenv("KDE_FULL_SESSION");
313 // for : Qt: Session management error
314 unsetenv("SESSION_MANAGER");
315 // QMutex::lock , deadlocks without that.
316 // <thiago> you cannot connect to the user's session bus from another UID
317 unsetenv("DBUS_SESSION_BUS_ADDRESS");
318
319 // set temporarily LC_ALL to C, for su (to be able to parse "Password:")
320 const QByteArray old_lc_all = qgetenv("LC_ALL");
321 if (!old_lc_all.isEmpty()) {
322 qputenv("KDESU_LC_ALL", old_lc_all);
323 } else {
324 unsetenv("KDESU_LC_ALL");
325 }
326 qputenv("LC_ALL", "C");
327
328 // From now on, terminal output goes through the tty.
329
330 QByteArray path;
331 if (command.contains('/')) {
332 path = command;
333 } else {
335 if (file.isEmpty()) {
336 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " << command << "not found.";
337 _exit(1);
338 }
339 path = QFile::encodeName(file);
340 }
341
342 const char **argp = (const char **)malloc((args.count() + 2) * sizeof(char *));
343
344 i = 0;
345 argp[i++] = path.constData();
346 for (const QByteArray &arg : args) {
347 argp[i++] = arg.constData();
348 }
349
350 argp[i] = nullptr;
351
352 execv(path.constData(), const_cast<char **>(argp));
353 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
354 << "execv(" << path << "):" << strerror(errno);
355 _exit(1);
356 return -1; // Shut up compiler. Never reached.
357}
358
359/*
360 * Wait until the terminal is set into no echo mode. At least one su
361 * (RH6 w/ Linux-PAM patches) sets noecho mode AFTER writing the Password:
362 * prompt, using TCSAFLUSH. This flushes the terminal I/O queues, possibly
363 * taking the password with it. So we wait until no echo mode is set
364 * before writing the password.
365 * Note that this is done on the slave fd. While Linux allows tcgetattr() on
366 * the master side, Solaris doesn't.
367 */
369{
371
372 struct termios tio;
373 while (1) {
374 if (!checkPid(m_pid)) {
375 qCCritical(KSU_LOG) << "process has exited while waiting for password.";
376 return -1;
377 }
378 if (!d->pty->tcGetAttr(&tio)) {
379 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
380 << "tcgetattr():" << strerror(errno);
381 return -1;
382 }
383 if (tio.c_lflag & ECHO) {
384 // qDebug() << "[" << __FILE__ << ":" << __LINE__ << "] " << "Echo mode still on.";
385 usleep(10000);
386 continue;
387 }
388 break;
389 }
390 return 0;
391}
392
394{
396
397 d->wantLocalEcho = enable;
398 if (!d->pty) {
399 // Apply it on init
400 return 0;
401 }
402
403 return d->pty->setEcho(enable) ? 0 : -1;
404}
405
406void PtyProcess::setTerminal(bool terminal)
407{
408 m_terminal = terminal;
409}
410
411void PtyProcess::setErase(bool erase)
412{
413 m_erase = erase;
414}
415
416/*
417 * Copy output to stdout until the child process exits, or a line of output
418 * matches `m_exitString'.
419 * We have to use waitpid() to test for exit. Merely waiting for EOF on the
420 * pty does not work, because the target process may have children still
421 * attached to the terminal.
422 */
424{
425 fd_set fds;
426 FD_ZERO(&fds);
427 QByteArray remainder;
428
429 while (1) {
430 FD_SET(fd(), &fds);
431
432 // specify timeout to make sure select() does not block, even if the
433 // process is dead / non-responsive. It does not matter if we abort too
434 // early. In that case 0 is returned, and we'll try again in the next
435 // iteration. (As long as we don't consistently time out in each iteration)
436 timeval timeout;
437 timeout.tv_sec = 0;
438 timeout.tv_usec = 100000;
439 int ret = select(fd() + 1, &fds, nullptr, nullptr, &timeout);
440 if (ret == -1) {
441 if (errno != EINTR) {
442 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
443 << "select():" << strerror(errno);
444 return -1;
445 }
446 ret = 0;
447 }
448
449 if (ret) {
450 for (;;) {
451 QByteArray output = readAll(false);
452 if (output.isEmpty()) {
453 break;
454 }
455 if (m_terminal) {
456 fwrite(output.constData(), output.size(), 1, stdout);
457 fflush(stdout);
458 }
459 if (!m_exitString.isEmpty()) {
460 // match exit string only at line starts
461 remainder += output;
462 while (remainder.length() >= m_exitString.length()) {
463 if (remainder.startsWith(m_exitString)) {
464 kill(m_pid, SIGTERM);
465 remainder.remove(0, m_exitString.length());
466 }
467 int off = remainder.indexOf('\n');
468 if (off < 0) {
469 break;
470 }
471 remainder.remove(0, off + 1);
472 }
473 }
474 }
475 }
476
477 ret = checkPidExited(m_pid);
478 if (ret == Error) {
479 if (errno == ECHILD) {
480 return 0;
481 } else {
482 return 1;
483 }
484 } else if (ret == Killed) {
485 return 0;
486 } else if (ret == NotExited) {
487 continue; // keep checking
488 } else {
489 return ret;
490 }
491 }
492}
493
494/*
495 * SetupTTY: Creates a new session. The filedescriptor "fd" should be
496 * connected to the tty. It is closed after the tty is reopened to make it
497 * our controlling terminal. This way the tty is always opened at least once
498 * so we'll never get EIO when reading from it.
499 */
500int PtyProcess::setupTTY()
501{
503
504 // Reset signal handlers
505 for (int sig = 1; sig < NSIG; sig++) {
506 signal(sig, SIG_DFL);
507 }
508 signal(SIGHUP, SIG_IGN);
509
510 d->pty->setCTty();
511
512 // Connect stdin, stdout and stderr
513 int slave = d->pty->slaveFd();
514 dup2(slave, 0);
515 dup2(slave, 1);
516 dup2(slave, 2);
517
518 // Close all file handles
519 // XXX this caused problems in KProcess - not sure why anymore. -- ???
520 // Because it will close the start notification pipe. -- ossi
521 struct rlimit rlp;
522 getrlimit(RLIMIT_NOFILE, &rlp);
523 for (int i = 3; i < (int)rlp.rlim_cur; i++) {
524 close(i);
525 }
526
527 // Disable OPOST processing. Otherwise, '\n' are (on Linux at least)
528 // translated to '\r\n'.
529 struct ::termios tio;
530 if (tcgetattr(0, &tio) < 0) {
531 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
532 << "tcgetattr():" << strerror(errno);
533 return -1;
534 }
535 tio.c_oflag &= ~OPOST;
536 if (tcsetattr(0, TCSANOW, &tio) < 0) {
537 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
538 << "tcsetattr():" << strerror(errno);
539 return -1;
540 }
541
542 return 0;
543}
544
545void PtyProcess::virtual_hook(int id, void *data)
546{
547 Q_UNUSED(id);
548 Q_UNUSED(data);
549 /*BASE::virtual_hook( id, data );*/
550}
551
552} // namespace KDESu
QString readEntry(const char *key, const char *aDefault=nullptr) const
Synchronous communication with tty programs.
Definition ptyprocess.h:36
int waitSlave()
Waits until the pty has cleared the ECHO flag.
void setExitString(const QByteArray &exit)
Sets the exit string.
int m_pid
PID of child process.
Definition ptyprocess.h:185
static int waitMS(int fd, int ms)
Wait ms milliseconds (ie.
int pid() const
Returns the pid of the process.
QByteArray readLine(bool block=true)
Reads a line from the program's standard out.
@ NotExited
Child hasn't exited.
Definition ptyprocess.h:41
@ Error
No child.
Definition ptyprocess.h:40
@ Killed
Child terminated by signal.
Definition ptyprocess.h:42
QByteArray m_exitString
String to scan for in output that indicates child has exited.
Definition ptyprocess.h:187
QByteArray readAll(bool block=true)
Read all available output from the program's standard out.
int exec(const QByteArray &command, const QList< QByteArray > &args)
Forks off and execute a command.
bool m_terminal
Indicates running in a terminal, causes additional newlines to be printed after output.
Definition ptyprocess.h:182
void setEnvironment(const QList< QByteArray > &env)
Set additinal environment variables.
void setErase(bool erase)
Overwrites the password as soon as it is used.
int fd() const
Returns the filedescriptor of the process.
void writeLine(const QByteArray &line, bool addNewline=true)
Writes a line of text to the program's standard in.
static int checkPidExited(pid_t pid)
Check process exit status for process pid.
static bool checkPid(pid_t pid)
Basic check for the existence of pid.
int waitForChild()
Waits for the child to exit.
QList< QByteArray > environment() const
Returns the additional environment variables set by setEnvironment()
int enableLocalEcho(bool enable=true)
Enables/disables local echo on the pseudo tty.
virtual void virtual_hook(int id, void *data)
Standard hack to add virtual methods in a BC way.
void unreadLine(const QByteArray &line, bool addNewline=true)
Puts back a line of input.
void setTerminal(bool terminal)
Enables/disables terminal output.
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
const char * constData() const const
bool contains(char ch) const const
char * data()
int indexOf(char ch, int from) const const
bool isEmpty() const const
QByteArray left(int len) const const
int length() const const
QByteArray & prepend(char ch)
QByteArray & remove(int pos, int len)
void reserve(int size)
void resize(int size)
int size() const const
bool startsWith(const QByteArray &ba) const const
QString decodeName(const QByteArray &localFileName)
QByteArray encodeName(const QString &fileName)
int count(const T &value) const const
QString findExecutable(const QString &executableName, const QStringList &paths)
const QChar * constData() const const
bool isEmpty() const const
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sun Feb 25 2024 18:39:15 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.