Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
**/__pycache__/**
build/**
8 changes: 5 additions & 3 deletions csrc/binding.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include <nanobind/nanobind.h>
#include <nanobind/stl/string.h>
#include <nanobind/stl/vector.h>

#include <utility>
#include <random>
Expand All @@ -18,10 +19,10 @@ namespace nb = nanobind;

void do_bench(int result_fd, int input_fd, int supervisor_sock_fd, const std::string& kernel_qualname, const nb::object& test_generator,
const nb::dict& test_kwargs, std::uintptr_t stream, bool discard, bool nvtx, bool landlock, bool mseal,
bool allow_root) {
bool allow_root, const std::vector<std::string>& writable_paths) {
std::vector<char> signature_bytes(32);
auto config = read_benchmark_parameters(input_fd, signature_bytes.data());
auto mgr = make_benchmark_manager(result_fd, signature_bytes, config.Seed, discard, nvtx, landlock, mseal, allow_root, supervisor_sock_fd);
auto mgr = make_benchmark_manager(result_fd, signature_bytes, config.Seed, discard, nvtx, landlock, mseal, allow_root, supervisor_sock_fd, writable_paths);
cleanse(signature_bytes.data(), 32);

{
Expand Down Expand Up @@ -63,7 +64,8 @@ NB_MODULE(_pygpubench, m) {
nb::arg("nvtx") = false,
nb::arg("landlock") = true,
nb::arg("mseal") = true,
nb::arg("allow_root") = false
nb::arg("allow_root") = false,
nb::arg("writable_paths") = std::vector<std::string>{"/tmp"}
);

m.def("run_supervisor", [](int sock_fd) {
Expand Down
13 changes: 9 additions & 4 deletions csrc/landlock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
#include <linux/landlock.h>
#include <seccomp.h>
#include <system_error>
#include <string>
#include <vector>
#include <unordered_set>
#include <utility>
#include <sstream>
Expand Down Expand Up @@ -83,7 +85,7 @@ static void allow_path(LandlockFd& ruleset, const char *path, uint64_t access) {
landlock_add_rule(ruleset, LANDLOCK_RULE_PATH_BENEATH, &attr, 0);
}

void install_landlock() {
void install_landlock(const std::vector<std::string>& writable_paths) {
const std::uint64_t RO = LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR;

Expand Down Expand Up @@ -111,9 +113,12 @@ void install_landlock() {
// Read-only: entire filesystem
allow_path(ruleset_fd, "/", RO);

// Read-write: /tmp and /dev only
allow_path(ruleset_fd, "/tmp", RW);
allow_path(ruleset_fd, "/dev", RW); // needed for /dev/null etc, used e.g., by triton
// Read-write: /dev is always needed for /dev/null etc, used e.g., by triton.
allow_path(ruleset_fd, "/dev", RW);
// Additional writable paths are caller-configurable (defaults to /tmp).
for (const std::string& path : writable_paths) {
allow_path(ruleset_fd, path.c_str(), RW);
}

// required for landlock
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) {
Expand Down
35 changes: 27 additions & 8 deletions csrc/manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
#include "manager.h"
#include "utils.h"
#include "check.h"
#include <algorithm>
#include <chrono>
#include <cuda_runtime.h>
#include <numeric>
#include <optional>
#include <system_error>
#include <cstdlib>
Expand All @@ -27,7 +29,7 @@ static constexpr std::size_t ArenaSize = 2 * 1024 * 1024;
static constexpr std::size_t BenchmarkManagerArenaSize = 128 * 1024 * 1024;

extern void clear_cache(void* dummy_memory, int size, bool discard, cudaStream_t stream);
extern void install_landlock();
extern void install_landlock(const std::vector<std::string>& writable_paths);
extern bool mseal_supported();
extern void seal_mappings();
extern bool supports_seccomp_notify();
Expand Down Expand Up @@ -139,7 +141,8 @@ void BenchmarkManagerDeleter::operator()(BenchmarkManager* p) const noexcept {

BenchmarkManagerPtr make_benchmark_manager(
int result_fd, const std::vector<char>& signature, std::uint64_t seed,
bool discard, bool nvtx, bool landlock, bool mseal, bool allow_root, int supervisor_socket)
bool discard, bool nvtx, bool landlock, bool mseal, bool allow_root, int supervisor_socket,
const std::vector<std::string>& writable_paths)
{
const std::size_t page_size = static_cast<std::size_t>(getpagesize());
const std::size_t alloc_size = (BenchmarkManagerArenaSize + page_size - 1) & ~(page_size - 1);
Expand All @@ -155,7 +158,7 @@ BenchmarkManagerPtr make_benchmark_manager(
raw = new (mem) BenchmarkManager(
static_cast<std::byte*>(mem), alloc_size,
result_fd, signature, seed,
discard, nvtx, landlock, mseal, allow_root, supervisor_socket);
discard, nvtx, landlock, mseal, allow_root, supervisor_socket, writable_paths);
} catch (...) {
// If construction throws, release the mmap'd region before propagating.
if (munmap(mem, alloc_size) != 0) {
Expand All @@ -170,7 +173,8 @@ BenchmarkManagerPtr make_benchmark_manager(

BenchmarkManager::BenchmarkManager(std::byte* arena, std::size_t arena_size,
int result_fd, const std::vector<char>& signature, std::uint64_t seed, bool discard,
bool nvtx, bool landlock, bool mseal, bool allow_root, int supervisor_socket)
bool nvtx, bool landlock, bool mseal, bool allow_root, int supervisor_socket,
const std::vector<std::string>& writable_paths)
: mArena(arena),
mResource(arena + sizeof(BenchmarkManager),
arena_size - sizeof(BenchmarkManager),
Expand Down Expand Up @@ -202,6 +206,7 @@ BenchmarkManager::BenchmarkManager(std::byte* arena, std::size_t arena_size,

mNVTXEnabled = nvtx;
mLandlock = landlock;
mWritablePaths = writable_paths;
mSeal = mseal;
mAllowRoot = allow_root;
mDiscardCache = discard;
Expand All @@ -225,6 +230,8 @@ BenchmarkManager::~BenchmarkManager() {
for (auto& exp: mExpectedOutputs) cudaFree(exp.Value);
}

static nb::tuple ensure_contiguous_tuple(const nb::tuple& tup);

std::pair<std::vector<nb::tuple>, std::vector<nb::tuple>> BenchmarkManager::setup_benchmark(const nb::callable& generate_test_case, const nb::dict& kwargs, int repeats) {
std::mt19937_64 rng(mSeed);
std::uniform_int_distribution<std::uint64_t> dist(0, std::numeric_limits<std::uint64_t>::max());
Expand All @@ -244,14 +251,26 @@ std::pair<std::vector<nb::tuple>, std::vector<nb::tuple>> BenchmarkManager::setu
call_kwargs["seed"] = dist(rng);

auto gen = nb::cast<nb::tuple>(generate_test_case(**call_kwargs));
kernel_args[i] = nb::cast<nb::tuple>(gen[0]);
expected[i] = nb::cast<nb::tuple>(gen[1]);
kernel_args[i] = ensure_contiguous_tuple(nb::cast<nb::tuple>(gen[0]));
expected[i] = ensure_contiguous_tuple(nb::cast<nb::tuple>(gen[1]));
}
return std::make_pair(std::move(kernel_args), std::move(expected));
}

bool can_convert_to_tensor(nb::handle obj) {
return nb::isinstance<nb_cuda_array>(obj);
return nb::isinstance<nb_any_cuda_array>(obj);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this something better handled at the python side?

}

static nb::tuple ensure_contiguous_tuple(const nb::tuple& tup) {
nb::list new_tup;
for (auto item : tup) {
if (nb::isinstance<nb_any_cuda_array>(item)) {
new_tup.append(nb::cast<nb::object>(item).attr("contiguous")());
} else {
new_tup.append(item);
}
}
return nb::tuple(new_tup);
}

auto BenchmarkManager::make_shadow_args(const nb::tuple& args, cudaStream_t stream,
Expand Down Expand Up @@ -332,7 +351,7 @@ void BenchmarkManager::install_protections() {

// restrict access to file system
if (mLandlock)
install_landlock();
install_landlock(mWritablePaths);

if (mSeal) {
if (!mseal_supported()) {
Expand Down
13 changes: 10 additions & 3 deletions csrc/manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <cstdio>
#include <fstream>
#include <memory_resource>
#include <string>
#include <vector>
#include <cuda_runtime.h>
#include <memory>
Expand All @@ -22,6 +23,9 @@
namespace nb = nanobind;

using nb_cuda_array = nb::ndarray<nb::c_contig, nb::device::cuda>;
/// Broader type that accepts any CUDA ndarray (including noncontiguous),
/// used only for detection before forcing contiguity.
using nb_any_cuda_array = nb::ndarray<nb::device::cuda>;

struct BenchmarkParameters {
std::uint64_t Seed;
Expand All @@ -43,7 +47,8 @@ using BenchmarkManagerPtr = std::unique_ptr<BenchmarkManager, BenchmarkManagerDe

BenchmarkManagerPtr make_benchmark_manager(
int result_fd, const std::vector<char>& signature, std::uint64_t seed,
bool discard, bool nvtx, bool landlock, bool mseal, bool allow_root, int supervisor_socket);
bool discard, bool nvtx, bool landlock, bool mseal, bool allow_root, int supervisor_socket,
const std::vector<std::string>& writable_paths);


class BenchmarkManager {
Expand All @@ -53,14 +58,15 @@ class BenchmarkManager {
void send_report();
void clean_up();
private:
friend BenchmarkManagerPtr make_benchmark_manager(int result_fd, const std::vector<char>& signature, std::uint64_t seed, bool discard, bool nvtx, bool landlock, bool mseal, bool allow_root, int supervisor_socket);
friend BenchmarkManagerPtr make_benchmark_manager(int result_fd, const std::vector<char>& signature, std::uint64_t seed, bool discard, bool nvtx, bool landlock, bool mseal, bool allow_root, int supervisor_socket, const std::vector<std::string>& writable_paths);
friend BenchmarkManagerDeleter;
/// `arena` is the mmap region that owns all memory for this object and its vectors.
/// The BenchmarkManager must have been placement-newed into the front of that region;
/// the rest is used as a monotonic PMR arena for internal vectors.
BenchmarkManager(std::byte* arena, std::size_t arena_size,
int result_fd, const std::vector<char>& signature, std::uint64_t seed,
bool discard, bool nvtx, bool landlock, bool mseal, bool allow_root, int supervisor_socket);
bool discard, bool nvtx, bool landlock, bool mseal, bool allow_root, int supervisor_socket,
const std::vector<std::string>& writable_paths);
~BenchmarkManager();

struct Expected {
Expand Down Expand Up @@ -109,6 +115,7 @@ class BenchmarkManager {
bool mNVTXEnabled = false;
bool mDiscardCache = true;
bool mLandlock = true;
std::vector<std::string> mWritablePaths;
bool mSeal = true;
bool mAllowRoot = false;
int mSupervisorSock = -1;
Expand Down
7 changes: 6 additions & 1 deletion csrc/supervisor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,12 @@ static bool handle_notification(int unotify_fd, const Config& cfg) {

if (ioctl(unotify_fd, SECCOMP_IOCTL_NOTIF_RECV, &req) < 0) {
if (errno == EINTR) return true;
if (errno == ENODEV) return false;
// ENODEV: all filter users gone. ENOENT: the notifying task died
// before we could RECV its notification (the same "target died"
// condition the NOTIF_SEND path below treats as benign). Both mean
// no live syscall is left to police -- the tracee has exited -- so
// stop the loop quietly instead of perror'ing teardown noise.
if (errno == ENODEV || errno == ENOENT) return false;
perror("supervisor: SECCOMP_IOCTL_NOTIF_RECV");
return false;
}
Expand Down
7 changes: 6 additions & 1 deletion python/pygpubench/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
def _do_bench_impl(out_fd: "multiprocessing.connection.Connection", in_fd: "multiprocessing.connection.Connection", supervisor_sock: "socket.socket",
qualname: str, test_generator: TestGeneratorInterface,
test_args: dict, stream: int = None, discard: bool = True,
nvtx: bool = False, tb_conn: "multiprocessing.connection.Connection" = None, landlock=True, mseal=True, allow_root=False):
nvtx: bool = False, tb_conn: "multiprocessing.connection.Connection" = None, landlock=True, mseal=True, allow_root=False,
writable_paths=("/tmp",)):
"""
Benchmarks the kernel referred to by `qualname` against the test case returned by `test_generator`.
:param out_fd: Writable file descriptor to which benchmark results are written.
Expand All @@ -46,6 +47,7 @@ def _do_bench_impl(out_fd: "multiprocessing.connection.Connection", in_fd: "mult
:param landlock: Whether to enable landlock. Enabled by default, prevents write access to the file system outside /tmp.
:param mseal: Whether to enable memory sealing. Enabled by default, prevents making executable mappings writable.
:param allow_root: Whether to allow the benchmark to run as root (opt-in via ``allow_root=True``). When run as root, the benchmark process's memory can be read through /proc/self/mem despite being protected.
:param writable_paths: Filesystem paths (and everything beneath them) the benchmark is allowed to write to when landlock is enabled. Defaults to ``("/tmp",)``. ``/dev`` is always writable (needed by e.g. triton); the rest of the filesystem stays read-only.
"""
if stream is None:
import torch
Expand All @@ -66,6 +68,7 @@ def _do_bench_impl(out_fd: "multiprocessing.connection.Connection", in_fd: "mult
landlock,
mseal,
allow_root,
list(writable_paths),
)
except BaseException:
if tb_conn is not None:
Expand Down Expand Up @@ -157,6 +160,7 @@ def do_bench_isolated(
landlock = True,
mseal = True,
allow_root = False,
writable_paths = ("/tmp",),
) -> BenchmarkResult:
"""
Runs kernel benchmark (`do_bench_impl`) in a subprocess for proper isolation.
Expand Down Expand Up @@ -204,6 +208,7 @@ def do_bench_isolated(
landlock,
mseal,
allow_root,
writable_paths,
),
)

Expand Down