security: remove setuid root from privileged helpers

This commit is contained in:
specialworld83 2026-07-27 22:54:14 +02:00
commit e99e386f4e
27 changed files with 994 additions and 645 deletions

View file

@ -542,8 +542,8 @@ target_include_directories(BastionGuard
)
target_compile_definitions(BastionGuard PRIVATE
BASTIONGUARD_VERSION="2.0.1"
BASTIONGUARD_BUILD=20260726
BASTIONGUARD_VERSION="2.0.2"
BASTIONGUARD_BUILD=20260727
)
target_link_libraries(BastionGuard
@ -1111,29 +1111,9 @@ install(TARGETS bastionguard-privhelper
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE)
install(CODE "
message(STATUS \"[privhelper] Imposto owner root e setuid su bastionguard-privhelper\")
# Security: never install this helper setuid. Privileged execution is requested
# explicitly by PrivacyPage through pkexec.
execute_process(
COMMAND chown root:root /usr/bin/bastionguard-privhelper
RESULT_VARIABLE CHOWN_RES
)
if(NOT CHOWN_RES EQUAL 0)
message(FATAL_ERROR \"[privhelper] chown root fallito\")
endif()
execute_process(
COMMAND chmod 4755 /usr/bin/bastionguard-privhelper
RESULT_VARIABLE CHMOD_RES
)
if(NOT CHMOD_RES EQUAL 0)
message(FATAL_ERROR \"[privhelper] chmod 4755 fallito\")
endif()
message(STATUS \"[privhelper] ✔ bastionguard-privhelper installato come setuid root\")
")
# ======================
# Helper privilegiato — bastionguard-firewall
@ -1157,29 +1137,9 @@ install(TARGETS bastionguard-firewall
WORLD_READ WORLD_EXECUTE
)
install(CODE "
message(STATUS \"[firewall-helper] Imposto owner root e setuid su bastionguard-firewall\")
# Security: never install this helper setuid. Backend invokes it through pkexec
# and supplies a validated, per-request temporary input file.
execute_process(
COMMAND chown root:root /usr/bin/bastionguard-firewall
RESULT_VARIABLE CHOWN_RES
)
if(NOT CHOWN_RES EQUAL 0)
message(FATAL_ERROR \"[firewall-helper] chown root fallito\")
endif()
execute_process(
COMMAND chmod 4755 /usr/bin/bastionguard-firewall
RESULT_VARIABLE CHMOD_RES
)
if(NOT CHMOD_RES EQUAL 0)
message(FATAL_ERROR \"[firewall-helper] chmod 4755 fallito\")
endif()
message(STATUS \"[firewall-helper] ✔ bastionguard-firewall installato come setuid root\")
")

View file

@ -2,7 +2,7 @@
# Maintainer: BastionGuard <info@bastionguard.eu>
pkgname=bastionguard
pkgver=2.0.1
pkgver=2.0.2
pkgrel=0
pkgdesc="Transparent security control plane for Linux desktops"
url="https://bastionguard.eu/"

View file

@ -1,2 +1,2 @@
version=2.0.1
build=20260726
version=2.0.2
build=20260727

View file

@ -11,20 +11,7 @@ set -e
case "$1" in
configure|abort-upgrade|abort-deconfigure|abort-remove)
# ------------------------------------------------------------
# 1) setuid helpers via dpkg-statoverride (preferred)
# ------------------------------------------------------------
if command -v dpkg-statoverride >/dev/null 2>&1; then
if [ -e /usr/bin/bastionguard-privhelper ]; then
dpkg-statoverride --list /usr/bin/bastionguard-privhelper >/dev/null 2>&1 \
|| dpkg-statoverride --add root root 4755 /usr/bin/bastionguard-privhelper || true
fi
if [ -e /usr/bin/bastionguard-firewall ]; then
dpkg-statoverride --list /usr/bin/bastionguard-firewall >/dev/null 2>&1 \
|| dpkg-statoverride --add root root 4755 /usr/bin/bastionguard-firewall || true
fi
fi
# ------------------------------------------------------------
# 2) CEF sandbox permissions (if installed)

View file

@ -672,8 +672,8 @@ target_include_directories(BastionGuard
)
target_compile_definitions(BastionGuard PRIVATE
BASTIONGUARD_VERSION="2.0.1"
BASTIONGUARD_BUILD=20260726
BASTIONGUARD_VERSION="2.0.2"
BASTIONGUARD_BUILD=20260727
)
target_link_libraries(BastionGuard
@ -1201,29 +1201,6 @@ install(TARGETS bastionguard-privhelper
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE)
bg_install_code( "
message(STATUS \"[privhelper] Imposto owner root e setuid su bastionguard-privhelper\")
execute_process(
COMMAND chown root:root /usr/bin/bastionguard-privhelper
RESULT_VARIABLE CHOWN_RES
)
if(NOT CHOWN_RES EQUAL 0)
message(FATAL_ERROR \"[privhelper] chown root fallito\")
endif()
execute_process(
COMMAND chmod 4755 /usr/bin/bastionguard-privhelper
RESULT_VARIABLE CHMOD_RES
)
if(NOT CHMOD_RES EQUAL 0)
message(FATAL_ERROR \"[privhelper] chmod 4755 fallito\")
endif()
message(STATUS \"[privhelper] ✔ bastionguard-privhelper installato come setuid root\")
")
# ======================
# Helper privilegiato — bastionguard-firewall
@ -1247,29 +1224,6 @@ install(TARGETS bastionguard-firewall
WORLD_READ WORLD_EXECUTE
)
bg_install_code( "
message(STATUS \"[firewall-helper] Imposto owner root e setuid su bastionguard-firewall\")
execute_process(
COMMAND chown root:root /usr/bin/bastionguard-firewall
RESULT_VARIABLE CHOWN_RES
)
if(NOT CHOWN_RES EQUAL 0)
message(FATAL_ERROR \"[firewall-helper] chown root fallito\")
endif()
execute_process(
COMMAND chmod 4755 /usr/bin/bastionguard-firewall
RESULT_VARIABLE CHMOD_RES
)
if(NOT CHMOD_RES EQUAL 0)
message(FATAL_ERROR \"[firewall-helper] chmod 4755 fallito\")
endif()
message(STATUS \"[firewall-helper] ✔ bastionguard-firewall installato come setuid root\")
")
# ======================

2
debian/changelog vendored
View file

@ -1,4 +1,4 @@
bastionguard (2.0.1-1) stable; urgency=low
bastionguard (2.0.2-2) stable; urgency=low
* Debian package.

View file

@ -1,4 +1,4 @@
bastionguard (2.0.1-1ubuntu24.04.1) noble; urgency=low
bastionguard (2.0.2-1ubuntu24.04.1) noble; urgency=low
* Ubuntu 24.04 (Noble) package.

View file

@ -1,4 +1,4 @@
bastionguard (2.0.1-1ubuntu25.10) questing; urgency=low
bastionguard (2.0.2-1ubuntu25.10) questing; urgency=low
* Ubuntu 25.10 (Questing Quokka) package.

View file

@ -1,4 +1,4 @@
bastionguard (2.0.1-1ubuntu26.04) questing; urgency=low
bastionguard (2.0.2-1ubuntu26.04) questing; urgency=low
* Ubuntu 26.04 (Resolute Raccoon) package.

View file

@ -19,7 +19,7 @@
#
Name: bastionguard
Version: 2.0.1
Version: 2.0.2
Release: 1%{?dist}
Summary: BastionGuard Security Platform

View file

@ -736,8 +736,8 @@ target_include_directories(BastionGuard
target_compile_definitions(BastionGuard PRIVATE
BASTIONGUARD_VERSION="2.0.1"
BASTIONGUARD_BUILD=20260726
BASTIONGUARD_VERSION="2.0.2"
BASTIONGUARD_BUILD=20260727
)
target_link_libraries(BastionGuard
@ -1265,29 +1265,6 @@ install(TARGETS bastionguard-privhelper
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE)
bg_install_code( "
message(STATUS \"[privhelper] Imposto owner root e setuid su bastionguard-privhelper\")
execute_process(
COMMAND chown root:root /usr/bin/bastionguard-privhelper
RESULT_VARIABLE CHOWN_RES
)
if(NOT CHOWN_RES EQUAL 0)
message(FATAL_ERROR \"[privhelper] chown root fallito\")
endif()
execute_process(
COMMAND chmod 4755 /usr/bin/bastionguard-privhelper
RESULT_VARIABLE CHMOD_RES
)
if(NOT CHMOD_RES EQUAL 0)
message(FATAL_ERROR \"[privhelper] chmod 4755 fallito\")
endif()
message(STATUS \"[privhelper] ✔ bastionguard-privhelper installato come setuid root\")
")
# ======================
# Helper privilegiato — bastionguard-firewall
@ -1311,29 +1288,6 @@ install(TARGETS bastionguard-firewall
WORLD_READ WORLD_EXECUTE
)
bg_install_code( "
message(STATUS \"[firewall-helper] Imposto owner root e setuid su bastionguard-firewall\")
execute_process(
COMMAND chown root:root /usr/bin/bastionguard-firewall
RESULT_VARIABLE CHOWN_RES
)
if(NOT CHOWN_RES EQUAL 0)
message(FATAL_ERROR \"[firewall-helper] chown root fallito\")
endif()
execute_process(
COMMAND chmod 4755 /usr/bin/bastionguard-firewall
RESULT_VARIABLE CHMOD_RES
)
if(NOT CHMOD_RES EQUAL 0)
message(FATAL_ERROR \"[firewall-helper] chmod 4755 fallito\")
endif()
message(STATUS \"[firewall-helper] ✔ bastionguard-firewall installato come setuid root\")
")
# ======================

View file

@ -19,7 +19,7 @@
#
Name: bastionguard
Version: 2.0.1
Version: 2.0.2
Release: %mkrel 1
%global yara_version 4.5.5
%global yara_stage %{_builddir}/%{name}-%{version}/.yara-stage

View file

@ -757,8 +757,8 @@ target_include_directories(BastionGuard
target_compile_definitions(BastionGuard PRIVATE
BASTIONGUARD_VERSION="2.0.1"
BASTIONGUARD_BUILD=20260726
BASTIONGUARD_VERSION="2.0.2"
BASTIONGUARD_BUILD=20260727
)
target_link_libraries(BastionGuard
@ -1286,29 +1286,6 @@ install(TARGETS bastionguard-privhelper
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE)
bg_install_code( "
message(STATUS \"[privhelper] Imposto owner root e setuid su bastionguard-privhelper\")
execute_process(
COMMAND chown root:root /usr/bin/bastionguard-privhelper
RESULT_VARIABLE CHOWN_RES
)
if(NOT CHOWN_RES EQUAL 0)
message(FATAL_ERROR \"[privhelper] chown root fallito\")
endif()
execute_process(
COMMAND chmod 4755 /usr/bin/bastionguard-privhelper
RESULT_VARIABLE CHMOD_RES
)
if(NOT CHMOD_RES EQUAL 0)
message(FATAL_ERROR \"[privhelper] chmod 4755 fallito\")
endif()
message(STATUS \"[privhelper] ✔ bastionguard-privhelper installato come setuid root\")
")
# ======================
# Helper privilegiato — bastionguard-firewall
@ -1332,29 +1309,6 @@ install(TARGETS bastionguard-firewall
WORLD_READ WORLD_EXECUTE
)
bg_install_code( "
message(STATUS \"[firewall-helper] Imposto owner root e setuid su bastionguard-firewall\")
execute_process(
COMMAND chown root:root /usr/bin/bastionguard-firewall
RESULT_VARIABLE CHOWN_RES
)
if(NOT CHOWN_RES EQUAL 0)
message(FATAL_ERROR \"[firewall-helper] chown root fallito\")
endif()
execute_process(
COMMAND chmod 4755 /usr/bin/bastionguard-firewall
RESULT_VARIABLE CHMOD_RES
)
if(NOT CHMOD_RES EQUAL 0)
message(FATAL_ERROR \"[firewall-helper] chmod 4755 fallito\")
endif()
message(STATUS \"[firewall-helper] ✔ bastionguard-firewall installato come setuid root\")
")
# ======================

View file

@ -20,7 +20,7 @@
Name: bastionguard
Version: 2.0.1
Version: 2.0.2
Release: 1
%global yara_version 4.5.5

View file

@ -763,8 +763,8 @@ target_include_directories(BastionGuard
target_compile_definitions(BastionGuard PRIVATE
BASTIONGUARD_VERSION="2.0.1"
BASTIONGUARD_BUILD=20260726
BASTIONGUARD_VERSION="2.0.2"
BASTIONGUARD_BUILD=20260727
)
target_link_libraries(BastionGuard
@ -1292,29 +1292,6 @@ install(TARGETS bastionguard-privhelper
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE)
bg_install_code( "
message(STATUS \"[privhelper] Imposto owner root e setuid su bastionguard-privhelper\")
execute_process(
COMMAND chown root:root /usr/bin/bastionguard-privhelper
RESULT_VARIABLE CHOWN_RES
)
if(NOT CHOWN_RES EQUAL 0)
message(FATAL_ERROR \"[privhelper] chown root fallito\")
endif()
execute_process(
COMMAND chmod 4755 /usr/bin/bastionguard-privhelper
RESULT_VARIABLE CHMOD_RES
)
if(NOT CHMOD_RES EQUAL 0)
message(FATAL_ERROR \"[privhelper] chmod 4755 fallito\")
endif()
message(STATUS \"[privhelper] ✔ bastionguard-privhelper installato come setuid root\")
")
# ======================
# Helper privilegiato — bastionguard-firewall
@ -1338,29 +1315,6 @@ install(TARGETS bastionguard-firewall
WORLD_READ WORLD_EXECUTE
)
bg_install_code( "
message(STATUS \"[firewall-helper] Imposto owner root e setuid su bastionguard-firewall\")
execute_process(
COMMAND chown root:root /usr/bin/bastionguard-firewall
RESULT_VARIABLE CHOWN_RES
)
if(NOT CHOWN_RES EQUAL 0)
message(FATAL_ERROR \"[firewall-helper] chown root fallito\")
endif()
execute_process(
COMMAND chmod 4755 /usr/bin/bastionguard-firewall
RESULT_VARIABLE CHMOD_RES
)
if(NOT CHMOD_RES EQUAL 0)
message(FATAL_ERROR \"[firewall-helper] chmod 4755 fallito\")
endif()
message(STATUS \"[firewall-helper] ✔ bastionguard-firewall installato come setuid root\")
")
# ======================

View file

@ -23,7 +23,7 @@
#
Name: bastionguard
Version: 2.0.1
Version: 2.0.2
Release: 1leap
Summary: BastionGuard Security Platform
License: GPLv3

View file

@ -719,8 +719,8 @@ target_include_directories(BastionGuard
)
target_compile_definitions(BastionGuard PRIVATE
BASTIONGUARD_VERSION="2.0.1"
BASTIONGUARD_BUILD=20260726
BASTIONGUARD_VERSION="2.0.2"
BASTIONGUARD_BUILD=20260727
)
target_link_libraries(BastionGuard
@ -1248,29 +1248,6 @@ install(TARGETS bastionguard-privhelper
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE)
bg_install_code( "
message(STATUS \"[privhelper] Imposto owner root e setuid su bastionguard-privhelper\")
execute_process(
COMMAND chown root:root /usr/bin/bastionguard-privhelper
RESULT_VARIABLE CHOWN_RES
)
if(NOT CHOWN_RES EQUAL 0)
message(FATAL_ERROR \"[privhelper] chown root fallito\")
endif()
execute_process(
COMMAND chmod 4755 /usr/bin/bastionguard-privhelper
RESULT_VARIABLE CHMOD_RES
)
if(NOT CHMOD_RES EQUAL 0)
message(FATAL_ERROR \"[privhelper] chmod 4755 fallito\")
endif()
message(STATUS \"[privhelper] ✔ bastionguard-privhelper installato come setuid root\")
")
# ======================
# Helper privilegiato — bastionguard-firewall
@ -1294,29 +1271,6 @@ install(TARGETS bastionguard-firewall
WORLD_READ WORLD_EXECUTE
)
bg_install_code( "
message(STATUS \"[firewall-helper] Imposto owner root e setuid su bastionguard-firewall\")
execute_process(
COMMAND chown root:root /usr/bin/bastionguard-firewall
RESULT_VARIABLE CHOWN_RES
)
if(NOT CHOWN_RES EQUAL 0)
message(FATAL_ERROR \"[firewall-helper] chown root fallito\")
endif()
execute_process(
COMMAND chmod 4755 /usr/bin/bastionguard-firewall
RESULT_VARIABLE CHMOD_RES
)
if(NOT CHMOD_RES EQUAL 0)
message(FATAL_ERROR \"[firewall-helper] chmod 4755 fallito\")
endif()
message(STATUS \"[firewall-helper] ✔ bastionguard-firewall installato come setuid root\")
")
# ======================

View file

@ -23,7 +23,7 @@
#
Name: bastionguard
Version: 2.0.1
Version: 2.0.2
Release: 1
Summary: BastionGuard Security Platform
License: GPLv3

View file

@ -719,8 +719,8 @@ target_include_directories(BastionGuard
)
target_compile_definitions(BastionGuard PRIVATE
BASTIONGUARD_VERSION="2.0.1"
BASTIONGUARD_BUILD=20260726
BASTIONGUARD_VERSION="2.0.2"
BASTIONGUARD_BUILD=20260727
)
target_link_libraries(BastionGuard
@ -1248,29 +1248,6 @@ install(TARGETS bastionguard-privhelper
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE)
bg_install_code( "
message(STATUS \"[privhelper] Imposto owner root e setuid su bastionguard-privhelper\")
execute_process(
COMMAND chown root:root /usr/bin/bastionguard-privhelper
RESULT_VARIABLE CHOWN_RES
)
if(NOT CHOWN_RES EQUAL 0)
message(FATAL_ERROR \"[privhelper] chown root fallito\")
endif()
execute_process(
COMMAND chmod 4755 /usr/bin/bastionguard-privhelper
RESULT_VARIABLE CHMOD_RES
)
if(NOT CHMOD_RES EQUAL 0)
message(FATAL_ERROR \"[privhelper] chmod 4755 fallito\")
endif()
message(STATUS \"[privhelper] ✔ bastionguard-privhelper installato come setuid root\")
")
# ======================
# Helper privilegiato — bastionguard-firewall
@ -1294,29 +1271,6 @@ install(TARGETS bastionguard-firewall
WORLD_READ WORLD_EXECUTE
)
bg_install_code( "
message(STATUS \"[firewall-helper] Imposto owner root e setuid su bastionguard-firewall\")
execute_process(
COMMAND chown root:root /usr/bin/bastionguard-firewall
RESULT_VARIABLE CHOWN_RES
)
if(NOT CHOWN_RES EQUAL 0)
message(FATAL_ERROR \"[firewall-helper] chown root fallito\")
endif()
execute_process(
COMMAND chmod 4755 /usr/bin/bastionguard-firewall
RESULT_VARIABLE CHMOD_RES
)
if(NOT CHMOD_RES EQUAL 0)
message(FATAL_ERROR \"[firewall-helper] chmod 4755 fallito\")
endif()
message(STATUS \"[firewall-helper] ✔ bastionguard-firewall installato come setuid root\")
")
# ======================

View file

@ -21,7 +21,7 @@
# Maintainer: BastionGuard info@bastionguard.eu
pkgname=bastionguard
pkgver=2.0.1
pkgver=2.0.2
pkgrel=1
pkgdesc="BastionGuard - transparent security control plane for Linux desktops"
arch=('x86_64')

View file

@ -636,8 +636,8 @@ target_include_directories(BastionGuard
)
target_compile_definitions(BastionGuard PRIVATE
BASTIONGUARD_VERSION="2.0.1"
BASTIONGUARD_BUILD=20260726
BASTIONGUARD_VERSION="2.0.2"
BASTIONGUARD_BUILD=20260727
)
target_link_libraries(BastionGuard
@ -1189,29 +1189,6 @@ install(TARGETS bastionguard-privhelper
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE)
bg_install_code( "
message(STATUS \"[privhelper] Imposto owner root e setuid su bastionguard-privhelper\")
execute_process(
COMMAND chown root:root /usr/bin/bastionguard-privhelper
RESULT_VARIABLE CHOWN_RES
)
if(NOT CHOWN_RES EQUAL 0)
message(FATAL_ERROR \"[privhelper] chown root fallito\")
endif()
execute_process(
COMMAND chmod 4755 /usr/bin/bastionguard-privhelper
RESULT_VARIABLE CHMOD_RES
)
if(NOT CHMOD_RES EQUAL 0)
message(FATAL_ERROR \"[privhelper] chmod 4755 fallito\")
endif()
message(STATUS \"[privhelper] ✔ bastionguard-privhelper installato come setuid root\")
")
# ======================
# Helper privilegiato — bastionguard-firewall
@ -1235,29 +1212,6 @@ install(TARGETS bastionguard-firewall
WORLD_READ WORLD_EXECUTE
)
bg_install_code( "
message(STATUS \"[firewall-helper] Imposto owner root e setuid su bastionguard-firewall\")
execute_process(
COMMAND chown root:root /usr/bin/bastionguard-firewall
RESULT_VARIABLE CHOWN_RES
)
if(NOT CHOWN_RES EQUAL 0)
message(FATAL_ERROR \"[firewall-helper] chown root fallito\")
endif()
execute_process(
COMMAND chmod 4755 /usr/bin/bastionguard-firewall
RESULT_VARIABLE CHMOD_RES
)
if(NOT CHMOD_RES EQUAL 0)
message(FATAL_ERROR \"[firewall-helper] chmod 4755 fallito\")
endif()
message(STATUS \"[firewall-helper] ✔ bastionguard-firewall installato come setuid root\")
")
# ======================

View file

@ -35,6 +35,9 @@
#include <stdexcept>
#include <fstream>
#include <unistd.h>
#include <cerrno>
#include <sys/stat.h>
#include <sys/wait.h>
#include <algorithm>
#include <unordered_set>
#include <regex>
@ -2975,22 +2978,110 @@ Backend::ScanStatus Backend::get_current_scan_files()
namespace {
bool create_secure_firewall_input(
const std::vector<std::string>& blacklistLines,
std::string& outPath
) {
std::string pathTemplate = "/tmp/bastionguard-firewall-ips-XXXXXX";
std::vector<char> pathBuffer(pathTemplate.begin(), pathTemplate.end());
pathBuffer.push_back('\0');
const int fd = ::mkstemp(pathBuffer.data());
if (fd < 0) {
std::cerr << _("[Firewall] Impossibile creare il file temporaneo sicuro\n");
return false;
}
outPath.assign(pathBuffer.data());
if (::fchmod(fd, S_IRUSR | S_IWUSR) != 0) {
::close(fd);
::unlink(outPath.c_str());
outPath.clear();
std::cerr << _("[Firewall] Impossibile proteggere il file temporaneo\n");
return false;
}
::close(fd);
if (!BlacklistIpExtractor::extractAndWrite(blacklistLines, outPath)) {
::unlink(outPath.c_str());
outPath.clear();
return false;
}
// extractAndWrite() riapre il file: imponiamo nuovamente 0600 e
// verifichiamo che il pathname indichi ancora un singolo file regolare
// appartenente all'utente che ha avviato BastionGuard.
if (::chmod(outPath.c_str(), S_IRUSR | S_IWUSR) != 0) {
::unlink(outPath.c_str());
outPath.clear();
std::cerr << _("[Firewall] Impossibile impostare i permessi del file temporaneo\n");
return false;
}
struct stat st {};
if (::lstat(outPath.c_str(), &st) != 0 ||
!S_ISREG(st.st_mode) ||
st.st_uid != ::getuid() ||
st.st_nlink != 1 ||
(st.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
::unlink(outPath.c_str());
outPath.clear();
std::cerr << _("[Firewall] File temporaneo non sicuro\n");
return false;
}
return true;
}
int run_firewall_helper(const std::string& inputPath) {
const pid_t pid = ::fork();
if (pid < 0)
return -1;
if (pid == 0) {
::execl(
"/usr/bin/pkexec",
"pkexec",
"/usr/bin/bastionguard-firewall",
"apply",
inputPath.c_str(),
static_cast<char*>(nullptr)
);
_exit(127);
}
int status = 0;
while (::waitpid(pid, &status, 0) < 0) {
if (errno == EINTR)
continue;
return -1;
}
if (!WIFEXITED(status))
return -1;
return WEXITSTATUS(status);
}
} // namespace
void Backend::applyFirewallFromBlacklist(
const std::vector<std::string>& blacklistLines
) {
if (!firewallEnabled_ || firewallType_ == FirewallType::NONE)
return;
const std::string outPath =
"/tmp/bastionguard-firewall-ips.txt";
if (!BlacklistIpExtractor::extractAndWrite(
blacklistLines, outPath)) {
std::string outPath;
if (!create_secure_firewall_input(blacklistLines, outPath)) {
std::cout << _("[Firewall] Nessun IP valido estratto\n");
return;
}
scheduleFirewallApply();
scheduleFirewallApply(outPath);
}
@ -3082,29 +3173,35 @@ void Backend::applyFirewallFromPhishingBlacklist()
fin.close();
const std::string outPath =
"/tmp/bastionguard-firewall-ips.txt";
if (!BlacklistIpExtractor::extractAndWrite(lines, outPath)) {
std::string outPath;
if (!create_secure_firewall_input(lines, outPath)) {
std::cout << _("[Firewall] Nessun IP valido trovato\n");
return;
}
scheduleFirewallApply();
scheduleFirewallApply(outPath);
}
void Backend::scheduleFirewallApply() {
void Backend::scheduleFirewallApply(const std::string& inputPath) {
static std::atomic<bool> pending{false};
if (pending.exchange(true))
if (pending.exchange(true)) {
::unlink(inputPath.c_str());
return;
}
std::thread([] {
std::thread([inputPath] {
std::this_thread::sleep_for(std::chrono::seconds(2));
system("pkexec /usr/bin/bastionguard-firewall apply");
const int exitCode = run_firewall_helper(inputPath);
if (exitCode != 0) {
std::cerr << _("[Firewall] Applicazione helper fallita, codice: ")
<< exitCode << "\n";
::unlink(inputPath.c_str());
}
pending.store(false);
}).detach();
}

View file

@ -189,7 +189,7 @@ public:
void loadFirewallConfig();
void saveFirewallConfig() const;
void applyFirewallFromPhishingBlacklist();
void scheduleFirewallApply();
void scheduleFirewallApply(const std::string& inputPath);
private:
Backend();
Backend(const Backend&) = delete;

View file

@ -29,6 +29,8 @@
#include <atomic>
#include <csignal>
#include <unistd.h>
#include <cerrno>
#include <sys/wait.h>
#include <glibmm/i18n.h>
#include <glibmm/miscutils.h>
#include <glibmm/main.h>
@ -41,6 +43,36 @@ std::queue<std::string> alert_queue_;
Gtk::Window* current_alert_ = nullptr;
static bool run_privhelper_pkexec(const std::string& action) {
if (action != "block" && action != "unblock")
return false;
const pid_t pid = ::fork();
if (pid < 0)
return false;
if (pid == 0) {
::execl(
"/usr/bin/pkexec",
"pkexec",
"/usr/bin/bastionguard-privhelper",
action.c_str(),
static_cast<char*>(nullptr)
);
_exit(127);
}
int status = 0;
while (::waitpid(pid, &status, 0) < 0) {
if (errno == EINTR)
continue;
return false;
}
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
}
static std::string read_text_file(const std::string& path) {
std::ifstream f(path);
if (!f.is_open()) return {};
@ -313,9 +345,7 @@ void PrivacyPage::start_privacy_daemon() {
void PrivacyPage::unblock_devices_local() {
std::string cmd = "/usr/bin/bastionguard-privhelper unblock";
int rc = std::system(cmd.c_str());
if (WIFEXITED(rc) && WEXITSTATUS(rc) == 0)
if (run_privhelper_pkexec("unblock"))
write_log(_("📷 Webcam/Microfono sbloccati automaticamente"));
else
write_log(_("❌ Errore nello sblocco automatico"));
@ -606,11 +636,7 @@ void PrivacyPage::show_next_alert() {
auto alive = alive_;
std::thread([this, alive]() {
std::string cmd = "/usr/bin/bastionguard-privhelper block";
int rc = std::system(cmd.c_str());
bool ok = (WIFEXITED(rc) && WEXITSTATUS(rc) == 0);
const bool ok = run_privhelper_pkexec("block");
Glib::signal_idle().connect_once([this, alive, ok]() {
BastionGuard::if_alive(alive, [this, ok]() {
@ -859,13 +885,7 @@ void PrivacyPage::run_privacy_script_async(bool unblock) {
auto alive = alive_;
std::thread([this, alive, unblock]() {
std::string cmd = "/usr/bin/bastionguard-privhelper ";
cmd += (unblock ? "unblock" : "block");
int rc = std::system(cmd.c_str());
const bool ok = (WIFEXITED(rc) && WEXITSTATUS(rc) == 0);
const bool ok = run_privhelper_pkexec(unblock ? "unblock" : "block");
Glib::signal_idle().connect_once([this, alive, ok, unblock]() {
@ -908,9 +928,7 @@ void PrivacyPage::run_privacy_script_async(bool unblock) {
void PrivacyPage::manual_unlock_devices() {
auto alive = alive_;
std::thread([this, alive]() {
std::string cmd = "/usr/bin/bastionguard-privhelper unblock";
int rc = std::system(cmd.c_str());
bool ok = (WIFEXITED(rc) && WEXITSTATUS(rc) == 0);
const bool ok = run_privhelper_pkexec("unblock");
Glib::signal_idle().connect_once([this, alive, ok]() {
BastionGuard::if_alive(alive, [this, ok]() {

View file

@ -18,149 +18,367 @@
* The BastionGuard name and branding are not licensed under the GPL.
*/
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <filesystem>
#include <arpa/inet.h>
#include <cerrno>
#include <cctype>
#include <cstdlib>
#include <unistd.h>
#include <cstring>
#include <fcntl.h>
#include <iostream>
#include <limits>
#include <optional>
#include <string>
#include <string_view>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <unordered_set>
#include <vector>
#include <glib/gi18n.h>
namespace fs = std::filesystem;
static constexpr const char* BLACKLIST_PATH =
"/tmp/bastionguard-firewall-ips.txt";
namespace {
static bool run_cmd(const std::string& cmd) {
int rc = std::system(cmd.c_str());
return WIFEXITED(rc) && WEXITSTATUS(rc) == 0;
}
constexpr std::string_view kInputPrefix = "/tmp/bastionguard-firewall-ips-";
constexpr off_t kMaxInputBytes = 16 * 1024 * 1024;
constexpr std::size_t kMaxEntries = 250000;
constexpr std::size_t kMaxLineLength = 128;
static bool command_exists(const std::string& cmd) {
return run_cmd("command -v " + cmd + " >/dev/null 2>&1");
}
struct SecureInput {
int fd{-1};
std::string path;
static std::vector<std::string> load_blacklist() {
std::vector<std::string> ips;
SecureInput() = default;
SecureInput(int input_fd, std::string input_path)
: fd(input_fd), path(std::move(input_path)) {}
std::ifstream fin(BLACKLIST_PATH);
if (!fin) {
std::cerr << _("[Firewall] Impossibile aprire blacklist: ")
<< BLACKLIST_PATH << "\n";
return ips;
SecureInput(const SecureInput&) = delete;
SecureInput& operator=(const SecureInput&) = delete;
SecureInput(SecureInput&& other) noexcept
: fd(other.fd), path(std::move(other.path)) {
other.fd = -1;
}
std::string line;
while (std::getline(fin, line)) {
if (line.empty()) continue;
if (line[0] == '#') continue;
ips.push_back(line);
SecureInput& operator=(SecureInput&& other) noexcept {
if (this != &other) {
if (fd >= 0) ::close(fd);
fd = other.fd;
path = std::move(other.path);
other.fd = -1;
}
return *this;
}
~SecureInput() {
if (fd >= 0) ::close(fd);
}
};
std::optional<uid_t> parse_uid(const char* value) {
if (!value || !*value) return std::nullopt;
unsigned long long parsed = 0;
for (const unsigned char ch : std::string(value)) {
if (!std::isdigit(ch)) return std::nullopt;
parsed = parsed * 10 + static_cast<unsigned long long>(ch - '0');
if (parsed > std::numeric_limits<uid_t>::max()) return std::nullopt;
}
return static_cast<uid_t>(parsed);
}
uid_t original_caller_uid() {
if (const auto uid = parse_uid(std::getenv("PKEXEC_UID"))) {
return *uid;
}
return ::getuid();
}
bool allowed_input_path(const std::string& path) {
if (!path.starts_with(kInputPrefix)) return false;
const std::string_view suffix(path.data() + kInputPrefix.size(),
path.size() - kInputPrefix.size());
return !suffix.empty() && suffix.find('/') == std::string_view::npos;
}
std::optional<SecureInput> open_input_securely(const std::string& path,
uid_t caller_uid) {
if (!allowed_input_path(path)) {
std::cerr << _("[Firewall] Percorso input non autorizzato.\n");
return std::nullopt;
}
const int fd = ::open(path.c_str(), O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
if (fd < 0) {
std::cerr << _("[Firewall] Impossibile aprire il file input: ")
<< std::strerror(errno) << "\n";
return std::nullopt;
}
struct stat st {};
if (::fstat(fd, &st) != 0) {
std::cerr << _("[Firewall] fstat fallita: ")
<< std::strerror(errno) << "\n";
::close(fd);
return std::nullopt;
}
const bool ownership_ok =
(caller_uid == 0 && st.st_uid == 0) ||
(caller_uid != 0 && st.st_uid == caller_uid);
if (!S_ISREG(st.st_mode) ||
st.st_nlink != 1 ||
!ownership_ok ||
(st.st_mode & (S_IWGRP | S_IWOTH)) != 0 ||
st.st_size <= 0 ||
st.st_size > kMaxInputBytes) {
std::cerr << _("[Firewall] File input non sicuro o non valido.\n");
::close(fd);
return std::nullopt;
}
// The descriptor remains valid after unlink and cannot be replaced through
// the pathname while the privileged process is reading it.
if (::unlink(path.c_str()) != 0) {
std::cerr << _("[Firewall] Impossibile rimuovere il file temporaneo: ")
<< std::strerror(errno) << "\n";
::close(fd);
return std::nullopt;
}
return SecureInput(fd, path);
}
std::string trim(std::string value) {
const auto first = value.find_first_not_of(" \t\r\n");
if (first == std::string::npos) return {};
const auto last = value.find_last_not_of(" \t\r\n");
return value.substr(first, last - first + 1);
}
bool is_valid_ip(const std::string& value) {
if (value.empty() || value.size() > kMaxLineLength) return false;
in_addr ipv4 {};
if (::inet_pton(AF_INET, value.c_str(), &ipv4) == 1) return true;
in6_addr ipv6 {};
return ::inet_pton(AF_INET6, value.c_str(), &ipv6) == 1;
}
std::vector<std::string> load_and_validate_ips(int fd) {
std::string data;
data.reserve(64 * 1024);
char buffer[8192];
while (true) {
const ssize_t count = ::read(fd, buffer, sizeof(buffer));
if (count == 0) break;
if (count < 0) {
if (errno == EINTR) continue;
std::cerr << _("[Firewall] Errore lettura file input: ")
<< std::strerror(errno) << "\n";
return {};
}
if (data.size() + static_cast<std::size_t>(count) >
static_cast<std::size_t>(kMaxInputBytes)) {
std::cerr << _("[Firewall] File input troppo grande.\n");
return {};
}
data.append(buffer, static_cast<std::size_t>(count));
}
std::vector<std::string> ips;
std::unordered_set<std::string> seen;
std::size_t start = 0;
while (start <= data.size()) {
const auto end = data.find('\n', start);
std::string line = trim(data.substr(
start,
end == std::string::npos ? std::string::npos : end - start));
if (!line.empty() && line.front() != '#') {
if (!is_valid_ip(line)) {
std::cerr << _("[Firewall] Voce IP rifiutata: ") << line << "\n";
return {};
}
if (seen.insert(line).second) {
ips.push_back(std::move(line));
if (ips.size() > kMaxEntries) {
std::cerr << _("[Firewall] Troppe voci nel file input.\n");
return {};
}
}
}
if (end == std::string::npos) break;
start = end + 1;
}
return ips;
}
static bool apply_ufw(const std::vector<std::string>& ips) {
if (!command_exists("ufw"))
std::optional<std::string> find_program(
std::initializer_list<const char*> candidates) {
for (const char* candidate : candidates) {
if (::access(candidate, X_OK) == 0) return std::string(candidate);
}
return std::nullopt;
}
bool run_program(const std::string& executable,
const std::vector<std::string>& arguments,
bool silence_stderr = false) {
std::vector<std::string> storage;
storage.reserve(arguments.size() + 1);
storage.push_back(executable);
storage.insert(storage.end(), arguments.begin(), arguments.end());
std::vector<char*> argv;
argv.reserve(storage.size() + 1);
for (auto& value : storage) argv.push_back(value.data());
argv.push_back(nullptr);
const pid_t pid = ::fork();
if (pid < 0) return false;
if (pid == 0) {
if (silence_stderr) {
const int null_fd = ::open("/dev/null", O_WRONLY | O_CLOEXEC);
if (null_fd >= 0) {
::dup2(null_fd, STDERR_FILENO);
if (null_fd != STDERR_FILENO) ::close(null_fd);
}
}
::execv(executable.c_str(), argv.data());
_exit(127);
}
int status = 0;
while (::waitpid(pid, &status, 0) < 0) {
if (errno == EINTR) continue;
return false;
}
run_cmd("ufw --force enable");
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
}
bool apply_ufw(const std::vector<std::string>& ips) {
const auto ufw = find_program({"/usr/sbin/ufw", "/usr/bin/ufw"});
if (!ufw) return false;
if (!run_program(*ufw, {"--force", "enable"})) return false;
bool success = true;
for (const auto& ip : ips) {
run_cmd("ufw deny from " + ip);
if (!run_program(*ufw, {"deny", "from", ip})) success = false;
}
return true;
return success;
}
static bool apply_firewalld(const std::vector<std::string>& ips) {
if (!command_exists("firewall-cmd"))
return false;
bool apply_firewalld(const std::vector<std::string>& ips) {
const auto firewall_cmd =
find_program({"/usr/bin/firewall-cmd", "/usr/sbin/firewall-cmd"});
if (!firewall_cmd) return false;
run_cmd(
"firewall-cmd --permanent "
"--new-ipset=bastionguard-blacklist "
"--type=hash:ip 2>/dev/null"
);
run_cmd(
"firewall-cmd --permanent "
"--zone=public --add-source-ipset=bastionguard-blacklist"
);
// These operations are idempotent from BastionGuard's perspective. An
// already existing ipset/source association is not fatal.
run_program(*firewall_cmd,
{"--permanent", "--new-ipset=bastionguard-blacklist",
"--type=hash:ip"},
true);
run_program(*firewall_cmd,
{"--permanent", "--zone=public",
"--add-source-ipset=bastionguard-blacklist"},
true);
bool success = true;
for (const auto& ip : ips) {
run_cmd(
"firewall-cmd --permanent "
"--ipset=bastionguard-blacklist --add-entry=" + ip
);
if (!run_program(*firewall_cmd,
{"--permanent",
"--ipset=bastionguard-blacklist",
"--add-entry=" + ip},
true)) {
// Duplicate entries may return a non-zero status. Continue and let
// reload determine whether firewalld is operational.
success = false;
}
}
run_cmd("firewall-cmd --reload");
return true;
const bool reloaded = run_program(*firewall_cmd, {"--reload"});
return reloaded && (success || !ips.empty());
}
static void clear_firewall() {
if (command_exists("ufw")) {
run_cmd("ufw --force reset");
void clear_firewall() {
if (const auto ufw = find_program({"/usr/sbin/ufw", "/usr/bin/ufw"})) {
run_program(*ufw, {"--force", "reset"});
}
if (command_exists("firewall-cmd")) {
run_cmd(
"firewall-cmd --permanent "
"--delete-ipset=bastionguard-blacklist 2>/dev/null"
);
run_cmd("firewall-cmd --reload");
if (const auto firewall_cmd =
find_program({"/usr/bin/firewall-cmd", "/usr/sbin/firewall-cmd"})) {
run_program(*firewall_cmd,
{"--permanent", "--delete-ipset=bastionguard-blacklist"},
true);
run_program(*firewall_cmd, {"--reload"});
}
}
} // namespace
int main(int argc, char* argv[]) {
const uid_t caller_uid = original_caller_uid();
clearenv();
setenv("PATH", "/usr/sbin:/usr/bin:/sbin:/bin", 1);
::clearenv();
::setenv("PATH", "/usr/sbin:/usr/bin:/sbin:/bin", 1);
if (geteuid() != 0) {
std::cerr << _("[Firewall] Deve essere eseguito come root.\n");
if (::geteuid() != 0) {
std::cerr << _("[Firewall] Deve essere eseguito come root tramite pkexec.\n");
return 10;
}
if (argc != 2) {
std::cerr << "Uso: " << argv[0] << " apply|clear\n";
if (argc < 2) {
std::cerr << "Uso: " << argv[0] << " apply <input-file>|clear\n";
return 1;
}
std::string action = argv[1];
const std::string action = argv[1];
if (action == "clear") {
if (argc != 2) return 1;
clear_firewall();
fs::remove(BLACKLIST_PATH);
std::cout << _("[Firewall] Regole rimosse.\n");
return 0;
}
if (action != "apply") {
std::cerr << _("[Firewall] Comando non valido: ") << action << "\n";
if (action != "apply" || argc != 3) {
std::cerr << _("[Firewall] Comando non valido.\n");
return 2;
}
auto ips = load_blacklist();
auto input = open_input_securely(argv[2], caller_uid);
if (!input) return 3;
auto ips = load_and_validate_ips(input->fd);
if (ips.empty()) {
std::cerr << _("[Firewall] Blacklist vuota o non valida.\n");
return 3;
return 4;
}
bool ok =
apply_firewalld(ips) ||
apply_ufw(ips);
fs::remove(BLACKLIST_PATH);
const bool ok = apply_firewalld(ips) || apply_ufw(ips);
if (ok) {
std::cout << _("[Firewall] Regole applicate (")
<< ips.size() << " IP).\n";
<< ips.size() << _(" IP).\n");
return 0;
}
std::cerr << _("[Firewall] Nessun firewall supportato trovato.\n");
std::cerr << _("[Firewall] Nessun firewall supportato o applicazione fallita.\n");
return 5;
}

View file

@ -18,138 +18,540 @@
* The BastionGuard name and branding are not licensed under the GPL.
*/
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cerrno>
#include <cctype>
#include <climits>
#include <cstring>
#include <cstdlib>
#include <string>
#include <dirent.h>
#include <fcntl.h>
#include <grp.h>
#include <iostream>
#include <pwd.h>
#include <set>
#include <sstream>
#include <vector>
#include <filesystem>
#include <string>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <array>
#include <glib/gi18n.h>
#include <vector>
namespace fs = std::filesystem;
namespace {
constexpr const char* kStateDir = "/run/bastionguard";
constexpr const char* kStateFile = "/run/bastionguard/camera-device-modes";
constexpr std::size_t kMaxPipewireOutput = 1024U * 1024U;
static std::string exec_shell(const std::string& cmd) {
std::array<char, 256> buffer;
std::string result;
FILE* pipe = popen(cmd.c_str(), "r");
if (!pipe) return {};
while (fgets(buffer.data(), buffer.size(), pipe))
result += buffer.data();
pclose(pipe);
return result;
struct CallerIdentity {
uid_t uid{0};
gid_t gid{0};
std::string name{"root"};
std::string home{"/root"};
};
bool parse_uid(const char* value, uid_t& out)
{
if (!value || !*value)
return false;
errno = 0;
char* end = nullptr;
const unsigned long parsed = std::strtoul(value, &end, 10);
if (errno != 0 || end == value || *end != '\0' || parsed > UINT_MAX)
return false;
out = static_cast<uid_t>(parsed);
return true;
}
static bool has_pipewire() {
int rc = std::system("/usr/bin/pw-cli info 0 > /dev/null 2>&1");
return (WIFEXITED(rc) && WEXITSTATUS(rc) == 0);
}
CallerIdentity resolve_caller_identity()
{
uid_t uid = getuid();
static void block_v4l2_devices() {
for (const auto& e : fs::directory_iterator("/dev")) {
auto p = e.path().string();
if (p.rfind("/dev/video", 0) == 0) {
chmod(p.c_str(), 0000);
if (uid == 0) {
uid_t env_uid = 0;
if (parse_uid(std::getenv("PKEXEC_UID"), env_uid) ||
parse_uid(std::getenv("SUDO_UID"), env_uid)) {
uid = env_uid;
}
}
}
static void unblock_v4l2_devices() {
for (const auto& e : fs::directory_iterator("/dev")) {
auto p = e.path().string();
if (p.rfind("/dev/video", 0) == 0) {
chmod(p.c_str(), 0660);
}
CallerIdentity identity;
identity.uid = uid;
if (passwd* pw = getpwuid(uid)) {
identity.gid = pw->pw_gid;
if (pw->pw_name)
identity.name = pw->pw_name;
if (pw->pw_dir && *pw->pw_dir)
identity.home = pw->pw_dir;
}
return identity;
}
static bool pipewire_disable_camera() {
std::string list = exec_shell("/usr/bin/pw-cli ls Node");
std::istringstream ss(list);
std::string line;
std::vector<std::string> ids;
while (std::getline(ss, line)) {
if (line.find("id ") != std::string::npos) {
std::string id = line.substr(line.find("id ") + 3);
id = id.substr(0, id.find_first_of(", \t\n"));
std::streampos cur = ss.tellg();
std::string sub;
bool isVideoDevice = false;
while (std::getline(ss, sub)) {
if (sub.find("media.class") != std::string::npos &&
sub.find("Video/Device") != std::string::npos) {
isVideoDevice = true;
break;
}
if (sub.rfind("id ", 0) == 0)
break;
bool runtime_dir_is_safe(uid_t uid, const std::string& path)
{
struct stat st {};
if (lstat(path.c_str(), &st) != 0)
return false;
return S_ISDIR(st.st_mode) &&
st.st_uid == uid &&
(st.st_mode & (S_IWGRP | S_IWOTH)) == 0;
}
bool drop_to_caller(const CallerIdentity& caller)
{
if (caller.uid == 0)
return true;
if (initgroups(caller.name.c_str(), caller.gid) != 0)
return false;
if (setresgid(caller.gid, caller.gid, caller.gid) != 0)
return false;
if (setresuid(caller.uid, caller.uid, caller.uid) != 0)
return false;
return true;
}
std::vector<char*> make_exec_argv(const std::vector<std::string>& args)
{
std::vector<char*> argv;
argv.reserve(args.size() + 1);
for (const auto& arg : args)
argv.push_back(const_cast<char*>(arg.c_str()));
argv.push_back(nullptr);
return argv;
}
bool run_as_caller(const CallerIdentity& caller,
const std::vector<std::string>& args,
std::string* output = nullptr)
{
if (args.empty() || args.front().empty() || args.front().front() != '/')
return false;
int pipefd[2] = {-1, -1};
if (output && pipe2(pipefd, O_CLOEXEC) != 0)
return false;
const pid_t pid = fork();
if (pid < 0) {
if (output) {
close(pipefd[0]);
close(pipefd[1]);
}
return false;
}
if (pid == 0) {
if (output) {
close(pipefd[0]);
if (dup2(pipefd[1], STDOUT_FILENO) < 0)
_exit(126);
close(pipefd[1]);
}
if (!drop_to_caller(caller))
_exit(126);
clearenv();
setenv("PATH", "/usr/bin:/bin", 1);
setenv("HOME", caller.home.c_str(), 1);
setenv("USER", caller.name.c_str(), 1);
setenv("LOGNAME", caller.name.c_str(), 1);
const std::string runtime = "/run/user/" + std::to_string(caller.uid);
if (runtime_dir_is_safe(caller.uid, runtime))
setenv("XDG_RUNTIME_DIR", runtime.c_str(), 1);
auto argv = make_exec_argv(args);
execv(args.front().c_str(), argv.data());
_exit(errno == ENOENT ? 127 : 126);
}
if (output) {
close(pipefd[1]);
output->clear();
char buffer[4096];
while (output->size() < kMaxPipewireOutput) {
const ssize_t n = read(pipefd[0], buffer, sizeof(buffer));
if (n > 0) {
const std::size_t remaining = kMaxPipewireOutput - output->size();
output->append(buffer, std::min<std::size_t>(static_cast<std::size_t>(n), remaining));
continue;
}
if (isVideoDevice)
ids.push_back(id);
ss.clear();
ss.seekg(cur);
if (n < 0 && errno == EINTR)
continue;
break;
}
close(pipefd[0]);
}
bool success = true;
for (auto &id : ids) {
std::string cmd = "/usr/bin/pw-cli set-param " + id + " Device/Disabled true";
int rc = std::system(cmd.c_str());
if (!(WIFEXITED(rc) && WEXITSTATUS(rc) == 0)) {
std::cerr << _("Errore disabilitando nodo PipeWire id=") << id << "\n";
success = false;
}
int status = 0;
while (waitpid(pid, &status, 0) < 0) {
if (errno != EINTR)
return false;
}
return success;
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
}
static bool pipewire_enable_camera() {
std::string list = exec_shell("pw-cli ls Node");
std::istringstream ss(list);
std::string line;
std::string find_pw_cli()
{
static constexpr const char* candidates[] = {
"/usr/bin/pw-cli",
"/bin/pw-cli"
};
for (const char* candidate : candidates) {
if (access(candidate, X_OK) == 0)
return candidate;
}
return {};
}
bool is_decimal_id(const std::string& value)
{
return !value.empty() &&
value.size() <= 10 &&
std::all_of(value.begin(), value.end(), [](unsigned char c) {
return std::isdigit(c) != 0;
});
}
std::vector<std::string> parse_video_node_ids(const std::string& listing)
{
std::vector<std::string> ids;
while (std::getline(ss, line)) {
if (line.find("id ") != std::string::npos) {
std::string id = line.substr(line.find("id ") + 3);
id = id.substr(0, id.find_first_of(", \t\n"));
std::streampos cur = ss.tellg();
std::string sub;
bool isVideoDevice = false;
while (std::getline(ss, sub)) {
if (sub.find("media.class") != std::string::npos &&
sub.find("Video/Device") != std::string::npos) {
isVideoDevice = true;
break;
}
if (sub.rfind("id ", 0) == 0)
break;
}
if (isVideoDevice)
ids.push_back(id);
ss.clear();
ss.seekg(cur);
std::set<std::string> seen;
std::istringstream stream(listing);
std::string current_id;
bool current_is_video = false;
auto commit = [&]() {
if (current_is_video && is_decimal_id(current_id) && seen.insert(current_id).second)
ids.push_back(current_id);
current_id.clear();
current_is_video = false;
};
std::string line;
while (std::getline(stream, line)) {
const std::size_t id_pos = line.find("id ");
if (id_pos != std::string::npos) {
commit();
std::size_t begin = id_pos + 3;
while (begin < line.size() && std::isspace(static_cast<unsigned char>(line[begin])))
++begin;
std::size_t end = begin;
while (end < line.size() && std::isdigit(static_cast<unsigned char>(line[end])))
++end;
if (end > begin)
current_id = line.substr(begin, end - begin);
continue;
}
if (!current_id.empty() &&
line.find("media.class") != std::string::npos &&
line.find("Video/Device") != std::string::npos) {
current_is_video = true;
}
}
bool success = true;
for (auto &id : ids) {
std::string cmd = "pw-cli set-param " + id + " Device/Disabled false";
int rc = std::system(cmd.c_str());
if (!(WIFEXITED(rc) && WEXITSTATUS(rc) == 0)) {
std::cerr << _("Errore riabilitando nodo PipeWire id=") << id << "\n";
success = false;
}
}
return success;
commit();
return ids;
}
int main(int argc, char* argv[]) {
bool set_pipewire_camera_state(const CallerIdentity& caller, bool disabled)
{
const std::string pw_cli = find_pw_cli();
if (pw_cli.empty())
return false;
std::string listing;
if (!run_as_caller(caller, {pw_cli, "ls", "Node"}, &listing))
return false;
const auto ids = parse_video_node_ids(listing);
if (ids.empty())
return false;
bool all_ok = true;
for (const auto& id : ids) {
const bool ok = run_as_caller(
caller,
{pw_cli, "set-param", id, "Device/Disabled", disabled ? "true" : "false"}
);
if (!ok) {
std::cerr << "[Privacy] Unable to update PipeWire camera node " << id << "\n";
all_ok = false;
}
}
return all_ok;
}
bool is_video_device_name(const std::string& name)
{
static constexpr const char prefix[] = "video";
if (name.rfind(prefix, 0) != 0 || name.size() <= sizeof(prefix) - 1)
return false;
return std::all_of(name.begin() + static_cast<std::ptrdiff_t>(sizeof(prefix) - 1),
name.end(),
[](unsigned char c) { return std::isdigit(c) != 0; });
}
bool ensure_state_directory()
{
struct stat st {};
if (lstat(kStateDir, &st) != 0) {
if (errno != ENOENT || mkdir(kStateDir, 0755) != 0)
return false;
if (lstat(kStateDir, &st) != 0)
return false;
}
if (!S_ISDIR(st.st_mode) || st.st_uid != 0 ||
(st.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
return false;
}
if (chown(kStateDir, 0, 0) != 0 || chmod(kStateDir, 0755) != 0)
return false;
return true;
}
int open_video_device(int dev_dir_fd, const std::string& name, struct stat& st)
{
if (!is_video_device_name(name))
return -1;
const int fd = openat(dev_dir_fd,
name.c_str(),
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOFOLLOW);
if (fd < 0)
return -1;
if (fstat(fd, &st) != 0 || !S_ISCHR(st.st_mode)) {
close(fd);
return -1;
}
return fd;
}
bool block_v4l2_devices()
{
if (!ensure_state_directory()) {
std::cerr << "[Privacy] Unsafe or unavailable state directory\n";
return false;
}
const int state_fd = open(kStateFile,
O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW,
0600);
if (state_fd < 0)
return false;
struct stat state_st {};
if (fstat(state_fd, &state_st) != 0 ||
!S_ISREG(state_st.st_mode) ||
state_st.st_uid != 0 ||
state_st.st_nlink != 1) {
close(state_fd);
unlink(kStateFile);
return false;
}
fchmod(state_fd, 0600);
const int dev_fd = open("/dev", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (dev_fd < 0) {
close(state_fd);
unlink(kStateFile);
return false;
}
DIR* dev_dir = fdopendir(dev_fd);
if (!dev_dir) {
close(dev_fd);
close(state_fd);
unlink(kStateFile);
return false;
}
std::size_t blocked = 0;
while (dirent* entry = readdir(dev_dir)) {
const std::string name(entry->d_name);
struct stat st {};
const int fd = open_video_device(dirfd(dev_dir), name, st);
if (fd < 0)
continue;
const mode_t original_mode = st.st_mode & 07777;
const std::string record = name + " " +
std::to_string(static_cast<unsigned int>(major(st.st_rdev))) + " " +
std::to_string(static_cast<unsigned int>(minor(st.st_rdev))) + " " +
[&]() {
std::ostringstream out;
out << std::oct << static_cast<unsigned int>(original_mode);
return out.str();
}() + "\n";
const ssize_t written = write(state_fd, record.data(), record.size());
if (written == static_cast<ssize_t>(record.size()) && fchmod(fd, 0000) == 0)
++blocked;
close(fd);
}
closedir(dev_dir); // closes dev_fd
fsync(state_fd);
close(state_fd);
if (blocked == 0)
unlink(kStateFile);
// No matching device is an idempotent no-op, not an operational failure.
return true;
}
bool restore_legacy_v4l2_modes()
{
const int dev_fd = open("/dev", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (dev_fd < 0)
return false;
DIR* dev_dir = fdopendir(dev_fd);
if (!dev_dir) {
close(dev_fd);
return false;
}
while (dirent* entry = readdir(dev_dir)) {
const std::string name(entry->d_name);
struct stat st {};
const int fd = open_video_device(dirfd(dev_dir), name, st);
if (fd < 0)
continue;
if ((st.st_mode & 07777) == 0000)
fchmod(fd, 0660);
close(fd);
}
closedir(dev_dir);
return true;
}
bool restore_v4l2_devices()
{
const int state_fd = open(kStateFile, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
if (state_fd < 0) {
if (errno == ENOENT)
return restore_legacy_v4l2_modes();
return false;
}
struct stat state_st {};
if (fstat(state_fd, &state_st) != 0 ||
!S_ISREG(state_st.st_mode) ||
state_st.st_uid != 0 ||
state_st.st_nlink != 1 ||
(state_st.st_mode & (S_IRWXG | S_IRWXO)) != 0) {
close(state_fd);
return false;
}
FILE* state = fdopen(state_fd, "r");
if (!state) {
close(state_fd);
return false;
}
const int dev_fd = open("/dev", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (dev_fd < 0) {
fclose(state);
return false;
}
char* line = nullptr;
std::size_t capacity = 0;
while (getline(&line, &capacity, state) >= 0) {
std::istringstream record(line);
std::string name;
unsigned int expected_major = 0;
unsigned int expected_minor = 0;
std::string mode_text;
std::string extra;
if (!(record >> name >> expected_major >> expected_minor >> mode_text) ||
(record >> extra) ||
!is_video_device_name(name)) {
continue;
}
unsigned long parsed_mode = 0;
try {
std::size_t consumed = 0;
parsed_mode = std::stoul(mode_text, &consumed, 8);
if (consumed != mode_text.size() || parsed_mode > 07777)
continue;
} catch (...) {
continue;
}
struct stat st {};
const int fd = open_video_device(dev_fd, name, st);
if (fd < 0)
continue;
if (major(st.st_rdev) == expected_major &&
minor(st.st_rdev) == expected_minor) {
fchmod(fd, static_cast<mode_t>(parsed_mode));
}
close(fd);
}
free(line);
close(dev_fd);
fclose(state); // closes state_fd
unlink(kStateFile);
return true;
}
} // namespace
int main(int argc, char* argv[])
{
const uid_t real_uid = getuid();
const uid_t effective_uid = geteuid();
const CallerIdentity caller = resolve_caller_identity();
// A setuid installation is explicitly unsupported. Privilege must be
// granted by pkexec/sudo so the authorization step remains visible.
if (effective_uid == 0 && real_uid != 0) {
std::cerr << "[Privacy] Refusing setuid execution; use pkexec or sudo\n";
return 11;
}
clearenv();
setenv("PATH", "/usr/sbin:/usr/bin:/sbin:/bin", 1);
if (geteuid() != 0) {
std::cerr << _("Errore: questo helper deve essere eseguito come root.\n");
if (effective_uid != 0) {
std::cerr << "[Privacy] This helper must be executed as root via pkexec or sudo\n";
return 10;
}
@ -158,31 +560,20 @@ int main(int argc, char* argv[]) {
return 1;
}
std::string cmd = argv[1];
bool ok = false;
const std::string action(argv[1]);
if (has_pipewire()) {
if (cmd == "block") {
ok = pipewire_disable_camera();
} else if (cmd == "unblock") {
ok = pipewire_enable_camera();
} else {
std::cerr << _("Comando non valido: ") << cmd << "\n";
return 2;
}
} else {
if (cmd == "block") {
block_v4l2_devices();
ok = true;
} else if (cmd == "unblock") {
unblock_v4l2_devices();
ok = true;
} else {
std::cerr << _("Comando non valido: ") << cmd << "\n";
return 2;
}
if (action == "block") {
if (set_pipewire_camera_state(caller, true))
return 0;
return block_v4l2_devices() ? 0 : 5;
}
return ok ? 0 : 5;
}
if (action == "unblock") {
const bool pipewire_ok = set_pipewire_camera_state(caller, false);
const bool v4l2_ok = restore_v4l2_devices();
return (pipewire_ok || v4l2_ok) ? 0 : 5;
}
std::cerr << "[Privacy] Invalid command: " << action << "\n";
return 2;
}