File indexing completed on 2024-04-28 15:22:04

0001 /*
0002     This file is part of the KDE project, module kdesu.
0003     SPDX-FileCopyrightText: 1999, 2000 Geert Jansen <jansen@kde.org>
0004 
0005     This file contains code from TEShell.C of the KDE konsole.
0006     SPDX-FileCopyrightText: 1997, 1998 Lars Doelle <lars.doelle@on-line.de>
0007 
0008     SPDX-License-Identifier: GPL-2.0-only
0009 
0010     process.cpp: Functionality to build a front end to password asking terminal programs.
0011 */
0012 
0013 #include "ptyprocess.h"
0014 #include "kcookie_p.h"
0015 #include "ptyprocess_p.h"
0016 
0017 #include <config-kdesu.h>
0018 #include <ksu_debug.h>
0019 
0020 #include <cerrno>
0021 #include <fcntl.h>
0022 #include <signal.h>
0023 #include <stdlib.h>
0024 #include <termios.h>
0025 #include <unistd.h>
0026 
0027 #include <sys/resource.h>
0028 #include <sys/stat.h>
0029 #include <sys/time.h>
0030 #include <sys/wait.h>
0031 
0032 #if HAVE_SYS_SELECT_H
0033 #include <sys/select.h> // Needed on some systems.
0034 #endif
0035 
0036 #include <QFile>
0037 #include <QStandardPaths>
0038 
0039 #include <KConfigGroup>
0040 #include <KSharedConfig>
0041 
0042 extern int kdesuDebugArea();
0043 
0044 namespace KDESu
0045 {
0046 using namespace KDESuPrivate;
0047 
0048 /*
0049 ** Wait for @p ms milliseconds
0050 ** @param fd file descriptor
0051 ** @param ms time to wait in milliseconds
0052 ** @return
0053 */
0054 int PtyProcess::waitMS(int fd, int ms)
0055 {
0056     struct timeval tv;
0057     tv.tv_sec = 0;
0058     tv.tv_usec = 1000 * ms;
0059 
0060     fd_set fds;
0061     FD_ZERO(&fds);
0062     FD_SET(fd, &fds);
0063     return select(fd + 1, &fds, nullptr, nullptr, &tv);
0064 }
0065 
0066 // XXX this function is nonsense:
0067 // - for our child, we could use waitpid().
0068 // - the configurability at this place it *complete* braindamage
0069 /*
0070 ** Basic check for the existence of @p pid.
0071 ** Returns true iff @p pid is an extant process.
0072 */
0073 bool PtyProcess::checkPid(pid_t pid)
0074 {
0075     KSharedConfig::Ptr config = KSharedConfig::openConfig();
0076     KConfigGroup cg(config, "super-user-command");
0077     QString superUserCommand = cg.readEntry("super-user-command", "sudo");
0078     // sudo does not accept signals from user so we except it
0079     if (superUserCommand == QLatin1String("sudo")) {
0080         return true;
0081     } else {
0082         return kill(pid, 0) == 0;
0083     }
0084 }
0085 
0086 /*
0087 ** Check process exit status for process @p pid.
0088 ** On error (no child, no exit), return Error (-1).
0089 ** If child @p pid has exited, return its exit status,
0090 ** (which may be zero).
0091 ** If child @p has not exited, return NotExited (-2).
0092 */
0093 int PtyProcess::checkPidExited(pid_t pid)
0094 {
0095     int state;
0096     int ret;
0097     ret = waitpid(pid, &state, WNOHANG);
0098 
0099     if (ret < 0) {
0100         qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
0101                             << "waitpid():" << strerror(errno);
0102         return Error;
0103     }
0104     if (ret == pid) {
0105         if (WIFEXITED(state)) {
0106             return WEXITSTATUS(state);
0107         }
0108 
0109         return Killed;
0110     }
0111 
0112     return NotExited;
0113 }
0114 
0115 PtyProcess::PtyProcess()
0116     : PtyProcess(*new PtyProcessPrivate)
0117 {
0118 }
0119 
0120 PtyProcess::PtyProcess(PtyProcessPrivate &dd)
0121     : d(&dd)
0122 {
0123     m_terminal = false;
0124     m_erase = false;
0125 }
0126 
0127 PtyProcess::~PtyProcess() = default;
0128 
0129 int PtyProcess::init()
0130 {
0131     delete d->pty;
0132     d->pty = new KPty();
0133     if (!d->pty->open()) {
0134         qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
0135                             << "Failed to open PTY.";
0136         return -1;
0137     }
0138     if (!d->wantLocalEcho) {
0139         enableLocalEcho(false);
0140     }
0141     d->inputBuffer.resize(0);
0142     return 0;
0143 }
0144 
0145 /** Set additional environment variables. */
0146 void PtyProcess::setEnvironment(const QList<QByteArray> &env)
0147 {
0148     d->env = env;
0149 }
0150 
0151 int PtyProcess::fd() const
0152 {
0153     return d->pty ? d->pty->masterFd() : -1;
0154 }
0155 
0156 int PtyProcess::pid() const
0157 {
0158     return m_pid;
0159 }
0160 
0161 /** Returns the additional environment variables set by setEnvironment() */
0162 QList<QByteArray> PtyProcess::environment() const
0163 {
0164     return d->env;
0165 }
0166 
0167 QByteArray PtyProcess::readAll(bool block)
0168 {
0169     QByteArray ret;
0170     if (!d->inputBuffer.isEmpty()) {
0171         // if there is still something in the buffer, we need not block.
0172         // we should still try to read any further output, from the fd, though.
0173         block = false;
0174         ret = d->inputBuffer;
0175         d->inputBuffer.resize(0);
0176     }
0177 
0178     int flags = fcntl(fd(), F_GETFL);
0179     if (flags < 0) {
0180         qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
0181                             << "fcntl(F_GETFL):" << strerror(errno);
0182         return ret;
0183     }
0184     int oflags = flags;
0185     if (block) {
0186         flags &= ~O_NONBLOCK;
0187     } else {
0188         flags |= O_NONBLOCK;
0189     }
0190 
0191     if ((flags != oflags) && (fcntl(fd(), F_SETFL, flags) < 0)) {
0192         // We get an error here when the child process has closed
0193         // the file descriptor already.
0194         return ret;
0195     }
0196 
0197     while (1) {
0198         ret.reserve(ret.size() + 0x8000);
0199         int nbytes = read(fd(), ret.data() + ret.size(), 0x8000);
0200         if (nbytes == -1) {
0201             if (errno == EINTR) {
0202                 continue;
0203             } else {
0204                 break;
0205             }
0206         }
0207         if (nbytes == 0) {
0208             break; // nothing available / eof
0209         }
0210 
0211         ret.resize(ret.size() + nbytes);
0212         break;
0213     }
0214 
0215     return ret;
0216 }
0217 
0218 QByteArray PtyProcess::readLine(bool block)
0219 {
0220     d->inputBuffer = readAll(block);
0221 
0222     int pos;
0223     QByteArray ret;
0224     if (!d->inputBuffer.isEmpty()) {
0225         pos = d->inputBuffer.indexOf('\n');
0226         if (pos == -1) {
0227             // NOTE: this means we return something even if there in no full line!
0228             ret = d->inputBuffer;
0229             d->inputBuffer.resize(0);
0230         } else {
0231             ret = d->inputBuffer.left(pos);
0232             d->inputBuffer.remove(0, pos + 1);
0233         }
0234     }
0235 
0236     return ret;
0237 }
0238 
0239 void PtyProcess::writeLine(const QByteArray &line, bool addnl)
0240 {
0241     if (!line.isEmpty()) {
0242         write(fd(), line.constData(), line.length());
0243     }
0244     if (addnl) {
0245         write(fd(), "\n", 1);
0246     }
0247 }
0248 
0249 void PtyProcess::unreadLine(const QByteArray &line, bool addnl)
0250 {
0251     QByteArray tmp = line;
0252     if (addnl) {
0253         tmp += '\n';
0254     }
0255     if (!tmp.isEmpty()) {
0256         d->inputBuffer.prepend(tmp);
0257     }
0258 }
0259 
0260 void PtyProcess::setExitString(const QByteArray &exit)
0261 {
0262     m_exitString = exit;
0263 }
0264 
0265 /*
0266  * Fork and execute the command. This returns in the parent.
0267  */
0268 int PtyProcess::exec(const QByteArray &command, const QList<QByteArray> &args)
0269 {
0270     int i;
0271 
0272     if (init() < 0) {
0273         return -1;
0274     }
0275 
0276     if ((m_pid = fork()) == -1) {
0277         qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
0278                             << "fork():" << strerror(errno);
0279         return -1;
0280     }
0281 
0282     // Parent
0283     if (m_pid) {
0284         d->pty->closeSlave();
0285         return 0;
0286     }
0287 
0288     // Child
0289     if (setupTTY() < 0) {
0290         _exit(1);
0291     }
0292 
0293     for (const QByteArray &var : std::as_const(d->env)) {
0294         putenv(const_cast<char *>(var.constData()));
0295     }
0296     unsetenv("KDE_FULL_SESSION");
0297     // for : Qt: Session management error
0298     unsetenv("SESSION_MANAGER");
0299     // QMutex::lock , deadlocks without that.
0300     // <thiago> you cannot connect to the user's session bus from another UID
0301     unsetenv("DBUS_SESSION_BUS_ADDRESS");
0302 
0303     // set temporarily LC_ALL to C, for su (to be able to parse "Password:")
0304     const QByteArray old_lc_all = qgetenv("LC_ALL");
0305     if (!old_lc_all.isEmpty()) {
0306         qputenv("KDESU_LC_ALL", old_lc_all);
0307     } else {
0308         unsetenv("KDESU_LC_ALL");
0309     }
0310     qputenv("LC_ALL", "C");
0311 
0312     // From now on, terminal output goes through the tty.
0313 
0314     QByteArray path;
0315     if (command.contains('/')) {
0316         path = command;
0317     } else {
0318         QString file = QStandardPaths::findExecutable(QFile::decodeName(command));
0319         if (file.isEmpty()) {
0320             qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " << command << "not found.";
0321             _exit(1);
0322         }
0323         path = QFile::encodeName(file);
0324     }
0325 
0326     const char **argp = (const char **)malloc((args.count() + 2) * sizeof(char *));
0327 
0328     i = 0;
0329     argp[i++] = path.constData();
0330     for (const QByteArray &arg : args) {
0331         argp[i++] = arg.constData();
0332     }
0333 
0334     argp[i] = nullptr;
0335 
0336     execv(path.constData(), const_cast<char **>(argp));
0337     qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
0338                         << "execv(" << path << "):" << strerror(errno);
0339     _exit(1);
0340     return -1; // Shut up compiler. Never reached.
0341 }
0342 
0343 /*
0344  * Wait until the terminal is set into no echo mode. At least one su
0345  * (RH6 w/ Linux-PAM patches) sets noecho mode AFTER writing the Password:
0346  * prompt, using TCSAFLUSH. This flushes the terminal I/O queues, possibly
0347  * taking the password  with it. So we wait until no echo mode is set
0348  * before writing the password.
0349  * Note that this is done on the slave fd. While Linux allows tcgetattr() on
0350  * the master side, Solaris doesn't.
0351  */
0352 int PtyProcess::waitSlave()
0353 {
0354     struct termios tio;
0355     while (1) {
0356         if (!checkPid(m_pid)) {
0357             qCCritical(KSU_LOG) << "process has exited while waiting for password.";
0358             return -1;
0359         }
0360         if (!d->pty->tcGetAttr(&tio)) {
0361             qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
0362                                 << "tcgetattr():" << strerror(errno);
0363             return -1;
0364         }
0365         if (tio.c_lflag & ECHO) {
0366             // qDebug() << "[" << __FILE__ << ":" << __LINE__ << "] " << "Echo mode still on.";
0367             usleep(10000);
0368             continue;
0369         }
0370         break;
0371     }
0372     return 0;
0373 }
0374 
0375 int PtyProcess::enableLocalEcho(bool enable)
0376 {
0377     d->wantLocalEcho = enable;
0378     if (!d->pty) {
0379         // Apply it on init
0380         return 0;
0381     }
0382 
0383     return d->pty->setEcho(enable) ? 0 : -1;
0384 }
0385 
0386 void PtyProcess::setTerminal(bool terminal)
0387 {
0388     m_terminal = terminal;
0389 }
0390 
0391 void PtyProcess::setErase(bool erase)
0392 {
0393     m_erase = erase;
0394 }
0395 
0396 /*
0397  * Copy output to stdout until the child process exits, or a line of output
0398  * matches `m_exitString'.
0399  * We have to use waitpid() to test for exit. Merely waiting for EOF on the
0400  * pty does not work, because the target process may have children still
0401  * attached to the terminal.
0402  */
0403 int PtyProcess::waitForChild()
0404 {
0405     fd_set fds;
0406     FD_ZERO(&fds);
0407     QByteArray remainder;
0408 
0409     while (1) {
0410         FD_SET(fd(), &fds);
0411 
0412         // specify timeout to make sure select() does not block, even if the
0413         // process is dead / non-responsive. It does not matter if we abort too
0414         // early. In that case 0 is returned, and we'll try again in the next
0415         // iteration. (As long as we don't consistently time out in each iteration)
0416         timeval timeout;
0417         timeout.tv_sec = 0;
0418         timeout.tv_usec = 100000;
0419         int ret = select(fd() + 1, &fds, nullptr, nullptr, &timeout);
0420         if (ret == -1) {
0421             if (errno != EINTR) {
0422                 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
0423                                     << "select():" << strerror(errno);
0424                 return -1;
0425             }
0426             ret = 0;
0427         }
0428 
0429         if (ret) {
0430             for (;;) {
0431                 QByteArray output = readAll(false);
0432                 if (output.isEmpty()) {
0433                     break;
0434                 }
0435                 if (m_terminal) {
0436                     fwrite(output.constData(), output.size(), 1, stdout);
0437                     fflush(stdout);
0438                 }
0439                 if (!m_exitString.isEmpty()) {
0440                     // match exit string only at line starts
0441                     remainder += output;
0442                     while (remainder.length() >= m_exitString.length()) {
0443                         if (remainder.startsWith(m_exitString)) {
0444                             kill(m_pid, SIGTERM);
0445                             remainder.remove(0, m_exitString.length());
0446                         }
0447                         int off = remainder.indexOf('\n');
0448                         if (off < 0) {
0449                             break;
0450                         }
0451                         remainder.remove(0, off + 1);
0452                     }
0453                 }
0454             }
0455         }
0456 
0457         ret = checkPidExited(m_pid);
0458         if (ret == Error) {
0459             if (errno == ECHILD) {
0460                 return 0;
0461             } else {
0462                 return 1;
0463             }
0464         } else if (ret == Killed) {
0465             return 0;
0466         } else if (ret == NotExited) {
0467             continue; // keep checking
0468         } else {
0469             return ret;
0470         }
0471     }
0472 }
0473 
0474 /*
0475  * SetupTTY: Creates a new session. The filedescriptor "fd" should be
0476  * connected to the tty. It is closed after the tty is reopened to make it
0477  * our controlling terminal. This way the tty is always opened at least once
0478  * so we'll never get EIO when reading from it.
0479  */
0480 int PtyProcess::setupTTY()
0481 {
0482     // Reset signal handlers
0483     for (int sig = 1; sig < NSIG; sig++) {
0484         signal(sig, SIG_DFL);
0485     }
0486     signal(SIGHUP, SIG_IGN);
0487 
0488     d->pty->setCTty();
0489 
0490     // Connect stdin, stdout and stderr
0491     int slave = d->pty->slaveFd();
0492     dup2(slave, 0);
0493     dup2(slave, 1);
0494     dup2(slave, 2);
0495 
0496     // Close all file handles
0497     // XXX this caused problems in KProcess - not sure why anymore. -- ???
0498     // Because it will close the start notification pipe. -- ossi
0499     struct rlimit rlp;
0500     getrlimit(RLIMIT_NOFILE, &rlp);
0501     for (int i = 3; i < (int)rlp.rlim_cur; i++) {
0502         close(i);
0503     }
0504 
0505     // Disable OPOST processing. Otherwise, '\n' are (on Linux at least)
0506     // translated to '\r\n'.
0507     struct ::termios tio;
0508     if (tcgetattr(0, &tio) < 0) {
0509         qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
0510                             << "tcgetattr():" << strerror(errno);
0511         return -1;
0512     }
0513     tio.c_oflag &= ~OPOST;
0514     if (tcsetattr(0, TCSANOW, &tio) < 0) {
0515         qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
0516                             << "tcsetattr():" << strerror(errno);
0517         return -1;
0518     }
0519 
0520     return 0;
0521 }
0522 
0523 void PtyProcess::virtual_hook(int id, void *data)
0524 {
0525     Q_UNUSED(id);
0526     Q_UNUSED(data);
0527     /*BASE::virtual_hook( id, data );*/
0528 }
0529 
0530 } // namespace KDESu