KDESu

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

KDE's Doxygen guidelines are available online.