mirror of
https://github.com/veracrypt/VeraCrypt.git
synced 2026-10-22 06:12:35 +00:00
The test compiles the requester helper code into a small harness and runs it as root on an ordinary file owned by an unprivileged user. It checks that the helper runs in a freshly executed process with a reduced environment, descriptors 0-2 on /dev/null and no other inherited descriptor, with and without close_range, including when a library is found only through LD_LIBRARY_PATH, as in the AppImage. It also checks that a failed or incomplete start is reported as a startup failure and never falls back to read-only access, and that invalid requests and a request socket whose peer is not root are refused.
324 lines
14 KiB
Python
324 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""Check requester-helper self-exec and fail-closed startup using production code.
|
|
|
|
After a matching Linux build:
|
|
sudo python3 Tests/test_linux_requester_helper.py --build-dir src --user NAME
|
|
|
|
Uses an ordinary disposable file, not encrypted volumes or secret material.
|
|
The harness links the matching Platform library and wraps execve/sendmsg only
|
|
in the test executable. It checks close_range, its older-kernel fallback, and
|
|
an AppImage-like dependency found only through LD_LIBRARY_PATH, and that the
|
|
helper refuses a socket whose peer is not root.
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
from pathlib import Path
|
|
import pwd
|
|
import shlex
|
|
import subprocess
|
|
import tempfile
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
HARNESS = r'''
|
|
#include <sys/resource.h>
|
|
#include <sys/syscall.h>
|
|
#ifdef TEST_NO_CLOSE_RANGE
|
|
#undef SYS_close_range
|
|
#endif
|
|
#include "Platform/Unix/RequesterAccess.cpp"
|
|
#include <stddef.h>
|
|
#include <stdio.h>
|
|
#include <sys/un.h>
|
|
#include <iostream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
|
|
using namespace VeraCrypt;
|
|
static bool FreshHelper;
|
|
static int ExecError;
|
|
static bool ExitBeforeReady;
|
|
static int ParentOnlyMarker;
|
|
static const int InheritedFd = 70000;
|
|
#ifdef TEST_BUNDLED_LIBRARY
|
|
extern "C" int vc_requester_test_dependency ();
|
|
#endif
|
|
|
|
extern "C" int __real_execve (const char *, char *const [], char *const []);
|
|
extern "C" int __wrap_execve (const char *path, char *const argv[], char *const envp[])
|
|
{
|
|
if (ExecError) { errno = ExecError; return -1; }
|
|
if (ExitBeforeReady) _exit (127); // Simulate an early dynamic-loader exit.
|
|
return __real_execve (path, argv, envp);
|
|
}
|
|
|
|
static bool OnDevNull (int fd)
|
|
{
|
|
struct stat st, null;
|
|
return fstat (fd, &st) == 0 && stat ("/dev/null", &null) == 0 && S_ISCHR (st.st_mode) && st.st_rdev == null.st_rdev;
|
|
}
|
|
|
|
extern "C" ssize_t __real_sendmsg (int, const struct msghdr *, int);
|
|
extern "C" ssize_t __wrap_sendmsg (int fd, const struct msghdr *message, int flags)
|
|
{
|
|
// Only the credential-dropped helper sends replies. An ordinary fork cannot
|
|
// pass this check: it retains the parent's marker and never re-enters main.
|
|
if (geteuid() != 0 && (!FreshHelper || ParentOnlyMarker != 0
|
|
|| getenv ("VC_REQUESTER_TEST_ENV") || prctl (PR_GET_DUMPABLE, 0, 0, 0, 0) != 0
|
|
|| !OnDevNull (0) || !OnDevNull (1) || !OnDevNull (2)
|
|
|| fcntl (InheritedFd, F_GETFD) != -1 || errno != EBADF))
|
|
{
|
|
errno = EPERM;
|
|
return -1;
|
|
}
|
|
return __real_sendmsg (fd, message, flags);
|
|
}
|
|
|
|
static void Require (bool condition, const char *message)
|
|
{
|
|
if (!condition) throw std::runtime_error (message);
|
|
}
|
|
|
|
struct TestFile : File { using File::OpenAsRequester; };
|
|
|
|
// Only root may send requests. Here the requester listens on the socket, so the
|
|
// helper sees the requester as its peer and must exit without answering.
|
|
static void CheckNonRootPeer (uid_t userId, gid_t groupId)
|
|
{
|
|
struct sockaddr_un address;
|
|
memset (&address, 0, sizeof (address));
|
|
address.sun_family = AF_UNIX;
|
|
snprintf (address.sun_path + 1, sizeof (address.sun_path) - 1, "vc-requester-peer-%d", static_cast <int> (getpid()));
|
|
socklen_t addressSize = offsetof (struct sockaddr_un, sun_path) + 1 + strlen (address.sun_path + 1);
|
|
|
|
int ready[2];
|
|
Require (pipe (ready) == 0, "pipe");
|
|
pid_t listener = fork();
|
|
Require (listener != -1, "fork listener");
|
|
if (listener == 0)
|
|
{
|
|
close (ready[0]);
|
|
int listening = -1;
|
|
if (setgroups (1, &groupId) != 0 || setgid (groupId) != 0 || setuid (userId) != 0
|
|
|| (listening = socket (AF_UNIX, SOCK_SEQPACKET, 0)) == -1
|
|
|| bind (listening, reinterpret_cast <struct sockaddr *> (&address), addressSize) != 0
|
|
|| listen (listening, 1) != 0 || write (ready[1], "r", 1) != 1)
|
|
_exit (2);
|
|
int connection = accept (listening, nullptr, nullptr);
|
|
char data;
|
|
_exit (connection != -1 && recv (connection, &data, sizeof (data), 0) == 0 ? 0 : 1);
|
|
}
|
|
|
|
close (ready[1]);
|
|
char data;
|
|
bool listening = read (ready[0], &data, 1) == 1;
|
|
close (ready[0]);
|
|
int socketFd = listening ? socket (AF_UNIX, SOCK_SEQPACKET, 0) : -1;
|
|
bool connected = socketFd != -1
|
|
&& connect (socketFd, reinterpret_cast <struct sockaddr *> (&address), addressSize) == 0;
|
|
int serviceStatus = -1;
|
|
if (connected)
|
|
{
|
|
pid_t service = fork();
|
|
if (service == 0)
|
|
_exit (File::RunRequesterHelperService (std::to_string (socketFd).c_str()));
|
|
Require (service != -1 && waitpid (service, &serviceStatus, 0) == service, "run helper service");
|
|
}
|
|
else
|
|
kill (listener, SIGKILL);
|
|
if (socketFd != -1)
|
|
close (socketFd);
|
|
int listenerStatus;
|
|
Require (waitpid (listener, &listenerStatus, 0) == listener, "wait for listener");
|
|
Require (connected, "connect to the requester's socket");
|
|
Require (WIFEXITED (serviceStatus) && WEXITSTATUS (serviceStatus) == 1, "helper service did not fail");
|
|
Require (WIFEXITED (listenerStatus) && WEXITSTATUS (listenerStatus) == 0, "helper answered a non-root peer");
|
|
}
|
|
|
|
int main (int argc, char **argv)
|
|
{
|
|
if (argc == 3 && strcmp (argv[1], TC_REQUESTER_HELPER_CMDLINE_OPTION) == 0)
|
|
{
|
|
FreshHelper = true;
|
|
return File::RunRequesterHelperService (argv[2]);
|
|
}
|
|
try
|
|
{
|
|
Require (argc == 4 && geteuid() == 0, "expected root: UID GID FILE");
|
|
#ifdef TEST_BUNDLED_LIBRARY
|
|
Require (vc_requester_test_dependency() == 1, "bundled library dependency");
|
|
#endif
|
|
ParentOnlyMarker = 123;
|
|
setenv ("VC_REQUESTER_TEST_ENV", "parent-only", 1);
|
|
int file = open (argv[3], O_RDONLY | O_CLOEXEC);
|
|
Require (file != -1, "open disposable file");
|
|
struct rlimit originalLimit, limit;
|
|
Require (getrlimit (RLIMIT_NOFILE, &originalLimit) == 0, "get fd limit");
|
|
limit = originalLimit;
|
|
if (limit.rlim_cur <= InheritedFd) limit.rlim_cur = InheritedFd + 1;
|
|
Require (setrlimit (RLIMIT_NOFILE, &limit) == 0, "raise soft fd limit");
|
|
Require (fcntl (file, F_DUPFD, InheritedFd) == InheritedFd, "create inherited high fd");
|
|
Require (setrlimit (RLIMIT_NOFILE, &originalLimit) == 0, "restore soft fd limit");
|
|
|
|
RequesterHelperRequest request;
|
|
memset (&request, 0, sizeof (request));
|
|
request.OpenFile = 1;
|
|
strcpy (request.Path, argv[3]);
|
|
request.Flags = O_RDWR | O_CLOEXEC | O_NOCTTY | O_NONBLOCK;
|
|
request.UserId = static_cast <uid_t> (atoi (argv[1]));
|
|
request.GroupId = static_cast <gid_t> (atoi (argv[2]));
|
|
RequesterHelperReply reply;
|
|
int descriptor;
|
|
Require (RunRequesterHelperProcess (request, reply, descriptor) == 0
|
|
&& reply.Stage == RequesterHelperSucceeded && descriptor != -1, "fresh open helper");
|
|
Require ((fcntl (descriptor, F_GETFD) & FD_CLOEXEC) != 0, "received fd is CLOEXEC");
|
|
Require ((fcntl (descriptor, F_GETFL) & O_NONBLOCK) == 0, "received fd is blocking");
|
|
std::cout << "PASS fresh open helper, minimal environment, descriptor cleanup, 0-2 on /dev/null\n";
|
|
|
|
RequesterHelperRequest timesRequest;
|
|
memset (×Request, 0, sizeof (timesRequest));
|
|
strcpy (timesRequest.Path, argv[3]);
|
|
timesRequest.UserId = request.UserId;
|
|
timesRequest.GroupId = request.GroupId;
|
|
timesRequest.Times[0].tv_sec = 1500000000;
|
|
timesRequest.Times[1].tv_sec = 1500000100;
|
|
int unused;
|
|
Require (RunRequesterHelperProcess (timesRequest, reply, unused) == 0
|
|
&& reply.Stage == RequesterHelperSucceeded && unused == -1, "fresh timestamp helper");
|
|
struct stat st;
|
|
Require (stat (argv[3], &st) == 0 && st.st_atime == timesRequest.Times[0].tv_sec
|
|
&& st.st_mtime == timesRequest.Times[1].tv_sec, "timestamps not restored through the path");
|
|
std::cout << "PASS fresh timestamp helper\n";
|
|
|
|
ExecError = EACCES;
|
|
timesRequest.Times[0].tv_sec += 100;
|
|
timesRequest.Times[1].tv_sec += 100;
|
|
Require (RunRequesterHelperProcess (timesRequest, reply, unused) == EACCES
|
|
&& reply.Stage == RequesterHelperStartup && unused == -1,
|
|
"failed exec must not run timestamp helper");
|
|
Require (stat (argv[3], &st) == 0 && st.st_atime == 1500000000 && st.st_mtime == 1500000100,
|
|
"failed exec changed timestamps");
|
|
request.Flags = O_RDONLY | O_CLOEXEC | O_NOCTTY | O_NONBLOCK;
|
|
Require (RunRequesterHelperProcess (request, reply, unused) == EACCES
|
|
&& reply.Stage == RequesterHelperStartup && unused == -1,
|
|
"failed exec must not run opening helper");
|
|
ExecError = ENOENT;
|
|
Require (RunRequesterHelperProcess (request, reply, unused) == ENOENT
|
|
&& reply.Stage == RequesterHelperStartup && unused == -1, "exec errno lost");
|
|
setenv ("SUDO_UID", argv[1], 1);
|
|
setenv ("SUDO_GID", argv[2], 1);
|
|
try
|
|
{
|
|
TestFile file;
|
|
file.OpenAsRequester (FilePath (argv[3]), O_RDWR);
|
|
throw std::runtime_error ("expected startup exception");
|
|
}
|
|
catch (const ExternalException &e)
|
|
{
|
|
string text = StringConverter::ToSingle (e.GetSubject());
|
|
Require (text.find ("Cannot start VeraCrypt's requester-access process (/proc/self/exe)") != string::npos
|
|
&& text.find (strerror (ENOENT)) != string::npos, "startup diagnostic lost context or errno");
|
|
}
|
|
std::cout << "PASS exact startup errno, diagnostic, and no fork-only/read-only fallback\n";
|
|
|
|
ExecError = 0;
|
|
ExitBeforeReady = true;
|
|
Require (RunRequesterHelperProcess (request, reply, unused) != 0
|
|
&& reply.Stage == RequesterHelperStartup && unused == -1, "early exit was not a startup failure");
|
|
try
|
|
{
|
|
TestFile file;
|
|
file.OpenAsRequester (FilePath (argv[3]), O_RDWR);
|
|
throw std::runtime_error ("expected early-exit exception");
|
|
}
|
|
catch (const ExternalException &e)
|
|
{
|
|
Require (e.GetSubject().find (L"did not report readiness") != wstring::npos, "early-exit diagnostic");
|
|
}
|
|
ExitBeforeReady = false;
|
|
std::cout << "PASS early exit reported as failure to become ready\n";
|
|
|
|
request.Flags |= O_TRUNC;
|
|
Require (RunRequesterHelperProcess (request, reply, unused) != 0 && unused == -1,
|
|
"invalid request accepted");
|
|
Require (fstat (descriptor, &st) == 0 && st.st_size != 0, "invalid request truncated file");
|
|
request.Flags &= ~O_TRUNC;
|
|
RequesterHelperRequest noPath = timesRequest;
|
|
noPath.Path[0] = '\0';
|
|
Require (RunRequesterHelperProcess (noPath, reply, unused) != 0 && unused == -1,
|
|
"timestamp request without a path accepted");
|
|
memset (request.Path, 'a', sizeof (request.Path));
|
|
Require (RunRequesterHelperProcess (request, reply, unused) != 0 && unused == -1,
|
|
"unterminated path accepted");
|
|
strcpy (request.Path, argv[3]);
|
|
request.UserId = 0;
|
|
Require (RunRequesterHelperProcess (request, reply, unused) != 0 && unused == -1,
|
|
"root requester accepted");
|
|
std::cout << "PASS invalid requests rejected\n";
|
|
|
|
CheckNonRootPeer (static_cast <uid_t> (atoi (argv[1])), static_cast <gid_t> (atoi (argv[2])));
|
|
std::cout << "PASS request socket from a non-root peer refused\n";
|
|
|
|
close (descriptor);
|
|
close (InheritedFd);
|
|
close (file);
|
|
Require (waitpid (-1, nullptr, WNOHANG) == -1 && errno == ECHILD, "unreaped helper");
|
|
return 0;
|
|
}
|
|
catch (const std::exception &e) { std::cerr << e.what() << '\n'; return 1; }
|
|
}
|
|
'''
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--build-dir", type=Path, default=ROOT / "src")
|
|
parser.add_argument("--user", required=True)
|
|
args = parser.parse_args()
|
|
if os.geteuid() != 0:
|
|
parser.error("run as root")
|
|
user = pwd.getpwnam(args.user)
|
|
if user.pw_uid == 0:
|
|
parser.error("--user must be unprivileged")
|
|
library = args.build_dir.resolve() / "Platform" / "Platform.a"
|
|
if not library.is_file():
|
|
parser.error("build VeraCrypt first, or supply --build-dir")
|
|
with tempfile.TemporaryDirectory(prefix="vc-requester-helper-") as directory:
|
|
root = Path(directory)
|
|
root.chmod(0o755)
|
|
source = root / "helper.cpp"
|
|
source.write_text(HARNESS)
|
|
target = root / "plain-file"
|
|
target.write_text("Disposable helper test data\n")
|
|
os.chown(target, user.pw_uid, user.pw_gid)
|
|
compiler = shlex.split(os.environ.get("CXX", "c++"))
|
|
bundled = root / "lib"
|
|
bundled.mkdir()
|
|
dependency = root / "dependency.cpp"
|
|
dependency.write_text('extern "C" int vc_requester_test_dependency () { return 1; }\n')
|
|
subprocess.run(compiler + ["-shared", "-fPIC", str(dependency), "-o",
|
|
str(bundled / "libvc_requester_test.so")], check=True)
|
|
for mode in ("close-range", "fd-fallback", "bundled-library"):
|
|
binary = root / ("helper-" + mode)
|
|
command = compiler + [
|
|
"-std=c++11", "-O2", "-Wall", "-DTC_LINUX", "-D_FILE_OFFSET_BITS=64",
|
|
"-I" + str(ROOT / "src"), str(source), str(library), "-pthread",
|
|
"-Wl,--wrap=execve", "-Wl,--wrap=sendmsg", "-o", str(binary),
|
|
]
|
|
env = os.environ.copy()
|
|
if mode == "fd-fallback":
|
|
command.append("-DTEST_NO_CLOSE_RANGE")
|
|
if mode == "bundled-library":
|
|
command += ["-DTEST_BUNDLED_LIBRARY", "-L" + str(bundled), "-lvc_requester_test"]
|
|
env["LD_LIBRARY_PATH"] = str(bundled)
|
|
subprocess.run(command, check=True)
|
|
print("Testing", mode, flush=True)
|
|
subprocess.run([str(binary), str(user.pw_uid), str(user.pw_gid), str(target)],
|
|
check=True, timeout=40, env=env)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|