forked from quickshell/quickshell
service/pam: move pam execution to subprocess to allow killing it
Many pam modules can't be aborted well without this.
This commit is contained in:
parent
b5c8774a79
commit
e89035b18c
|
@ -28,6 +28,7 @@ Checks: >
|
|||
-modernize-return-braced-init-list,
|
||||
-modernize-use-trailing-return-type,
|
||||
performance-*,
|
||||
-performance-avoid-endl,
|
||||
portability-std-allocator-const,
|
||||
readability-*,
|
||||
-readability-function-cognitive-complexity,
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
qt_add_library(quickshell-service-pam STATIC
|
||||
qml.cpp
|
||||
conversation.cpp
|
||||
ipc.cpp
|
||||
subprocess.cpp
|
||||
)
|
||||
qt_add_qml_module(quickshell-service-pam
|
||||
URI Quickshell.Services.Pam
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
#include "conversation.hpp"
|
||||
#include <utility>
|
||||
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qmutex.h>
|
||||
#include <qobject.h>
|
||||
#include <qsocketnotifier.h>
|
||||
#include <qstring.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <security/_pam_types.h>
|
||||
#include <security/pam_appl.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#include "ipc.hpp"
|
||||
|
||||
Q_LOGGING_CATEGORY(logPam, "quickshell.service.pam", QtWarningMsg);
|
||||
|
||||
QString PamError::toString(PamError::Enum value) {
|
||||
switch (value) {
|
||||
case ConnectionFailed: return "Failed to connect to pam";
|
||||
case StartFailed: return "Failed to start the PAM session";
|
||||
case TryAuthFailed: return "Failed to try authenticating";
|
||||
case InternalError: return "Internal error occurred";
|
||||
default: return "Invalid error";
|
||||
}
|
||||
}
|
||||
|
@ -26,160 +27,116 @@ QString PamResult::toString(PamResult::Enum value) {
|
|||
case Failed: return "Failed";
|
||||
case Error: return "Error occurred while authenticating";
|
||||
case MaxTries: return "The authentication method has no more attempts available";
|
||||
// case Expired: return "The account has expired";
|
||||
// case PermissionDenied: return "Permission denied";
|
||||
default: return "Invalid result";
|
||||
}
|
||||
}
|
||||
|
||||
void PamConversation::run() {
|
||||
auto conv = pam_conv {
|
||||
.conv = &PamConversation::conversation,
|
||||
.appdata_ptr = this,
|
||||
};
|
||||
PamConversation::~PamConversation() { this->abort(); }
|
||||
|
||||
pam_handle_t* handle = nullptr;
|
||||
|
||||
qCInfo(logPam) << this << "Starting pam session for user" << this->user << "with config"
|
||||
<< this->config << "in configdir" << this->configDir;
|
||||
|
||||
auto result = pam_start_confdir(
|
||||
this->config.toStdString().c_str(),
|
||||
this->user.toStdString().c_str(),
|
||||
&conv,
|
||||
this->configDir.toStdString().c_str(),
|
||||
&handle
|
||||
);
|
||||
|
||||
if (result != PAM_SUCCESS) {
|
||||
qCCritical(logPam) << this << "Unable to start pam conversation with error"
|
||||
<< QString(pam_strerror(handle, result));
|
||||
emit this->error(PamError::ConnectionFailed);
|
||||
this->deleteLater();
|
||||
void PamConversation::start(const QString& configDir, const QString& config, const QString& user) {
|
||||
this->childPid = PamConversation::createSubprocess(&this->pipes, configDir, config, user);
|
||||
if (this->childPid == 0) {
|
||||
qCCritical(logPam) << "Failed to create pam subprocess.";
|
||||
emit this->error(PamError::InternalError);
|
||||
return;
|
||||
}
|
||||
|
||||
result = pam_authenticate(handle, 0);
|
||||
|
||||
// Seems to require root and quickshell should not run as root.
|
||||
// if (result == PAM_SUCCESS) {
|
||||
// result = pam_acct_mgmt(handle, 0);
|
||||
// }
|
||||
|
||||
switch (result) {
|
||||
case PAM_SUCCESS:
|
||||
qCInfo(logPam) << this << "ended with successful authentication.";
|
||||
emit this->completed(PamResult::Success);
|
||||
break;
|
||||
case PAM_AUTH_ERR:
|
||||
qCInfo(logPam) << this << "ended with failed authentication.";
|
||||
emit this->completed(PamResult::Failed);
|
||||
break;
|
||||
case PAM_MAXTRIES:
|
||||
qCInfo(logPam) << this << "ended with failure: max tries.";
|
||||
emit this->completed(PamResult::MaxTries);
|
||||
break;
|
||||
/*case PAM_ACCT_EXPIRED:
|
||||
qCInfo(logPam) << this << "ended with failure: account expiration.";
|
||||
emit this->completed(PamResult::Expired);
|
||||
break;
|
||||
case PAM_PERM_DENIED:
|
||||
qCInfo(logPam) << this << "ended with failure: permission denied.";
|
||||
emit this->completed(PamResult::PermissionDenied);
|
||||
break;*/
|
||||
default:
|
||||
qCCritical(logPam) << this << "ended with error:" << QString(pam_strerror(handle, result));
|
||||
emit this->error(PamError::TryAuthFailed);
|
||||
break;
|
||||
}
|
||||
|
||||
result = pam_end(handle, result);
|
||||
if (result != PAM_SUCCESS) {
|
||||
qCCritical(logPam) << this << "Failed to end pam conversation with error code"
|
||||
<< QString(pam_strerror(handle, result));
|
||||
}
|
||||
|
||||
this->deleteLater();
|
||||
QObject::connect(&this->notifier, &QSocketNotifier::activated, this, &PamConversation::onMessage);
|
||||
this->notifier.setSocket(this->pipes.fdIn);
|
||||
this->notifier.setEnabled(true);
|
||||
}
|
||||
|
||||
void PamConversation::abort() {
|
||||
qCDebug(logPam) << "Abort requested for" << this;
|
||||
auto locker = QMutexLocker(&this->wakeMutex);
|
||||
this->mAbort = true;
|
||||
this->waker.wakeOne();
|
||||
if (this->childPid != 0) {
|
||||
qCDebug(logPam) << "Killing subprocess for" << this;
|
||||
kill(this->childPid, SIGKILL); // NOLINT (include)
|
||||
waitpid(this->childPid, nullptr, 0);
|
||||
this->childPid = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void PamConversation::respond(QString response) {
|
||||
qCDebug(logPam) << "Set response for" << this;
|
||||
auto locker = QMutexLocker(&this->wakeMutex);
|
||||
this->response = std::move(response);
|
||||
this->hasResponse = true;
|
||||
this->waker.wakeOne();
|
||||
void PamConversation::internalError() {
|
||||
if (this->childPid != 0) {
|
||||
qCDebug(logPam) << "Killing subprocess for" << this;
|
||||
kill(this->childPid, SIGKILL); // NOLINT (include)
|
||||
waitpid(this->childPid, nullptr, 0);
|
||||
this->childPid = 0;
|
||||
emit this->error(PamError::InternalError);
|
||||
}
|
||||
}
|
||||
|
||||
int PamConversation::conversation(
|
||||
int msgCount,
|
||||
const pam_message** msgArray,
|
||||
pam_response** responseArray,
|
||||
void* appdata
|
||||
) {
|
||||
auto* delegate = static_cast<PamConversation*>(appdata);
|
||||
void PamConversation::respond(const QString& response) {
|
||||
qCDebug(logPam) << "Sending response for" << this;
|
||||
if (!this->pipes.writeString(response.toStdString())) {
|
||||
qCCritical(logPam) << "Failed to write response to subprocess.";
|
||||
this->internalError();
|
||||
}
|
||||
}
|
||||
|
||||
void PamConversation::onMessage() {
|
||||
{
|
||||
auto locker = QMutexLocker(&delegate->wakeMutex);
|
||||
if (delegate->mAbort) {
|
||||
return PAM_ERROR_MSG;
|
||||
}
|
||||
}
|
||||
qCDebug(logPam) << "Got message from subprocess.";
|
||||
|
||||
// freed by libc so must be alloc'd by it.
|
||||
auto* responses = static_cast<pam_response*>(calloc(msgCount, sizeof(pam_response))); // NOLINT
|
||||
auto type = PamIpcEvent::Exit;
|
||||
|
||||
for (auto i = 0; i < msgCount; i++) {
|
||||
const auto* message = msgArray[i]; // NOLINT
|
||||
auto& response = responses[i]; // NOLINT
|
||||
auto ok = this->pipes.readBytes(
|
||||
reinterpret_cast<char*>(&type), // NOLINT
|
||||
sizeof(PamIpcEvent)
|
||||
);
|
||||
|
||||
auto msgString = QString(message->msg);
|
||||
auto messageChanged = true; // message->msg_style != PAM_PROMPT_ECHO_OFF;
|
||||
auto isError = message->msg_style == PAM_ERROR_MSG;
|
||||
auto responseRequired =
|
||||
message->msg_style == PAM_PROMPT_ECHO_OFF || message->msg_style == PAM_PROMPT_ECHO_ON;
|
||||
if (!ok) goto fail;
|
||||
|
||||
qCDebug(logPam) << delegate << "got new message message:" << msgString
|
||||
<< "messageChanged:" << messageChanged << "isError:" << isError
|
||||
<< "responseRequired" << responseRequired;
|
||||
if (type == PamIpcEvent::Exit) {
|
||||
auto code = PamIpcExitCode::OtherError;
|
||||
|
||||
delegate->hasResponse = false;
|
||||
emit delegate->message(msgString, messageChanged, isError, responseRequired);
|
||||
ok = this->pipes.readBytes(
|
||||
reinterpret_cast<char*>(&code), // NOLINT
|
||||
sizeof(PamIpcExitCode)
|
||||
);
|
||||
|
||||
{
|
||||
auto locker = QMutexLocker(&delegate->wakeMutex);
|
||||
if (!ok) goto fail;
|
||||
|
||||
if (delegate->mAbort) {
|
||||
free(responses); // NOLINT
|
||||
return PAM_ERROR_MSG;
|
||||
qCDebug(logPam) << "Subprocess exited with code" << static_cast<int>(code);
|
||||
|
||||
switch (code) {
|
||||
case PamIpcExitCode::Success: emit this->completed(PamResult::Success); break;
|
||||
case PamIpcExitCode::AuthFailed: emit this->completed(PamResult::Failed); break;
|
||||
case PamIpcExitCode::StartFailed: emit this->error(PamError::StartFailed); break;
|
||||
case PamIpcExitCode::MaxTries: emit this->completed(PamResult::MaxTries); break;
|
||||
case PamIpcExitCode::PamError: emit this->error(PamError::TryAuthFailed); break;
|
||||
case PamIpcExitCode::OtherError: emit this->error(PamError::InternalError); break;
|
||||
}
|
||||
|
||||
if (responseRequired) {
|
||||
if (!delegate->hasResponse) {
|
||||
delegate->waker.wait(locker.mutex());
|
||||
waitpid(this->childPid, nullptr, 0);
|
||||
this->childPid = 0;
|
||||
} else if (type == PamIpcEvent::Request) {
|
||||
PamIpcRequestFlags flags {};
|
||||
|
||||
if (delegate->mAbort) {
|
||||
free(responses); // NOLINT
|
||||
return PAM_ERROR_MSG;
|
||||
}
|
||||
}
|
||||
ok = this->pipes.readBytes(
|
||||
reinterpret_cast<char*>(&flags), // NOLINT
|
||||
sizeof(PamIpcRequestFlags)
|
||||
);
|
||||
|
||||
if (!delegate->hasResponse) {
|
||||
qCCritical(logPam
|
||||
) << "Pam conversation requires response and does not have one. This should not happen.";
|
||||
}
|
||||
if (!ok) goto fail;
|
||||
|
||||
response.resp = strdup(delegate->response.toStdString().c_str()); // NOLINT (include error)
|
||||
}
|
||||
auto message = this->pipes.readString(&ok);
|
||||
|
||||
if (!ok) goto fail;
|
||||
|
||||
this->message(
|
||||
QString::fromUtf8(message),
|
||||
/*flags.echo*/ true,
|
||||
flags.error,
|
||||
flags.responseRequired
|
||||
);
|
||||
} else {
|
||||
qCCritical(logPam) << "Unexpected message from subprocess.";
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
||||
*responseArray = responses;
|
||||
return PAM_SUCCESS;
|
||||
fail:
|
||||
qCCritical(logPam) << "Failed to read subprocess request.";
|
||||
this->internalError();
|
||||
}
|
||||
|
|
|
@ -2,13 +2,16 @@
|
|||
|
||||
#include <utility>
|
||||
|
||||
#include <qmutex.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qthread.h>
|
||||
#include <qsocketnotifier.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qwaitcondition.h>
|
||||
#include <security/pam_appl.h>
|
||||
|
||||
#include "ipc.hpp"
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(logPam);
|
||||
|
||||
/// The result of an authentication.
|
||||
class PamResult: public QObject {
|
||||
|
@ -26,10 +29,6 @@ public:
|
|||
Error = 2,
|
||||
/// The authentication method ran out of tries and should not be used again.
|
||||
MaxTries = 3,
|
||||
// The account has expired.
|
||||
// Expired 4,
|
||||
// Permission denied.
|
||||
// PermissionDenied 5,
|
||||
};
|
||||
Q_ENUM(Enum);
|
||||
|
||||
|
@ -44,52 +43,56 @@ class PamError: public QObject {
|
|||
|
||||
public:
|
||||
enum Enum {
|
||||
/// Failed to initiate the pam connection.
|
||||
ConnectionFailed = 1,
|
||||
/// Failed to start the pam session.
|
||||
StartFailed = 1,
|
||||
/// Failed to try to authenticate the user.
|
||||
/// This is not the same as the user failing to authenticate.
|
||||
TryAuthFailed = 2,
|
||||
/// An error occurred inside quickshell's pam interface.
|
||||
InternalError = 3,
|
||||
};
|
||||
Q_ENUM(Enum);
|
||||
|
||||
Q_INVOKABLE static QString toString(PamError::Enum value);
|
||||
};
|
||||
|
||||
class PamConversation: public QThread {
|
||||
// PAM has no way to abort a running module except when it sends a message,
|
||||
// meaning aborts for things like fingerprint scanners
|
||||
// and hardware keys don't actually work without aborting the process...
|
||||
// so we have a subprocess.
|
||||
class PamConversation: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit PamConversation(QString config, QString configDir, QString user)
|
||||
: config(std::move(config))
|
||||
, configDir(std::move(configDir))
|
||||
, user(std::move(user)) {}
|
||||
explicit PamConversation(QObject* parent): QObject(parent) {}
|
||||
~PamConversation() override;
|
||||
Q_DISABLE_COPY_MOVE(PamConversation);
|
||||
|
||||
public:
|
||||
void run() override;
|
||||
void start(const QString& configDir, const QString& config, const QString& user);
|
||||
|
||||
void abort();
|
||||
void respond(QString response);
|
||||
void respond(const QString& response);
|
||||
|
||||
signals:
|
||||
void completed(PamResult::Enum result);
|
||||
void error(PamError::Enum error);
|
||||
void message(QString message, bool messageChanged, bool isError, bool responseRequired);
|
||||
|
||||
private slots:
|
||||
void onMessage();
|
||||
|
||||
private:
|
||||
static int conversation(
|
||||
int msgCount,
|
||||
const pam_message** msgArray,
|
||||
pam_response** responseArray,
|
||||
void* appdata
|
||||
static pid_t createSubprocess(
|
||||
PamIpcPipes* pipes,
|
||||
const QString& configDir,
|
||||
const QString& config,
|
||||
const QString& user
|
||||
);
|
||||
|
||||
QString config;
|
||||
QString configDir;
|
||||
QString user;
|
||||
void internalError();
|
||||
|
||||
QMutex wakeMutex;
|
||||
QWaitCondition waker;
|
||||
bool mAbort = false;
|
||||
bool hasResponse = false;
|
||||
QString response;
|
||||
pid_t childPid = 0;
|
||||
PamIpcPipes pipes;
|
||||
QSocketNotifier notifier {QSocketNotifier::Read};
|
||||
};
|
||||
|
|
69
src/services/pam/ipc.cpp
Normal file
69
src/services/pam/ipc.cpp
Normal file
|
@ -0,0 +1,69 @@
|
|||
#include "ipc.hpp"
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
PamIpcPipes::~PamIpcPipes() {
|
||||
if (this->fdIn != 0) close(this->fdIn);
|
||||
if (this->fdOut != 0) close(this->fdOut);
|
||||
}
|
||||
|
||||
bool PamIpcPipes::readBytes(char* buffer, size_t length) const {
|
||||
size_t i = 0;
|
||||
|
||||
while (i < length) {
|
||||
auto count = read(this->fdIn, buffer + i, length - i); // NOLINT
|
||||
|
||||
if (count == -1 || count == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
i += count;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PamIpcPipes::writeBytes(const char* buffer, size_t length) const {
|
||||
size_t i = 0;
|
||||
while (i < length) {
|
||||
auto count = write(this->fdOut, buffer + i, length - i); // NOLINT
|
||||
|
||||
if (count == -1 || count == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
i += count;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string PamIpcPipes::readString(bool* ok) const {
|
||||
if (ok != nullptr) *ok = false;
|
||||
|
||||
uint32_t length = 0;
|
||||
if (!this->readBytes(reinterpret_cast<char*>(&length), sizeof(length))) { // NOLINT
|
||||
return "";
|
||||
}
|
||||
|
||||
char data[length]; // NOLINT
|
||||
if (!this->readBytes(data, length)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (ok != nullptr) *ok = true;
|
||||
|
||||
return std::string(data, length);
|
||||
}
|
||||
|
||||
bool PamIpcPipes::writeString(const std::string& string) const {
|
||||
uint32_t length = string.length();
|
||||
if (!this->writeBytes(reinterpret_cast<char*>(&length), sizeof(length))) { // NOLINT
|
||||
return false;
|
||||
}
|
||||
|
||||
return this->writeBytes(string.data(), string.length());
|
||||
}
|
43
src/services/pam/ipc.hpp
Normal file
43
src/services/pam/ipc.hpp
Normal file
|
@ -0,0 +1,43 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include <qtclasshelpermacros.h>
|
||||
|
||||
enum class PamIpcEvent : uint8_t {
|
||||
Request,
|
||||
Exit,
|
||||
};
|
||||
|
||||
enum class PamIpcExitCode : uint8_t {
|
||||
Success,
|
||||
StartFailed,
|
||||
AuthFailed,
|
||||
MaxTries,
|
||||
PamError,
|
||||
OtherError,
|
||||
};
|
||||
|
||||
struct PamIpcRequestFlags {
|
||||
bool echo;
|
||||
bool error;
|
||||
bool responseRequired;
|
||||
};
|
||||
|
||||
class PamIpcPipes {
|
||||
public:
|
||||
explicit PamIpcPipes() = default;
|
||||
explicit PamIpcPipes(int fdIn, int fdOut): fdIn(fdIn), fdOut(fdOut) {}
|
||||
~PamIpcPipes();
|
||||
Q_DISABLE_COPY_MOVE(PamIpcPipes);
|
||||
|
||||
[[nodiscard]] bool readBytes(char* buffer, size_t length) const;
|
||||
[[nodiscard]] bool writeBytes(const char* buffer, size_t length) const;
|
||||
[[nodiscard]] std::string readString(bool* ok) const;
|
||||
[[nodiscard]] bool writeString(const std::string& string) const;
|
||||
|
||||
int fdIn = 0;
|
||||
int fdOut = 0;
|
||||
};
|
|
@ -13,12 +13,6 @@
|
|||
|
||||
#include "conversation.hpp"
|
||||
|
||||
PamContext::~PamContext() {
|
||||
if (this->conversation != nullptr && this->conversation->isRunning()) {
|
||||
this->conversation->abort();
|
||||
}
|
||||
}
|
||||
|
||||
void PamContext::componentComplete() {
|
||||
this->postInit = true;
|
||||
|
||||
|
@ -91,12 +85,12 @@ void PamContext::startConversation() {
|
|||
}
|
||||
}
|
||||
|
||||
this->conversation = new PamConversation(this->mConfig, this->mConfigDirectory, user);
|
||||
this->conversation = new PamConversation(this);
|
||||
QObject::connect(this->conversation, &PamConversation::completed, this, &PamContext::onCompleted);
|
||||
QObject::connect(this->conversation, &PamConversation::error, this, &PamContext::onError);
|
||||
QObject::connect(this->conversation, &PamConversation::message, this, &PamContext::onMessage);
|
||||
emit this->activeChanged();
|
||||
this->conversation->start();
|
||||
this->conversation->start(this->mConfigDirectory, this->mConfig, user);
|
||||
}
|
||||
|
||||
void PamContext::abortConversation() {
|
||||
|
@ -104,7 +98,7 @@ void PamContext::abortConversation() {
|
|||
this->mTargetActive = false;
|
||||
|
||||
QObject::disconnect(this->conversation, nullptr, this, nullptr);
|
||||
if (this->conversation->isRunning()) this->conversation->abort();
|
||||
this->conversation->deleteLater();
|
||||
this->conversation = nullptr;
|
||||
emit this->activeChanged();
|
||||
|
||||
|
@ -124,9 +118,9 @@ void PamContext::abortConversation() {
|
|||
}
|
||||
}
|
||||
|
||||
void PamContext::respond(QString response) {
|
||||
void PamContext::respond(const QString& response) {
|
||||
if (this->isActive() && this->mIsResponseRequired) {
|
||||
this->conversation->respond(std::move(response));
|
||||
this->conversation->respond(response);
|
||||
} else {
|
||||
qWarning() << "PamContext response was ignored as this context does not require one.";
|
||||
}
|
||||
|
|
|
@ -51,8 +51,6 @@ class PamContext
|
|||
|
||||
public:
|
||||
explicit PamContext(QObject* parent = nullptr): QObject(parent) {}
|
||||
~PamContext() override;
|
||||
Q_DISABLE_COPY_MOVE(PamContext);
|
||||
|
||||
void classBegin() override {}
|
||||
void componentComplete() override;
|
||||
|
@ -69,7 +67,7 @@ public:
|
|||
/// Respond to pam.
|
||||
///
|
||||
/// May not be called unless `responseRequired` is true.
|
||||
Q_INVOKABLE void respond(QString response);
|
||||
Q_INVOKABLE void respond(const QString& response);
|
||||
|
||||
[[nodiscard]] bool isActive() const;
|
||||
void setActive(bool active);
|
||||
|
|
209
src/services/pam/subprocess.cpp
Normal file
209
src/services/pam/subprocess.cpp
Normal file
|
@ -0,0 +1,209 @@
|
|||
#include "subprocess.hpp"
|
||||
#include <array>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qstring.h>
|
||||
#include <sched.h>
|
||||
#include <security/_pam_types.h>
|
||||
#include <security/pam_appl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "conversation.hpp"
|
||||
#include "ipc.hpp"
|
||||
|
||||
pid_t PamConversation::createSubprocess(
|
||||
PamIpcPipes* pipes,
|
||||
const QString& configDir,
|
||||
const QString& config,
|
||||
const QString& user
|
||||
) {
|
||||
auto toSubprocess = std::array<int, 2>();
|
||||
auto fromSubprocess = std::array<int, 2>();
|
||||
|
||||
if (pipe(toSubprocess.data()) == -1 || pipe(fromSubprocess.data()) == -1) {
|
||||
qCDebug(logPam) << "Failed to create pipes for subprocess.";
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto* configDirF = strdup(configDir.toStdString().c_str()); // NOLINT (include)
|
||||
auto* configF = strdup(config.toStdString().c_str()); // NOLINT (include)
|
||||
auto* userF = strdup(user.toStdString().c_str()); // NOLINT (include)
|
||||
auto log = logPam().isDebugEnabled();
|
||||
|
||||
auto pid = fork();
|
||||
|
||||
if (pid < 0) {
|
||||
qCDebug(logPam) << "Failed to fork for subprocess.";
|
||||
} else if (pid == 0) {
|
||||
close(toSubprocess[1]); // close w
|
||||
close(fromSubprocess[0]); // close r
|
||||
|
||||
{
|
||||
auto subprocess = PamSubprocess(log, toSubprocess[0], fromSubprocess[1]);
|
||||
auto code = subprocess.exec(configDirF, configF, userF);
|
||||
subprocess.sendCode(code);
|
||||
}
|
||||
|
||||
free(configDirF); // NOLINT
|
||||
free(configF); // NOLINT
|
||||
free(userF); // NOLINT
|
||||
|
||||
// do not do cleanup that may affect the parent
|
||||
_exit(0);
|
||||
} else {
|
||||
close(toSubprocess[0]); // close r
|
||||
close(fromSubprocess[1]); // close w
|
||||
|
||||
pipes->fdIn = fromSubprocess[0];
|
||||
pipes->fdOut = toSubprocess[1];
|
||||
|
||||
free(configDirF); // NOLINT
|
||||
free(configF); // NOLINT
|
||||
free(userF); // NOLINT
|
||||
|
||||
return pid;
|
||||
}
|
||||
|
||||
return -1; // should never happen but lint
|
||||
}
|
||||
|
||||
PamIpcExitCode PamSubprocess::exec(const char* configDir, const char* config, const char* user) {
|
||||
logIf(this->log) << "Waiting for parent confirmation..." << std::endl;
|
||||
|
||||
auto conv = pam_conv {
|
||||
.conv = &PamSubprocess::conversation,
|
||||
.appdata_ptr = this,
|
||||
};
|
||||
|
||||
pam_handle_t* handle = nullptr;
|
||||
|
||||
logIf(this->log) << "Starting pam session for user \"" << user << "\" with config \"" << config
|
||||
<< "\" in dir \"" << configDir << "\"" << std::endl;
|
||||
|
||||
auto result = pam_start_confdir(config, user, &conv, configDir, &handle);
|
||||
|
||||
if (result != PAM_SUCCESS) {
|
||||
logIf(true) << "Unable to start pam conversation with error \"" << pam_strerror(handle, result)
|
||||
<< "\" (code " << result << ")" << std::endl;
|
||||
return PamIpcExitCode::StartFailed;
|
||||
}
|
||||
|
||||
result = pam_authenticate(handle, 0);
|
||||
PamIpcExitCode code = PamIpcExitCode::OtherError;
|
||||
|
||||
switch (result) {
|
||||
case PAM_SUCCESS:
|
||||
logIf(this->log) << "Authenticated successfully." << std::endl;
|
||||
code = PamIpcExitCode::Success;
|
||||
break;
|
||||
case PAM_AUTH_ERR:
|
||||
logIf(this->log) << "Failed to authenticate." << std::endl;
|
||||
code = PamIpcExitCode::AuthFailed;
|
||||
break;
|
||||
case PAM_MAXTRIES:
|
||||
logIf(this->log) << "Failed to authenticate due to hitting max tries." << std::endl;
|
||||
code = PamIpcExitCode::MaxTries;
|
||||
break;
|
||||
default:
|
||||
logIf(true) << "Error while authenticating: \"" << pam_strerror(handle, result) << "\" (code "
|
||||
<< result << ")" << std::endl;
|
||||
code = PamIpcExitCode::PamError;
|
||||
break;
|
||||
}
|
||||
|
||||
result = pam_end(handle, result);
|
||||
if (result != PAM_SUCCESS) {
|
||||
logIf(true) << "Error in pam_end: \"" << pam_strerror(handle, result) << "\" (code " << result
|
||||
<< ")" << std::endl;
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
void PamSubprocess::sendCode(PamIpcExitCode code) {
|
||||
{
|
||||
auto eventType = PamIpcEvent::Exit;
|
||||
auto ok = this->pipes.writeBytes(
|
||||
reinterpret_cast<char*>(&eventType), // NOLINT
|
||||
sizeof(PamIpcEvent)
|
||||
);
|
||||
|
||||
if (!ok) goto fail;
|
||||
|
||||
ok = this->pipes.writeBytes(
|
||||
reinterpret_cast<char*>(&code), // NOLINT
|
||||
sizeof(PamIpcExitCode)
|
||||
);
|
||||
|
||||
if (!ok) goto fail;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
fail:
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
int PamSubprocess::conversation(
|
||||
int msgCount,
|
||||
const pam_message** msgArray,
|
||||
pam_response** responseArray,
|
||||
void* appdata
|
||||
) {
|
||||
auto* delegate = static_cast<PamSubprocess*>(appdata);
|
||||
|
||||
// freed by libc so must be alloc'd by it.
|
||||
auto* responses = static_cast<pam_response*>(calloc(msgCount, sizeof(pam_response))); // NOLINT
|
||||
|
||||
for (auto i = 0; i < msgCount; i++) {
|
||||
const auto* message = msgArray[i]; // NOLINT
|
||||
auto& response = responses[i]; // NOLINT
|
||||
|
||||
auto msgString = std::string(message->msg);
|
||||
auto req = PamIpcRequestFlags {
|
||||
.echo = message->msg_style != PAM_PROMPT_ECHO_OFF,
|
||||
.error = message->msg_style == PAM_ERROR_MSG,
|
||||
.responseRequired =
|
||||
message->msg_style == PAM_PROMPT_ECHO_OFF || message->msg_style == PAM_PROMPT_ECHO_ON,
|
||||
};
|
||||
|
||||
logIf(delegate->log) << "Relaying pam message: \"" << msgString << "\" echo: " << req.echo
|
||||
<< " error: " << req.error << " responseRequired: " << req.responseRequired
|
||||
<< std::endl;
|
||||
|
||||
auto eventType = PamIpcEvent::Request;
|
||||
auto ok = delegate->pipes.writeBytes(
|
||||
reinterpret_cast<char*>(&eventType), // NOLINT
|
||||
sizeof(PamIpcEvent)
|
||||
);
|
||||
|
||||
if (!ok) goto fail;
|
||||
|
||||
ok = delegate->pipes.writeBytes(
|
||||
reinterpret_cast<const char*>(&req), // NOLINT
|
||||
sizeof(PamIpcRequestFlags)
|
||||
);
|
||||
|
||||
if (!ok) goto fail;
|
||||
if (!delegate->pipes.writeString(msgString)) goto fail;
|
||||
|
||||
if (req.responseRequired) {
|
||||
auto ok = false;
|
||||
auto resp = delegate->pipes.readString(&ok);
|
||||
if (!ok) _exit(static_cast<int>(PamIpcExitCode::OtherError));
|
||||
logIf(delegate->log) << "Got response for request.";
|
||||
|
||||
response.resp = strdup(resp.c_str()); // NOLINT (include)
|
||||
}
|
||||
}
|
||||
|
||||
*responseArray = responses;
|
||||
return PAM_SUCCESS;
|
||||
|
||||
fail:
|
||||
free(responseArray); // NOLINT
|
||||
_exit(1);
|
||||
}
|
31
src/services/pam/subprocess.hpp
Normal file
31
src/services/pam/subprocess.hpp
Normal file
|
@ -0,0 +1,31 @@
|
|||
#pragma once
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <security/pam_appl.h>
|
||||
|
||||
#include "ipc.hpp"
|
||||
|
||||
// endls are intentional as it makes debugging much easier when the buffer actually flushes.
|
||||
// NOLINTBEGIN
|
||||
#define logIf(log) \
|
||||
if (log) std::cout << "quickshell.service.pam.subprocess: "
|
||||
// NOLINTEND
|
||||
|
||||
class PamSubprocess {
|
||||
public:
|
||||
explicit PamSubprocess(bool log, int fdIn, int fdOut): log(log), pipes(fdIn, fdOut) {}
|
||||
PamIpcExitCode exec(const char* configDir, const char* config, const char* user);
|
||||
void sendCode(PamIpcExitCode code);
|
||||
|
||||
private:
|
||||
static int conversation(
|
||||
int msgCount,
|
||||
const pam_message** msgArray,
|
||||
pam_response** responseArray,
|
||||
void* appdata
|
||||
);
|
||||
|
||||
bool log;
|
||||
PamIpcPipes pipes;
|
||||
};
|
Loading…
Reference in a new issue