diff --git a/cpp/include/zerobuffer/platform.h b/cpp/include/zerobuffer/platform.h index 36a15e8..cebb6f7 100644 --- a/cpp/include/zerobuffer/platform.h +++ b/cpp/include/zerobuffer/platform.h @@ -45,6 +45,10 @@ class SharedMemory { // Open existing shared memory static std::unique_ptr 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 open_readonly(const std::string& name); // Get memory pointer virtual void* data() = 0; diff --git a/cpp/src/latest_frame.cpp b/cpp/src/latest_frame.cpp index d4c69e1..9632828 100644 --- a/cpp/src/latest_frame.cpp +++ b/cpp/src/latest_frame.cpp @@ -281,7 +281,10 @@ bool LatestFrameReader::try_attach() { } std::unique_ptr 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 } diff --git a/cpp/src/platform_linux.cpp b/cpp/src/platform_linux.cpp index 6655e60..475f2a9 100644 --- a/cpp/src/platform_linux.cpp +++ b/cpp/src/platform_linux.cpp @@ -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))); @@ -110,7 +110,8 @@ class LinuxSharedMemory : public SharedMemory { size_ = static_cast(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()); @@ -157,6 +158,10 @@ std::unique_ptr SharedMemory::open(const std::string& name) { return std::make_unique(name, 0, false); } +std::unique_ptr SharedMemory::open_readonly(const std::string& name) { + return std::make_unique(name, 0, false, true); +} + void SharedMemory::remove(const std::string& name) { shm_unlink(name.c_str()); // Ignore errors - shared memory might not exist diff --git a/cpp/src/platform_windows.cpp b/cpp/src/platform_windows.cpp index ddbc826..7ba241f 100644 --- a/cpp/src/platform_windows.cpp +++ b/cpp/src/platform_windows.cpp @@ -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; @@ -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())); @@ -121,6 +122,10 @@ std::unique_ptr SharedMemory::open(const std::string& name) { return std::make_unique(name, 0, false); } +std::unique_ptr SharedMemory::open_readonly(const std::string& name) { + return std::make_unique(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 diff --git a/cpp/tests/test_latest_frame.cpp b/cpp/tests/test_latest_frame.cpp index 28533fa..8d8bd84 100644 --- a/cpp/tests/test_latest_frame.cpp +++ b/cpp/tests/test_latest_frame.cpp @@ -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)); +}