ZaStoGram_desktop/Telegram/SourceFiles/platform/linux/update_install_linux.cpp
loop-uh 0fd5d76a67 Merge upstream v7.3.1
Слияние с tdesktop от 64ca5475f2 до тега v7.3.1 (7.2.10 beta, 7.3, 7.3.1).

- Версия 7.3.1 (7003001), BetaChannel 0, комментарий ZaStoGram сохранён.
- Обновлятор: оригинал убрал пакеты v1 (RSA), а наш Packer и Forgejo
  выпускают именно их. v1 оставлен как UnpackUpdateV1 рядом с v2 и
  использует новые проверенные распаковку и извлечение файлов.
- mtproto: исправления импорта авторизации из оригинала перенесены в нашу
  структуру RequestRegistry; пути include новых файлов кошелька
  приведены к нашей раскладке mtproto/.
- prepare.py: стадии оригинала (ucrt64, ffmpeg 8, openssl 3.5, libheif,
  Rust, wallet-engine) с нашими skip-debug и patches_extra.
- lib_ui, lib_lottie: форки слиты с версиями из v7.3.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:43:47 +03:00

1299 lines
36 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "platform/linux/update_install_linux.h"
#include "base/platform/base_platform_info.h"
#include "base/basic_types.h"
#include "core/update_channel.h"
#include "core/update_keys.h"
#include "core/update_unpack.h"
#include "core/update_verify.h"
#ifndef TDESKTOP_DISABLE_AUTOUPDATE
#include "mtproto/files/dedicated_file_loader.h"
#include <glib/glib.hpp>
#endif // !TDESKTOP_DISABLE_AUTOUPDATE
#include <QtCore/QCryptographicHash>
#include <QtCore/QDateTime>
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
#include <QtCore/QSet>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <sys/wait.h>
#include <algorithm>
#include <array>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <dirent.h>
#include <fcntl.h>
#include <memory>
#include <unistd.h>
#include <utility>
#include <vector>
namespace Platform {
namespace {
[[nodiscard]] int ReportError(const QString &error) {
fprintf(stderr, "Update install: %s\n", error.toUtf8().constData());
fflush(stderr);
return 1;
}
[[nodiscard]] bool RootModeSupported(QString &error) {
#ifdef TDESKTOP_DISABLE_AUTOUPDATE
error = u"Automatic updates are disabled in this build."_q;
return false;
#else // TDESKTOP_DISABLE_AUTOUPDATE
if (Core::BuildUpdateChannel == Core::Updates::Channel::CanaryPrivate) {
error = u"Private-canary builds do not support this mode."_q;
return false;
}
return true;
#endif // !TDESKTOP_DISABLE_AUTOUPDATE
}
#ifndef TDESKTOP_DISABLE_AUTOUPDATE
constexpr auto kDirectoryFlags
= O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC;
constexpr auto kUpdaterPathLimit = 1024;
constexpr auto kCopyBufferSize = 64 * 1024;
constexpr auto kMaxDiagnosticBytes = 1024 * 1024;
constexpr auto kMaxDiagnosticFiles = 64;
constexpr auto kMaxUpdateCleanupDepth = 64;
struct DirectoryCloser {
void operator()(DIR *stream) const {
closedir(stream);
}
};
using DirectoryStream = std::unique_ptr<DIR, DirectoryCloser>;
class FileDescriptor final {
public:
FileDescriptor() = default;
explicit FileDescriptor(int value);
FileDescriptor(FileDescriptor &&other) noexcept;
FileDescriptor &operator=(FileDescriptor &&other) noexcept;
~FileDescriptor();
[[nodiscard]] bool close();
[[nodiscard]] int get() const;
[[nodiscard]] explicit operator bool() const;
private:
int _value = -1;
};
class PrivateDirectory final {
public:
PrivateDirectory() = default;
PrivateDirectory(const PrivateDirectory &) = delete;
PrivateDirectory &operator=(const PrivateDirectory &) = delete;
~PrivateDirectory();
[[nodiscard]] bool create(QString &error);
[[nodiscard]] bool cleanup();
[[nodiscard]] const QString &path() const;
private:
QString _path;
};
struct Installation {
QByteArray directoryPath;
QByteArray executableName;
FileDescriptor directory;
FileDescriptor executable;
};
struct ExpectedFile {
QByteArray path;
QByteArray digest;
off_t size = 0;
mode_t mode = 0;
};
FileDescriptor::FileDescriptor(int value) : _value(value) {
}
FileDescriptor::FileDescriptor(FileDescriptor &&other) noexcept
: _value(std::exchange(other._value, -1)) {
}
FileDescriptor &FileDescriptor::operator=(FileDescriptor &&other) noexcept {
if (this != &other) {
if (_value >= 0) {
::close(_value);
}
_value = std::exchange(other._value, -1);
}
return *this;
}
FileDescriptor::~FileDescriptor() {
if (_value >= 0) {
::close(_value);
}
}
bool FileDescriptor::close() {
return _value < 0 || ::close(std::exchange(_value, -1)) == 0;
}
int FileDescriptor::get() const {
return _value;
}
FileDescriptor::operator bool() const {
return _value >= 0;
}
[[nodiscard]] bool Fail(QString &error, const char *message) {
error = QString::fromLatin1(message);
return false;
}
void ReportProgress(const char *message) {
fprintf(stdout, "Update install: %s\n", message);
fflush(stdout);
}
[[nodiscard]] bool TrustedDirectory(const struct stat &info) {
return S_ISDIR(info.st_mode)
&& info.st_uid == 0
&& !(info.st_mode & (S_IWGRP | S_IWOTH));
}
[[nodiscard]] bool TrustedExecutable(const struct stat &info) {
return S_ISREG(info.st_mode)
&& info.st_uid == 0
&& info.st_nlink > 0
&& (info.st_mode & S_IXUSR)
&& !(info.st_mode & (S_IWGRP | S_IWOTH));
}
[[nodiscard]] FileDescriptor OpenTrustedDirectory(
int parent,
const QByteArray &name,
QString &error) {
auto result = FileDescriptor(openat(
parent,
name.constData(),
kDirectoryFlags));
struct stat info = {};
if (!result
|| fstat(result.get(), &info) != 0
|| !TrustedDirectory(info)) {
error = u"Installation ancestors must be root-owned, unwritable "
u"by other users, and free of symlinks."_q;
return FileDescriptor();
}
return result;
}
[[nodiscard]] bool CurrentExecutableMatches(
const Installation &installation,
QString &error) {
struct stat running = {};
struct stat entry = {};
if (fstat(installation.executable.get(), &running) != 0
|| fstatat(
installation.directory.get(),
installation.executableName.constData(),
&entry,
AT_SYMLINK_NOFOLLOW) != 0
|| !TrustedExecutable(running)
|| !TrustedExecutable(entry)
|| running.st_dev != entry.st_dev
|| running.st_ino != entry.st_ino) {
return Fail(error, "The installed executable is unsafe or was replaced.");
}
return true;
}
[[nodiscard]] bool SafePathComponent(const QByteArray &name) {
if (name.isEmpty() || name == "." || name == "..") {
return false;
}
for (const auto ch : name) {
if (uchar(ch) < 32 || uchar(ch) == 127 || ch == '\\') {
return false;
}
}
return true;
}
[[nodiscard]] QByteArray AsciiLower(QByteArray value) {
for (auto &ch : value) {
if (ch >= 'A' && ch <= 'Z') {
ch += 'a' - 'A';
}
}
return value;
}
[[nodiscard]] bool SafeExecutableName(const QByteArray &name) {
const auto lower = AsciiLower(name);
const auto reserved = { "updater", "ready", "tdata" };
for (const auto word : reserved) {
if (lower == word) {
return false;
}
}
return SafePathComponent(name);
}
[[nodiscard]] std::optional<Installation> OpenInstallation(QString &error) {
auto pathBuffer = std::array<char, kUpdaterPathLimit>();
const auto length = readlink(
"/proc/self/exe",
pathBuffer.data(),
pathBuffer.size());
if (length <= 0 || size_t(length) >= pathBuffer.size()) {
error = u"Could not obtain a complete /proc/self/exe path."_q;
return std::nullopt;
}
const auto path = QByteArray(pathBuffer.data(), length);
if (!path.startsWith('/') || path.endsWith(" (deleted)")) {
error = u"The executable path is unavailable or deleted."_q;
return std::nullopt;
}
const auto slash = path.lastIndexOf('/');
auto result = Installation{
.directoryPath = path.left(slash + 1),
.executableName = path.mid(slash + 1),
};
if (!SafeExecutableName(result.executableName)
|| result.directoryPath.size() + sizeof("Updater") > kUpdaterPathLimit) {
error = u"The installation path is unsafe for the update helper."_q;
return std::nullopt;
}
result.executable = FileDescriptor(open(
"/proc/self/exe",
O_RDONLY | O_CLOEXEC | O_NONBLOCK));
struct stat executable = {};
if (!result.executable
|| fstat(result.executable.get(), &executable) != 0
|| !TrustedExecutable(executable)) {
error = u"The running executable must be root-owned and protected."_q;
return std::nullopt;
}
result.directory = OpenTrustedDirectory(AT_FDCWD, "/", error);
if (!result.directory) {
return std::nullopt;
}
if (slash > 0) {
const auto components = path.mid(1, slash - 1).split('/');
for (const auto &part : components) {
if (!SafePathComponent(part)) {
error = u"The installation path contains an unsafe component."_q;
return std::nullopt;
}
result.directory = OpenTrustedDirectory(
result.directory.get(),
part,
error);
if (!result.directory) {
return std::nullopt;
}
}
}
if (flock(result.directory.get(), LOCK_EX | LOCK_NB) != 0) {
error = u"Could not lock the installation; another update may be running."_q;
return std::nullopt;
}
if (!CurrentExecutableMatches(result, error)) {
return std::nullopt;
}
return result;
}
[[nodiscard]] std::optional<QByteArray> ReadPackage(
const QString &path,
QString &error) {
const auto nativePath = QFile::encodeName(path);
if (QFile::decodeName(nativePath) != path || nativePath.contains('\0')) {
error = u"The package path cannot be represented without loss."_q;
return std::nullopt;
}
const auto file = FileDescriptor(open(
nativePath.constData(),
O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK));
struct stat info = {};
if (!file
|| fstat(file.get(), &info) != 0
|| !S_ISREG(info.st_mode)
|| info.st_size <= 0
|| info.st_size > MTP::AbstractDedicatedLoader::kMaxFileSize) {
error = u"The package must be a nonempty regular file within the "
u"update size limit, without a final symlink."_q;
return std::nullopt;
}
auto result = QByteArray(info.st_size, Qt::Uninitialized);
auto position = qsizetype(0);
while (position < result.size()) {
const auto count = read(
file.get(),
result.data() + position,
result.size() - position);
if (count < 0 && errno == EINTR) {
continue;
} else if (count <= 0) {
error = u"The package could not be read completely."_q;
return std::nullopt;
}
position += count;
}
auto extra = char(0);
auto count = ssize_t(0);
do {
count = read(file.get(), &extra, 1);
} while (count < 0 && errno == EINTR);
if (count != 0) {
error = u"The package grew or could not be read to its end."_q;
return std::nullopt;
}
return result;
}
[[nodiscard]] std::optional<Core::Updates::VerifiedUpdate> VerifyPackage(
const QByteArray &bytes,
bool beta,
QString &error) {
const auto root = Core::Updates::RootPublicKeyPem();
const auto held = Core::Updates::ParseVerifiedManifest(
Core::Updates::EmbeddedManifest(),
Core::Updates::EmbeddedManifestSignature(),
root,
&error);
if (!held) {
return std::nullopt;
}
const auto target = Core::Updates::TargetFromPlatformKey(
AutoUpdateKey().toLatin1());
if (!target) {
error = u"This platform has no supported update target."_q;
return std::nullopt;
}
return Core::Updates::VerifyUpdate(
bytes,
Core::BuildUpdateChannel,
AppBetaVersion || beta,
*target,
Core::RunningUpdateVersion(),
held,
root,
QDateTime::currentSecsSinceEpoch(),
&error);
}
PrivateDirectory::~PrivateDirectory() {
if (!cleanup()) {
fprintf(
stderr,
"Update install: could not remove private staging at %s.\n",
_path.toUtf8().constData());
fflush(stderr);
}
}
bool PrivateDirectory::create(QString &error) {
const auto root = OpenTrustedDirectory(AT_FDCWD, "/", error);
if (!root) {
return false;
}
const auto parent = FileDescriptor(openat(root.get(), "tmp", kDirectoryFlags));
struct stat info = {};
if (!parent
|| fstat(parent.get(), &info) != 0
|| !S_ISDIR(info.st_mode)
|| info.st_uid != 0
|| ((info.st_mode & (S_IWGRP | S_IWOTH))
&& !(info.st_mode & S_ISVTX))) {
return Fail(error, "The fixed /tmp parent is not safely root-owned.");
}
char pattern[] = "/tmp/telegram-update-XXXXXX";
const auto created = mkdtemp(pattern);
if (!created) {
return Fail(error, "Could not create private update staging.");
}
_path = QString::fromLatin1(created);
const auto directory = FileDescriptor(open(created, kDirectoryFlags));
if (!directory
|| fstat(directory.get(), &info) != 0
|| !S_ISDIR(info.st_mode)
|| info.st_uid != 0
|| (info.st_mode & 07777) != 0700) {
return Fail(error, "Private update staging is not root-owned mode 0700.");
}
const auto rootDir = QDir(_path);
if (!rootDir.mkdir(u"tupdates"_q)
|| !rootDir.mkdir(u"tupdates/temp"_q)
|| !rootDir.mkdir(u"DebugLogs"_q)) {
return Fail(error, "Could not create private update directories.");
}
return true;
}
bool PrivateDirectory::cleanup() {
if (_path.isEmpty()) {
return true;
}
if (!QDir(_path).removeRecursively()) {
return false;
}
struct stat info = {};
if (lstat(QFile::encodeName(_path).constData(), &info) == 0
|| errno != ENOENT) {
return false;
}
_path.clear();
return true;
}
const QString &PrivateDirectory::path() const {
return _path;
}
[[nodiscard]] bool WriteReady(const QString &directory, QString &error) {
auto file = QFile(directory + u"/ready"_q);
if (!file.open(QIODevice::WriteOnly | QIODevice::NewOnly)) {
return Fail(error, "Could not create the reserved update ready marker.");
}
if (file.write("1", 1) != 1 || !file.flush()) {
return Fail(error, "Could not write the private update ready marker.");
}
file.close();
if (file.error() != QFileDevice::NoError) {
return Fail(error, "Could not close the private update ready marker.");
}
return true;
}
[[nodiscard]] DirectoryStream OpenDirectoryStream(int directory, QString &error) {
const auto copy = openat(directory, ".", kDirectoryFlags);
auto stream = static_cast<DIR*>(nullptr);
const auto guard = gsl::finally([&] {
if (copy >= 0 && !stream) {
close(copy);
}
});
if (copy >= 0) {
stream = fdopendir(copy);
}
if (!stream) {
error = u"Could not enumerate a protected update directory."_q;
}
return DirectoryStream(stream);
}
[[nodiscard]] bool RemoveUpdateEntry(
int directory,
const QByteArray &name,
int depth,
QString &error) {
struct stat info = {};
if (fstatat(directory, name.constData(), &info, AT_SYMLINK_NOFOLLOW)) {
return errno == ENOENT
|| Fail(error, "Could not inspect pending update data.");
}
if (S_ISDIR(info.st_mode)) {
if (depth >= kMaxUpdateCleanupDepth) {
return Fail(error, "Pending update data exceeds the cleanup depth limit.");
}
const auto child = FileDescriptor(openat(
directory,
name.constData(),
kDirectoryFlags));
if (!child) {
return Fail(error, "Could not open pending update data safely.");
}
const auto stream = OpenDirectoryStream(child.get(), error);
if (!stream) {
return false;
}
while (true) {
errno = 0;
const auto entry = readdir(stream.get());
if (!entry) {
if (errno) {
return Fail(error, "Could not enumerate pending update data.");
}
break;
}
const auto childName = QByteArray(entry->d_name);
if (childName != "." && childName != ".."
&& !RemoveUpdateEntry(
child.get(),
childName,
depth + 1,
error)) {
return false;
}
}
}
if (unlinkat(
directory,
name.constData(),
S_ISDIR(info.st_mode) ? AT_REMOVEDIR : 0)) {
return Fail(error, "Could not remove pending update data.");
}
return true;
}
[[nodiscard]] bool RemoveOtherUpdates(
int directory,
const QByteArray &keep,
QString &error) {
const auto stream = OpenDirectoryStream(directory, error);
if (!stream) {
return false;
}
while (true) {
errno = 0;
const auto entry = readdir(stream.get());
if (!entry) {
return !errno || Fail(error, "Could not enumerate pending updates.");
}
const auto name = QByteArray(entry->d_name);
if (name != "." && name != ".." && name != keep
&& !RemoveUpdateEntry(directory, name, 1, error)) {
return false;
}
}
}
[[nodiscard]] bool TrustedFile(const struct stat &info) {
return S_ISREG(info.st_mode)
&& info.st_uid == 0
&& !(info.st_mode & (S_IWGRP | S_IWOTH));
}
[[nodiscard]] QByteArray FileDigest(int file, off_t size, QString &error) {
auto hash = QCryptographicHash(QCryptographicHash::Sha256);
auto buffer = std::array<char, kCopyBufferSize>();
while (size > 0) {
const auto count = read(
file,
buffer.data(),
std::min<off_t>(size, buffer.size()));
if (count < 0 && errno == EINTR) {
continue;
} else if (count <= 0) {
error = u"Could not read a complete installed or staged file."_q;
return QByteArray();
}
hash.addData(QByteArrayView(buffer.data(), count));
size -= count;
}
auto extra = char(0);
auto count = ssize_t(0);
do {
count = read(file, &extra, 1);
} while (count < 0 && errno == EINTR);
if (count != 0) {
error = u"An installed or staged file changed while being read."_q;
return QByteArray();
}
return hash.result();
}
[[nodiscard]] bool InventoryDirectory(
int directory,
const QByteArray &prefix,
const QByteArray &executableName,
std::vector<ExpectedFile> &files,
QSet<QByteArray> &destinations,
QString &error) {
const auto stream = OpenDirectoryStream(directory, error);
if (!stream) {
return false;
}
while (true) {
errno = 0;
const auto entry = readdir(stream.get());
if (!entry) {
return !errno || Fail(error, "Could not finish staged inventory.");
}
const auto name = QByteArray(entry->d_name);
if (name == "." || name == "..") {
continue;
} else if (!SafePathComponent(name)) {
return Fail(error, "Unsafe staged filename.");
}
const auto relative = prefix + name;
if (prefix.isEmpty()) {
const auto reserved = { "Telegram", "Updater", "ready", "tdata" };
for (const auto word : reserved) {
if (AsciiLower(name) == AsciiLower(word) && name != word) {
return Fail(error, "A staged name aliases a reserved filename.");
}
}
}
struct stat info = {};
if (fstatat(directory, name.constData(), &info, AT_SYMLINK_NOFOLLOW)) {
return Fail(error, "Could not inspect a staged entry.");
}
const auto skipped = relative == "ready"
|| relative == "tdata"
|| relative.startsWith("tdata/");
const auto target = (relative == "Telegram") ? executableName : relative;
if (!skipped) {
const auto key = AsciiLower(target);
if (destinations.contains(key)) {
return Fail(error, "Staged entries map to conflicting destinations.");
}
destinations.insert(key);
}
if (S_ISDIR(info.st_mode)) {
if (relative == "Telegram" || relative == "Updater"
|| relative == "ready") {
return Fail(error, "A required staged file is a directory.");
}
const auto child = OpenTrustedDirectory(directory, name, error);
if (!child || !InventoryDirectory(
child.get(),
relative + '/',
executableName,
files,
destinations,
error)) {
return false;
}
continue;
}
const auto mode = info.st_mode & 07777;
if (!TrustedFile(info)
|| info.st_size < 0
|| (mode != 0644 && mode != 0755)
|| ((relative == "Telegram" || relative == "Updater")
&& mode != 0755)) {
return Fail(error, "A staged file has an unsafe type, owner or mode.");
} else if (skipped) {
continue;
}
const auto file = FileDescriptor(openat(
directory,
name.constData(),
O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK));
struct stat opened = {};
if (!file || fstat(file.get(), &opened)
|| opened.st_dev != info.st_dev || opened.st_ino != info.st_ino) {
return Fail(error, "Could not open the inspected staged file.");
}
auto digest = FileDigest(file.get(), info.st_size, error);
if (digest.isEmpty()) {
return false;
}
files.push_back({ target, std::move(digest), info.st_size, mode });
}
}
[[nodiscard]] bool PreflightDestination(
int installation,
const QByteArray &path,
QString &error) {
auto parent = OpenTrustedDirectory(installation, ".", error);
if (!parent) {
return false;
}
const auto parts = path.split('/');
for (auto i = 0; i != parts.size(); ++i) {
struct stat info = {};
if (fstatat(
parent.get(),
parts[i].constData(),
&info,
AT_SYMLINK_NOFOLLOW)) {
return errno == ENOENT
|| Fail(error, "Could not inspect an installation destination.");
} else if (i + 1 == parts.size()) {
return TrustedFile(info)
|| Fail(error, "An installation target is not a protected file.");
}
parent = OpenTrustedDirectory(parent.get(), parts[i], error);
if (!parent) {
return false;
}
}
return false;
}
[[nodiscard]] bool FileMatches(
int file,
const ExpectedFile &expected,
QString &error) {
struct stat info = {};
if (fstat(file, &info)
|| !TrustedFile(info)
|| info.st_size != expected.size
|| (info.st_mode & 07777) != expected.mode) {
return Fail(error, "An update file has the wrong size, owner or mode.");
}
const auto digest = FileDigest(file, expected.size, error);
if (digest.isEmpty()) {
return false;
} else if (digest != expected.digest) {
return Fail(error, "An update file does not match the authenticated bytes.");
}
return true;
}
[[nodiscard]] bool InstalledFileMatches(
int installation,
const ExpectedFile &expected,
QString &error) {
auto parent = OpenTrustedDirectory(installation, ".", error);
const auto parts = expected.path.split('/');
for (auto i = 0; parent && i + 1 < parts.size(); ++i) {
parent = OpenTrustedDirectory(parent.get(), parts[i], error);
}
if (!parent) {
return false;
}
const auto file = FileDescriptor(openat(
parent.get(),
parts.back().constData(),
O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK));
if (!file) {
return Fail(error, "Could not open an expected installed file safely.");
}
return FileMatches(file.get(), expected, error);
}
[[nodiscard]] bool CopyFile(int source, int destination, off_t size) {
auto buffer = std::array<char, kCopyBufferSize>();
while (size > 0) {
const auto count = read(
source,
buffer.data(),
std::min<off_t>(size, buffer.size()));
if (count < 0 && errno == EINTR) {
continue;
} else if (count <= 0) {
return false;
}
for (auto offset = ssize_t(0); offset != count;) {
const auto written = write(
destination,
buffer.data() + offset,
count - offset);
if (written < 0 && errno == EINTR) {
continue;
} else if (written <= 0) {
return false;
}
offset += written;
}
size -= count;
}
return true;
}
[[nodiscard]] bool InstallUpdater(
const Installation &installation,
int staged,
const ExpectedFile &expected,
bool &changed,
QString &error) {
auto source = FileDescriptor(openat(
staged,
"Updater",
O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK));
if (!source) {
return Fail(error, "Could not open the authenticated update helper.");
} else if (!FileMatches(source.get(), expected, error)) {
return false;
} else if (lseek(source.get(), 0, SEEK_SET) != 0) {
return Fail(error, "Could not rewind the authenticated update helper.");
}
const auto directory = installation.directory.get();
if (!PreflightDestination(directory, "Updater", error)
|| !CurrentExecutableMatches(installation, error)) {
return false;
}
if (unlinkat(directory, "Updater", 0) == 0) {
changed = true;
} else if (errno != ENOENT) {
return Fail(error, "Could not remove the old installed update helper.");
}
auto destination = FileDescriptor(openat(
directory,
"Updater",
O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC,
0600));
if (!destination) {
return Fail(error, "Could not create the installed update helper.");
}
changed = true;
if (!CopyFile(source.get(), destination.get(), expected.size)
|| fchmod(destination.get(), expected.mode)
|| fsync(destination.get())) {
return Fail(error, "Could not completely write the installed update helper.");
}
const auto sourceClosed = source.close();
const auto destinationClosed = destination.close();
if (!sourceClosed || !destinationClosed) {
return Fail(error, "Could not close the copied update helper.");
} else if (!InstalledFileMatches(directory, expected, error)) {
return false;
} else if (unlinkat(staged, "Updater", 0)) {
return Fail(error, "Could not remove the staged update helper.");
}
return true;
}
[[nodiscard]] bool RelayUpdaterLogs(const QString &staging, QString &error) {
const auto path = QFile::encodeName(staging + u"/DebugLogs"_q);
const auto directory = OpenTrustedDirectory(AT_FDCWD, path, error);
if (!directory) {
return false;
}
const auto stream = OpenDirectoryStream(directory.get(), error);
if (!stream) {
return false;
}
auto remaining = off_t(kMaxDiagnosticBytes);
auto files = 0;
auto buffer = std::array<char, kCopyBufferSize>();
while (true) {
errno = 0;
const auto entry = readdir(stream.get());
if (!entry) {
return !errno || Fail(error, "Could not finish reading Updater logs.");
}
const auto name = QByteArray(entry->d_name);
if (name == "." || name == "..") {
continue;
} else if (!remaining || files++ == kMaxDiagnosticFiles) {
ReportProgress("Updater diagnostics truncated at the output limit.");
return true;
}
const auto file = FileDescriptor(openat(
directory.get(),
name.constData(),
O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK));
struct stat info = {};
if (!file || fstat(file.get(), &info) || !TrustedFile(info)
|| info.st_size < 0) {
return Fail(error, "Could not read a protected regular Updater log.");
}
auto left = std::min(remaining, info.st_size);
const auto truncated = left < info.st_size;
while (left > 0) {
const auto count = read(
file.get(),
buffer.data(),
std::min<off_t>(left, buffer.size()));
if (count < 0 && errno == EINTR) {
continue;
} else if (count <= 0) {
return Fail(error, "Could not read the complete Updater log.");
} else if (fwrite(buffer.data(), 1, count, stdout) != size_t(count)) {
return Fail(error, "Could not forward Updater diagnostics.");
}
left -= count;
remaining -= count;
}
if (fflush(stdout)) {
return Fail(error, "Could not flush Updater diagnostics.");
} else if (truncated) {
ReportProgress("Updater diagnostics truncated at the output limit.");
return true;
}
}
}
[[nodiscard]] bool RunUpdater(
const Installation &installation,
const QString &staging,
QString &error) {
namespace GLib = gi::repository::GLib;
const auto arguments = std::vector<std::string>{
(installation.directoryPath + "Updater").toStdString(),
"-debug",
"-justupdate",
"-writeprotected",
"-workpath",
QFile::encodeName(staging + '/').toStdString(),
"-exepath",
installation.directoryPath.toStdString(),
"-exename",
installation.executableName.toStdString(),
};
const auto environment = std::vector<std::string>{
"LANG=C",
"LC_ALL=C",
"TZ=UTC0",
};
auto status = 0;
const auto spawned = GLib::spawn_sync(
installation.directoryPath.toStdString(),
arguments,
environment,
GLib::SpawnFlags::DEFAULT_,
nullptr,
nullptr,
nullptr,
&status,
nullptr);
const auto relayed = RelayUpdaterLogs(staging, error);
if (!spawned) {
return Fail(error, "Could not execute the installed update helper.");
} else if (WIFSIGNALED(status)) {
error = u"The update helper was terminated by signal %1."_q
.arg(WTERMSIG(status));
return false;
} else if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
error = u"The update helper failed (wait status %1)."_q.arg(status);
return false;
}
return relayed;
}
[[nodiscard]] int InstallUpdate(const QString &packagePath, bool beta) {
auto error = QString();
const auto installation = OpenInstallation(error);
if (!installation) {
return ReportError(error);
}
ReportProgress("Reading the update package.");
auto bytes = ReadPackage(packagePath, error);
if (!bytes) {
return ReportError(error);
}
ReportProgress("Verifying the update package.");
auto verified = VerifyPackage(*bytes, beta, error);
bytes.reset();
if (!verified) {
return ReportError(error);
}
const auto previousMask = umask(022);
const auto restoreMask = gsl::finally([=] { umask(previousMask); });
auto staging = PrivateDirectory();
if (!staging.create(error)) {
return ReportError(error);
}
ReportProgress("Extracting the authenticated package into private staging.");
const auto version = verified->envelope.version;
auto uncompressed = Core::Updates::DecompressUpdatePayload(*verified, &error);
verified.reset();
if (!uncompressed) {
return ReportError(error);
}
const auto temp = staging.path() + u"/tupdates/temp"_q;
if (!Core::Updates::ExtractUpdateFiles(*uncompressed, version, temp, &error)
|| !WriteReady(temp, error)) {
return ReportError(error);
}
uncompressed.reset();
const auto staged = OpenTrustedDirectory(
AT_FDCWD,
QFile::encodeName(temp),
error);
if (!staged) {
return ReportError(error);
}
for (const auto required : { "Telegram", "Updater" }) {
struct stat info = {};
if (fstatat(staged.get(), required, &info, AT_SYMLINK_NOFOLLOW)
|| !TrustedExecutable(info)) {
return ReportError(u"The package requires exact top-level Telegram "
u"and Updater executables."_q);
}
}
ReportProgress("Checking authenticated files and installation destinations.");
auto files = std::vector<ExpectedFile>();
auto destinations = QSet<QByteArray>();
if (!InventoryDirectory(
staged.get(),
QByteArray(),
installation->executableName,
files,
destinations,
error)) {
return ReportError(error);
}
for (const auto &file : files) {
if (!PreflightDestination(installation->directory.get(), file.path, error)) {
return ReportError(error);
}
}
destinations.clear();
const auto updater = std::ranges::find(
files,
QByteArray("Updater"),
&ExpectedFile::path);
if (updater == files.end()) {
return ReportError(u"The authenticated update helper is missing."_q);
}
auto changed = false;
const auto failed = [&] {
if (changed) {
error += u" Installation may be incomplete."_q;
}
return ReportError(error);
};
ReportProgress("Installing the authenticated update helper.");
if (!InstallUpdater(*installation, staged.get(), *updater, changed, error)) {
return failed();
}
ReportProgress("Running the installed update helper.");
if (!RunUpdater(*installation, staging.path(), error)) {
return failed();
}
ReportProgress("Verifying all installed files against the package.");
for (const auto &file : files) {
if (!InstalledFileMatches(installation->directory.get(), file, error)) {
error = u"Installed file %1: %2"_q
.arg(QFile::decodeName(file.path), error);
return failed();
}
}
if (!staging.cleanup()) {
return ReportError(u"Files installed, but private staging cleanup failed."_q);
}
ReportProgress("Update installed and private staging removed.");
return 0;
}
#endif // !TDESKTOP_DISABLE_AUTOUPDATE
} // namespace
UpdateInstallMode GetUpdateInstallMode(
const QString &executablePath,
QString &error) {
#ifndef TDESKTOP_DISABLE_AUTOUPDATE
const auto native = QFile::encodeName(executablePath);
const auto slash = native.lastIndexOf('/');
if (!native.startsWith('/')
|| native.contains('\0')
|| QFile::decodeName(native) != executablePath
|| !SafeExecutableName(native.mid(slash + 1))) {
error = u"The update destination path is invalid."_q;
return UpdateInstallMode::Refused;
}
const auto directory = native.left(slash + 1);
struct stat parent = {};
struct statvfs filesystem = {};
if (stat(directory.constData(), &parent)
|| !S_ISDIR(parent.st_mode)
|| statvfs(directory.constData(), &filesystem)
|| (filesystem.f_flag & ST_RDONLY)) {
error = u"The update destination is unavailable or read-only."_q;
return UpdateInstallMode::Refused;
}
auto writable = faccessat(
AT_FDCWD,
directory.constData(),
W_OK | X_OK,
AT_EACCESS) == 0;
if (!writable && errno != EACCES) {
error = u"Could not check update replacement permissions."_q;
return UpdateInstallMode::Refused;
}
const auto user = geteuid();
const auto names = { native.mid(slash + 1), QByteArray("Updater") };
for (const auto &name : names) {
struct stat target = {};
if (lstat((directory + name).constData(), &target)) {
if (errno == ENOENT && name == "Updater") {
continue;
}
error = u"Could not inspect an update replacement target."_q;
return UpdateInstallMode::Refused;
} else if (!S_ISREG(target.st_mode)) {
error = u"Update replacement targets must be regular files."_q;
return UpdateInstallMode::Refused;
}
if ((parent.st_mode & S_ISVTX)
&& user != 0
&& user != parent.st_uid
&& user != target.st_uid) {
writable = false;
}
}
if (writable) {
return UpdateInstallMode::Writable;
} else if (!RootModeSupported(error)) {
return UpdateInstallMode::Refused;
}
const auto installation = OpenInstallation(error);
if (!installation
|| !PreflightDestination(installation->directory.get(), "Updater", error)) {
return UpdateInstallMode::Refused;
} else if (installation->directoryPath + installation->executableName
!= native) {
error = u"The update destination is not the running installed image."_q;
return UpdateInstallMode::Refused;
}
return UpdateInstallMode::Protected;
#else // !TDESKTOP_DISABLE_AUTOUPDATE
error = u"Automatic updates are disabled in this build."_q;
return UpdateInstallMode::Refused;
#endif // TDESKTOP_DISABLE_AUTOUPDATE
}
QString ReadyUpdatePackagePath(const QString &workingDirectory) {
return QDir(workingDirectory).absoluteFilePath(u"tupdates/ready-package"_q);
}
bool ReadyUpdatePackagePresent(const QString &workingDirectory) {
struct stat info = {};
return !lstat(
QFile::encodeName(ReadyUpdatePackagePath(workingDirectory)).constData(),
&info) || errno != ENOENT;
}
std::optional<QByteArray> ReadUpdatePackage(
const QString &path,
QString &error) {
#ifndef TDESKTOP_DISABLE_AUTOUPDATE
return ReadPackage(path, error);
#else // !TDESKTOP_DISABLE_AUTOUPDATE
error = u"Automatic updates are disabled in this build."_q;
return std::nullopt;
#endif // TDESKTOP_DISABLE_AUTOUPDATE
}
std::optional<QString> ValidateProtectedUpdate(
const QByteArray &bytes,
bool beta,
QString &error) {
if (!RootModeSupported(error)) {
return std::nullopt;
}
#ifndef TDESKTOP_DISABLE_AUTOUPDATE
if (!VerifyPackage(bytes, beta, error)) {
return std::nullopt;
}
const auto installation = OpenInstallation(error);
if (!installation
|| !PreflightDestination(installation->directory.get(), "Updater", error)) {
return std::nullopt;
}
return QFile::decodeName(
installation->directoryPath + installation->executableName);
#else // !TDESKTOP_DISABLE_AUTOUPDATE
return std::nullopt;
#endif // TDESKTOP_DISABLE_AUTOUPDATE
}
bool PrepareProtectedUpdate(
const QString &workingDirectory,
const QString &path,
const QByteArray &bytes,
bool beta,
QString &error) {
#ifndef TDESKTOP_DISABLE_AUTOUPDATE
if (!ValidateProtectedUpdate(bytes, beta, error)) {
return false;
}
const auto ready = ReadyUpdatePackagePath(workingDirectory);
const auto parent = QFileInfo(ready).absolutePath();
const auto name = QFile::encodeName(QFileInfo(path).fileName());
if (QFileInfo(path).absolutePath() != parent
|| !SafePathComponent(name)
|| QFile::decodeName(name) != QFileInfo(path).fileName()) {
return Fail(error, "The downloaded package is outside pending updates.");
}
const auto directory = FileDescriptor(open(
QFile::encodeName(parent).constData(),
kDirectoryFlags));
if (!directory) {
return Fail(error, "Could not open the pending updates directory safely.");
} else if (!RemoveOtherUpdates(directory.get(), name, error)) {
return false;
} else if (renameat(
directory.get(),
name.constData(),
directory.get(),
"ready-package")) {
return Fail(error, "Could not publish the verified update package.");
}
return true;
#else // !TDESKTOP_DISABLE_AUTOUPDATE
error = u"Automatic updates are disabled in this build."_q;
return false;
#endif // TDESKTOP_DISABLE_AUTOUPDATE
}
bool ClearUpdateData(
const QString &workingDirectory,
QString &error,
bool keepPackage) {
#ifndef TDESKTOP_DISABLE_AUTOUPDATE
const auto parent = FileDescriptor(open(
QFile::encodeName(workingDirectory).constData(),
O_RDONLY | O_DIRECTORY | O_CLOEXEC));
if (!parent) {
return Fail(error, "Could not open the update working directory.");
} else if (!keepPackage) {
return RemoveUpdateEntry(parent.get(), "tupdates", 0, error);
}
const auto directory = FileDescriptor(openat(
parent.get(),
"tupdates",
kDirectoryFlags));
if (!directory) {
return Fail(error, "Could not open the pending updates directory safely.");
}
return RemoveOtherUpdates(directory.get(), "ready-package", error);
#else // !TDESKTOP_DISABLE_AUTOUPDATE
error = u"Automatic updates are disabled in this build."_q;
return false;
#endif // TDESKTOP_DISABLE_AUTOUPDATE
}
std::optional<int> InstallUpdateIfRequested(const QStringList &arguments) {
auto requested = false;
for (auto i = 1; i < arguments.size(); ++i) {
if (arguments[i] == u"-installupdate"_q) {
requested = true;
break;
}
}
if (!requested) {
return std::nullopt;
}
if ((arguments.size() != 3 && arguments.size() != 4)
|| arguments[1] != u"-installupdate"_q
|| !arguments[2].startsWith('/')
|| arguments[2].contains(QChar(0))
|| (arguments.size() == 4 && arguments[3] != u"-beta"_q)) {
return ReportError(u"Usage: -installupdate <absolute-package-path> "
u"[-beta]. No other arguments are accepted."_q);
}
auto error = QString();
if (!RootModeSupported(error)) {
return ReportError(error);
}
#ifndef TDESKTOP_DISABLE_AUTOUPDATE
if (getuid() != 0 || geteuid() != 0) {
return ReportError(u"Update installation requires root."_q);
}
return InstallUpdate(arguments[2], arguments.size() == 4);
#else // !TDESKTOP_DISABLE_AUTOUPDATE
return ReportError(error);
#endif // !TDESKTOP_DISABLE_AUTOUPDATE
}
} // namespace Platform