File indexing completed on 2024-05-05 17:33:43
0001 /* vi: ts=8 sts=4 sw=4 0002 * 0003 * This file is part of the KDE project, module kdesu. 0004 * SPDX-FileCopyrightText: 1998 Pietro Iglio <iglio@fub.it> 0005 * SPDX-FileCopyrightText: 1999, 2000 Geert Jansen <jansen@kde.org> 0006 * SPDX-License-Identifier: Artistic-2.0 0007 */ 0008 0009 #include <config-kde-cli-tools.h> 0010 0011 #include <errno.h> 0012 #include <pwd.h> 0013 #include <stdlib.h> 0014 #include <string.h> 0015 #include <unistd.h> 0016 0017 #include <sys/resource.h> 0018 #include <sys/time.h> 0019 #if HAVE_SYS_WAIT_H 0020 #include <sys/wait.h> 0021 #endif 0022 #if HAVE_SYS_PRCTL_H 0023 #include <sys/prctl.h> 0024 #endif 0025 #if HAVE_SYS_PROCCTL_H 0026 #include <sys/procctl.h> 0027 #include <unistd.h> 0028 #endif 0029 0030 #include <QApplication> 0031 #include <QCommandLineParser> 0032 #include <QFile> 0033 #include <QFileInfo> 0034 #include <QLoggingCategory> 0035 0036 #include <KSharedConfig> 0037 #include <kaboutdata.h> 0038 #include <kconfiggroup.h> 0039 #include <klocalizedstring.h> 0040 #include <kmessagebox.h> 0041 #include <kshell.h> 0042 #include <kstartupinfo.h> 0043 #include <kuser.h> 0044 #include <kwindowsystem.h> 0045 0046 #include <kdesu/client.h> 0047 #include <kdesu/defaults.h> 0048 #include <kdesu/su.h> 0049 0050 #include "sudlg.h" 0051 0052 #define ERR strerror(errno) 0053 0054 static QLoggingCategory category("org.kde.kdesu"); 0055 0056 QByteArray command; 0057 const char *Version = PROJECT_VERSION; 0058 0059 // NOTE: if you change the position of the -u switch, be sure to adjust it 0060 // at the beginning of main() 0061 0062 static int startApp(QCommandLineParser &p); 0063 0064 int main(int argc, char *argv[]) 0065 { 0066 // disable ptrace 0067 #if HAVE_PR_SET_DUMPABLE 0068 prctl(PR_SET_DUMPABLE, 0); 0069 #endif 0070 #if HAVE_PROC_TRACE_CTL 0071 int mode = PROC_TRACE_CTL_DISABLE; 0072 procctl(P_PID, getpid(), PROC_TRACE_CTL, &mode); 0073 #endif 0074 0075 QApplication app(argc, argv); 0076 0077 // FIXME: this can be considered a poor man's solution, as it's not 0078 // directly obvious to a gui user. :) 0079 // anyway, i vote against removing it even when we have a proper gui 0080 // implementation. -- ossi 0081 QByteArray duser = qgetenv("ADMIN_ACCOUNT"); 0082 if (duser.isEmpty()) { 0083 duser = "root"; 0084 } 0085 0086 KLocalizedString::setApplicationDomain("kdesu5"); 0087 0088 KAboutData aboutData(QStringLiteral("kdesu"), 0089 i18n("KDE su"), 0090 QLatin1String(Version), 0091 i18n("Runs a program with elevated privileges."), 0092 KAboutLicense::Artistic, 0093 i18n("Copyright (c) 1998-2000 Geert Jansen, Pietro Iglio")); 0094 aboutData.addAuthor(i18n("Geert Jansen"), i18n("Maintainer"), QStringLiteral("jansen@kde.org"), QStringLiteral("http://www.stack.nl/~geertj/")); 0095 aboutData.addAuthor(i18n("Pietro Iglio"), i18n("Original author"), QStringLiteral("iglio@fub.it")); 0096 app.setWindowIcon(QIcon::fromTheme(QStringLiteral("dialog-password"))); 0097 0098 KAboutData::setApplicationData(aboutData); 0099 0100 // NOTE: if you change the position of the -u switch, be sure to adjust it 0101 // at the beginning of main() 0102 QCommandLineParser parser; 0103 aboutData.setupCommandLine(&parser); 0104 parser.addPositionalArgument(QStringLiteral("command"), i18n("Specifies the command to run as separate arguments")); 0105 parser.addOption(QCommandLineOption(QStringLiteral("c"), i18n("Specifies the command to run as one string"), QStringLiteral("command"))); 0106 parser.addOption(QCommandLineOption(QStringLiteral("f"), i18n("Run command under target uid if <file> is not writable"), QStringLiteral("file"))); 0107 parser.addOption(QCommandLineOption(QStringLiteral("u"), i18n("Specifies the target uid"), QStringLiteral("user"), QString::fromLatin1(duser))); 0108 parser.addOption(QCommandLineOption(QStringLiteral("n"), i18n("Do not keep password"))); 0109 parser.addOption(QCommandLineOption(QStringLiteral("s"), i18n("Stop the daemon (forgets all passwords)"))); 0110 parser.addOption(QCommandLineOption(QStringLiteral("t"), i18n("Enable terminal output (no password keeping)"))); 0111 parser.addOption( 0112 QCommandLineOption(QStringLiteral("p"), i18n("Set priority value: 0 <= prio <= 100, 0 is lowest"), QStringLiteral("prio"), QStringLiteral("50"))); 0113 parser.addOption(QCommandLineOption(QStringLiteral("r"), i18n("Use realtime scheduling"))); 0114 parser.addOption(QCommandLineOption(QStringLiteral("noignorebutton"), i18n("Do not display ignore button"))); 0115 parser.addOption(QCommandLineOption(QStringLiteral("i"), i18n("Specify icon to use in the password dialog"), QStringLiteral("icon name"))); 0116 parser.addOption(QCommandLineOption(QStringLiteral("d"), i18n("Do not show the command to be run in the dialog"))); 0117 #ifdef HAVE_X11 0118 /* KDialog originally used --embed for attaching the dialog box. However this is misleading and so we changed to --attach. 0119 * For consistancy, we silently map --embed to --attach */ 0120 parser.addOption(QCommandLineOption(QStringLiteral("attach"), 0121 i18nc("Transient means that the kdesu app will be attached to the app specified by the winid so that it is like a " 0122 "dialog box rather than some separate program", 0123 "Makes the dialog transient for an X app specified by winid"), 0124 QStringLiteral("winid"))); 0125 parser.addOption(QCommandLineOption(QStringLiteral("embed"), i18n("Embed into a window"), QStringLiteral("winid"))); 0126 #endif 0127 0128 // KApplication::disableAutoDcopRegistration(); 0129 // kdesu doesn't process SM events, so don't even connect to ksmserver 0130 QByteArray session_manager = qgetenv("SESSION_MANAGER"); 0131 if (!session_manager.isEmpty()) { 0132 unsetenv("SESSION_MANAGER"); 0133 } 0134 // but propagate it to the started app 0135 if (!session_manager.isEmpty()) { 0136 setenv("SESSION_MANAGER", session_manager.data(), 1); 0137 } 0138 0139 { 0140 #ifdef HAVE_X11 0141 KStartupInfoId id; 0142 id.initId(); 0143 id.setupStartupEnv(); // make DESKTOP_STARTUP_ID env. var. available again 0144 #endif 0145 } 0146 0147 parser.process(app); 0148 aboutData.processCommandLine(&parser); 0149 int result = startApp(parser); 0150 0151 if (result == 127) { 0152 KMessageBox::error(nullptr, i18n("Cannot execute command '%1'.", QString::fromLocal8Bit(command))); 0153 } 0154 if (result == -2) { 0155 KMessageBox::error(nullptr, i18n("Cannot execute command '%1'. It contains invalid characters.", QString::fromLocal8Bit(command))); 0156 } 0157 0158 return result; 0159 } 0160 0161 static int startApp(QCommandLineParser &p) 0162 { 0163 // Stop daemon and exit? 0164 if (p.isSet(QStringLiteral("s"))) { 0165 KDEsuClient client; 0166 if (client.ping() == -1) { 0167 qCCritical(category) << "Daemon not running -- nothing to stop\n"; 0168 p.showHelp(1); 0169 } 0170 if (client.stopServer() != -1) { 0171 qCDebug(category) << "Daemon stopped\n"; 0172 exit(0); 0173 } 0174 qCCritical(category) << "Could not stop daemon\n"; 0175 p.showHelp(1); 0176 } 0177 0178 QString icon; 0179 if (p.isSet(QStringLiteral("i"))) { 0180 icon = p.value(QStringLiteral("i")); 0181 } 0182 0183 bool prompt = true; 0184 if (p.isSet(QStringLiteral("d"))) { 0185 prompt = false; 0186 } 0187 0188 // Get target uid 0189 const QByteArray user = p.value(QStringLiteral("u")).toLocal8Bit(); 0190 QByteArray auth_user = user; 0191 struct passwd *pw = getpwnam(user.constData()); 0192 if (pw == nullptr) { 0193 qCCritical(category) << "User " << user << " does not exist\n"; 0194 p.showHelp(1); 0195 } 0196 bool other_uid = (getuid() != pw->pw_uid); 0197 bool change_uid = other_uid; 0198 if (!change_uid) { 0199 char *cur_user = getenv("USER"); 0200 if (!cur_user) { 0201 cur_user = getenv("LOGNAME"); 0202 } 0203 change_uid = (!cur_user || user != cur_user); 0204 } 0205 0206 // If file is writeable, do not change uid 0207 QString file = p.value(QStringLiteral("f")); 0208 if (other_uid && !file.isEmpty()) { 0209 if (file.startsWith(QLatin1Char('/'))) { 0210 file = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, file); 0211 if (file.isEmpty()) { 0212 qCCritical(category) << "Config file not found: " << file; 0213 p.showHelp(1); 0214 } 0215 } 0216 QFileInfo fi(file); 0217 if (!fi.exists()) { 0218 qCCritical(category) << "File does not exist: " << file; 0219 p.showHelp(1); 0220 } 0221 change_uid = !fi.isWritable(); 0222 } 0223 0224 // Get priority/scheduler 0225 QString tmp = p.value(QStringLiteral("p")); 0226 bool ok; 0227 int priority = tmp.toInt(&ok); 0228 if (!ok || (priority < 0) || (priority > 100)) { 0229 qCCritical(category) << i18n("Illegal priority: %1", tmp); 0230 p.showHelp(1); 0231 } 0232 int scheduler = SuProcess::SchedNormal; 0233 if (p.isSet(QStringLiteral("r"))) { 0234 scheduler = SuProcess::SchedRealtime; 0235 } 0236 if ((priority > 50) || (scheduler != SuProcess::SchedNormal)) { 0237 change_uid = true; 0238 auth_user = "root"; 0239 } 0240 0241 // Get command 0242 if (p.isSet(QStringLiteral("c"))) { 0243 command = p.value(QStringLiteral("c")).toLocal8Bit(); 0244 // Accepting additional arguments here is somewhat weird, 0245 // but one can conceive use cases: have a complex command with 0246 // redirections and additional file names which need to be quoted 0247 // safely. 0248 } else { 0249 if (p.positionalArguments().count() == 0) { 0250 qCCritical(category) << i18n("No command specified."); 0251 p.showHelp(1); 0252 } 0253 } 0254 const auto positionalArguments = p.positionalArguments(); 0255 for (const QString &arg : positionalArguments) { 0256 command += ' '; 0257 command += QFile::encodeName(KShell::quoteArg(arg)); 0258 } 0259 0260 // Don't change uid if we're don't need to. 0261 if (!change_uid) { 0262 int result = system(command.constData()); 0263 result = WEXITSTATUS(result); 0264 return result; 0265 } 0266 0267 // Check for daemon and start if necessary 0268 bool just_started = false; 0269 bool have_daemon = true; 0270 KDEsuClient client; 0271 if (client.ping() == -1) { 0272 if (client.startServer() == -1) { 0273 qCWarning(category) << "Could not start daemon, reduced functionality.\n"; 0274 have_daemon = false; 0275 } 0276 just_started = true; 0277 } 0278 0279 // Try to exec the command with kdesud. 0280 bool keep = !p.isSet(QStringLiteral("n")) && have_daemon; 0281 bool terminal = p.isSet(QStringLiteral("t")); 0282 bool withIgnoreButton = !p.isSet(QStringLiteral("noignorebutton")); 0283 int winid = -1; 0284 bool attach = p.isSet(QStringLiteral("attach")); 0285 if (attach) { 0286 winid = p.value(QStringLiteral("attach")).toInt(&attach, 0); // C style parsing. If the string begins with "0x", base 16 is used; if the string begins 0287 // with "0", base 8 is used; otherwise, base 10 is used. 0288 if (!attach) { 0289 qCWarning(category) << "Specified winid to attach to is not a valid number"; 0290 } 0291 } else if (p.isSet(QStringLiteral("embed"))) { 0292 /* KDialog originally used --embed for attaching the dialog box. However this is misleading and so we changed to --attach. 0293 * For consistancy, we silently map --embed to --attach */ 0294 attach = true; 0295 winid = p.value(QStringLiteral("embed")).toInt(&attach, 0); // C style parsing. If the string begins with "0x", base 16 is used; if the string begins 0296 // with "0", base 8 is used; otherwise, base 10 is used. 0297 if (!attach) { 0298 qCWarning(category) << "Specified winid to attach to is not a valid number"; 0299 } 0300 } 0301 0302 QList<QByteArray> env; 0303 QByteArray options; 0304 env << ("DESKTOP_STARTUP_ID=" + KStartupInfo::startupId()); 0305 0306 // TODO: Maybe should port this to XDG_*, somehow? 0307 // if (pw->pw_uid) 0308 // { 0309 // // Only propagate KDEHOME for non-root users, 0310 // // root uses KDEROOTHOME 0311 // 0312 // // Translate the KDEHOME of this user to the new user. 0313 // QString kdeHome = KGlobal::dirs()->relativeLocation("home", KGlobal::dirs()->localkdedir()); 0314 // if (kdeHome[0] != '/') 0315 // kdeHome.prepend("~/"); 0316 // else 0317 // kdeHome.clear(); // Use default 0318 // 0319 // env << ("KDEHOME="+ QFile::encodeName(kdeHome)); 0320 // } 0321 0322 KUser u; 0323 env << (QByteArray)("KDESU_USER=" + u.loginName().toLocal8Bit()); 0324 0325 if (keep && !terminal && !just_started) { 0326 client.setPriority(priority); 0327 client.setScheduler(scheduler); 0328 int result = client.exec(command, user, options, env); 0329 if (result == 0) { 0330 result = client.exitCode(); 0331 return result; 0332 } 0333 } 0334 0335 // Set core dump size to 0 because we will have 0336 // root's password in memory. 0337 struct rlimit rlim; 0338 rlim.rlim_cur = rlim.rlim_max = 0; 0339 if (setrlimit(RLIMIT_CORE, &rlim)) { 0340 qCCritical(category) << "rlimit(): " << ERR; 0341 p.showHelp(1); 0342 } 0343 0344 // Read configuration 0345 KConfigGroup config(KSharedConfig::openConfig(), "Passwords"); 0346 int timeout = config.readEntry("Timeout", defTimeout); 0347 0348 // Check if we need a password 0349 SuProcess proc; 0350 proc.setUser(auth_user); 0351 int needpw = proc.checkNeedPassword(); 0352 if (needpw < 0) { 0353 QString err = i18n("Su returned with an error.\n"); 0354 KMessageBox::error(nullptr, err); 0355 p.showHelp(1); 0356 } 0357 if (needpw == 0) { 0358 keep = 0; 0359 qDebug() << "Don't need password!!\n"; 0360 } 0361 0362 const QString cmd = QString::fromLocal8Bit(command); 0363 for (const QChar character : cmd) { 0364 if (!character.isPrint() && character.category() != QChar::Other_Surrogate) { 0365 return -2; 0366 } 0367 } 0368 0369 // Start the dialog 0370 QString password; 0371 if (needpw) { 0372 #ifdef HAVE_X11 0373 KStartupInfoId id; 0374 id.initId(); 0375 KStartupInfoData data; 0376 data.setSilent(KStartupInfoData::Yes); 0377 KStartupInfo::sendChange(id, data); 0378 #endif 0379 KDEsuDialog dlg(user, auth_user, keep && !terminal, icon, withIgnoreButton); 0380 if (prompt) { 0381 dlg.addCommentLine(i18n("Command:"), QFile::decodeName(command)); 0382 } 0383 if (defKeep) { 0384 dlg.setKeepPassword(true); 0385 } 0386 0387 if ((priority != 50) || (scheduler != SuProcess::SchedNormal)) { 0388 QString prio; 0389 if (scheduler == SuProcess::SchedRealtime) { 0390 prio += i18n("realtime: "); 0391 } 0392 prio += QStringLiteral("%1/100").arg(priority); 0393 if (prompt) { 0394 dlg.addCommentLine(i18n("Priority:"), prio); 0395 } 0396 } 0397 0398 // Attach dialog 0399 #ifdef HAVE_X11 0400 if (attach) { 0401 dlg.setAttribute(Qt::WA_NativeWindow, true); 0402 KWindowSystem::setMainWindow(dlg.windowHandle(), WId(winid)); 0403 } 0404 #endif 0405 int ret = dlg.exec(); 0406 if (ret == KDEsuDialog::Rejected) { 0407 #ifdef HAVE_X11 0408 KStartupInfo::sendFinish(id); 0409 #endif 0410 p.showHelp(1); 0411 } 0412 if (ret == KDEsuDialog::AsUser) { 0413 change_uid = false; 0414 } 0415 password = dlg.password(); 0416 keep = dlg.keepPassword(); 0417 #ifdef HAVE_X11 0418 data.setSilent(KStartupInfoData::No); 0419 KStartupInfo::sendChange(id, data); 0420 #endif 0421 } 0422 0423 // Some events may need to be handled (like a button animation) 0424 qApp->processEvents(); 0425 0426 // Run command 0427 if (!change_uid) { 0428 int result = system(command.constData()); 0429 result = WEXITSTATUS(result); 0430 return result; 0431 } else if (keep && have_daemon) { 0432 client.setPass(password.toLocal8Bit().constData(), timeout); 0433 client.setPriority(priority); 0434 client.setScheduler(scheduler); 0435 int result = client.exec(command, user, options, env); 0436 if (result == 0) { 0437 result = client.exitCode(); 0438 return result; 0439 } 0440 } else { 0441 SuProcess proc; 0442 proc.setTerminal(terminal); 0443 proc.setErase(true); 0444 proc.setUser(user); 0445 proc.setEnvironment(env); 0446 proc.setPriority(priority); 0447 proc.setScheduler(scheduler); 0448 proc.setCommand(command); 0449 int result = proc.exec(password.toLocal8Bit().constData()); 0450 return result; 0451 } 0452 return -1; 0453 }