Skip to content
Merged
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
4 changes: 4 additions & 0 deletions cpp/include/zerobuffer/platform.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ class SharedMemory {

// Open existing shared memory
static std::unique_ptr<SharedMemory> open(const std::string& name);

// Open existing shared memory read-only (PROT_READ / O_RDONLY) so a
// lower-privilege consumer can attach to a segment it may not write.
static std::unique_ptr<SharedMemory> open_readonly(const std::string& name);

// Get memory pointer
virtual void* data() = 0;
Expand Down
5 changes: 4 additions & 1 deletion cpp/src/latest_frame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,10 @@ bool LatestFrameReader::try_attach() {
}
std::unique_ptr<SharedMemory> shm;
try {
shm = SharedMemory::open(_name);
// Read-only mapping: the reader is a pure consumer, so a lower-privilege
// process can attach to a segment created by a higher-privilege writer
// (e.g. root-owned 0644) without EACCES from an O_RDWR open.
shm = SharedMemory::open_readonly(_name);
} catch (const ZeroBufferException&) {
return false; // absent -> caller polls
}
Expand Down
15 changes: 10 additions & 5 deletions cpp/src/platform_linux.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,12 @@ size_t align_to_boundary(size_t size, size_t alignment) {
// Linux SharedMemory implementation
class LinuxSharedMemory : public SharedMemory {
public:
LinuxSharedMemory(const std::string& name, size_t size, bool create)
LinuxSharedMemory(const std::string& name, size_t size, bool create, bool read_only = false)
: name_(name), size_(size), fd_(-1), data_(nullptr) {
int flags = create ? (O_CREAT | O_EXCL | O_RDWR) : O_RDWR;

int flags = create ? (O_CREAT | O_EXCL | O_RDWR) : (read_only ? O_RDONLY : O_RDWR);
mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; // 0666 - read/write for all

fd_ = shm_open(name.c_str(), flags, mode);
if (fd_ == -1) {
throw ZeroBufferException("Failed to open shared memory: " + std::string(strerror(errno)));
Expand All @@ -110,7 +110,8 @@ class LinuxSharedMemory : public SharedMemory {
size_ = static_cast<size_t>(st.st_size);
}

data_ = mmap(nullptr, size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0);
int prot = read_only ? PROT_READ : (PROT_READ | PROT_WRITE);
data_ = mmap(nullptr, size_, prot, MAP_SHARED, fd_, 0);
if (data_ == MAP_FAILED) {
close(fd_);
if (create) shm_unlink(name.c_str());
Expand Down Expand Up @@ -157,6 +158,10 @@ std::unique_ptr<SharedMemory> SharedMemory::open(const std::string& name) {
return std::make_unique<LinuxSharedMemory>(name, 0, false);
}

std::unique_ptr<SharedMemory> SharedMemory::open_readonly(const std::string& name) {
return std::make_unique<LinuxSharedMemory>(name, 0, false, true);
}

void SharedMemory::remove(const std::string& name) {
shm_unlink(name.c_str());
// Ignore errors - shared memory might not exist
Expand Down
17 changes: 11 additions & 6 deletions cpp/src/platform_windows.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ size_t align_to_boundary(size_t size, size_t alignment) {
// Windows SharedMemory implementation
class WindowsSharedMemory : public SharedMemory {
public:
WindowsSharedMemory(const std::string& name, size_t size, bool create)
WindowsSharedMemory(const std::string& name, size_t size, bool create, bool read_only = false)
: name_(name), size_(size), handle_(NULL), data_(nullptr) {

std::string fullName = "Global\\" + name;

DWORD access = read_only ? FILE_MAP_READ : FILE_MAP_ALL_ACCESS;

if (create) {
LARGE_INTEGER liSize;
liSize.QuadPart = size;
Expand All @@ -74,13 +75,13 @@ class WindowsSharedMemory : public SharedMemory {
throw ZeroBufferException("Shared memory already exists");
}
} else {
handle_ = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, fullName.c_str());
handle_ = OpenFileMappingA(access, FALSE, fullName.c_str());
if (handle_ == NULL) {
throw ZeroBufferException("Failed to open shared memory: " + std::to_string(GetLastError()));
}
}
data_ = MapViewOfFile(handle_, FILE_MAP_ALL_ACCESS, 0, 0, size);

data_ = MapViewOfFile(handle_, access, 0, 0, size);
if (data_ == nullptr) {
CloseHandle(handle_);
throw ZeroBufferException("Failed to map shared memory: " + std::to_string(GetLastError()));
Expand Down Expand Up @@ -121,6 +122,10 @@ std::unique_ptr<SharedMemory> SharedMemory::open(const std::string& name) {
return std::make_unique<WindowsSharedMemory>(name, 0, false);
}

std::unique_ptr<SharedMemory> SharedMemory::open_readonly(const std::string& name) {
return std::make_unique<WindowsSharedMemory>(name, 0, false, true);
}

void SharedMemory::remove(const std::string& name) {
// Windows doesn't have persistent shared memory that needs cleanup
// Shared memory is automatically cleaned up when all handles are closed
Expand Down
26 changes: 26 additions & 0 deletions cpp/tests/test_latest_frame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -402,3 +402,29 @@ TEST(LatestFrameTest, StaleSegmentReclaimedOnCreate) {
ASSERT_TRUE(f.valid);
EXPECT_EQ(f.sequence, 9u);
}

// The reader attaches with a read-only (PROT_READ) mapping: it must still read
// the published payload back byte-for-byte, and keep tracking further publishes.
// This proves read-only READS work; the cross-UID privilege boundary (root
// writer / non-root reader) is validated by the live run, not by same-user CI.
TEST(LatestFrameTest, ReadOnlyReaderReadsPublishedFrame) {
std::string name = make_name("readonly");
constexpr size_t FRAME = 4096;

LatestFrameWriter writer(name, FRAME, 3);
publish_pattern(writer, 42, FRAME);

LatestFrameReader reader(name); // now opens the segment read-only
Captured f = read_capture(reader, 1000ms);
ASSERT_TRUE(f.valid);
EXPECT_EQ(f.sequence, 42u);
EXPECT_EQ(f.size, FRAME);
EXPECT_TRUE(all_bytes_equal(f.bytes.data(), f.bytes.size(), 42));

// A later publish is still picked up through the read-only mapping.
publish_pattern(writer, 7, FRAME);
Captured f2 = read_capture(reader, 1000ms);
ASSERT_TRUE(f2.valid);
EXPECT_EQ(f2.sequence, 7u);
EXPECT_TRUE(all_bytes_equal(f2.bytes.data(), f2.bytes.size(), 7));
}
Loading