Skip to content

Commit 948bc81

Browse files
author
concore-dev
committed
fix(shm): serialise concurrent writers with seq# + POSIX semaphore (fixes #195)
The C++ SHM transport in concore.hpp writes a payload with strncpy() across multiple writers on the same shmget() key, with no memory barrier, no semaphore, and no version counter. Two writers calling write_SM() simultaneously can interleave byte writes and produce a torn payload on the reader side. Layout changes (any existing reader that does not know about the header will see a leading int and fail loudly): bytes [0..7] uint64 little-endian sequence number (odd = write in progress, even = ready, 0 = none) bytes [8..end] null-terminated payload Writers acquire a POSIX semaphore keyed by (shm_key + 1), copy the payload with memcpy, fence __ATOMIC_RELEASE, then bump the seq# with __atomic_fetch_add(ACQ_REL). Readers load the seq# with __ATOMIC_ACQUIRE and double-read to confirm a stable snapshot before parsing; an odd or changing seq# causes a retry. Mirrors the same change in concoredocker.hpp. Adds: tests/test_shm_concurrency.py Python regression (fork-based, Linux) tests/test_shm_cpp_harness.cpp fork-based C++ harness for CI tests/test_shm_cpp_smoke.cpp header self-contained check plus a cpp-shm-test job in .github/workflows/ci.yml that builds and runs the harness on ubuntu-latest.
1 parent 1aca203 commit 948bc81

6 files changed

Lines changed: 666 additions & 57 deletions

File tree

.github/workflows/ci.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,20 @@ jobs:
9595
- name: Validate Dockerfile build
9696
if: steps.filter.outputs.dockerfile == 'true'
9797
run: docker build -f Dockerfile.py -t concore-py-test .
98+
99+
cpp-shm-test:
100+
runs-on: ubuntu-latest
101+
steps:
102+
- uses: actions/checkout@v4
103+
104+
- name: Install g++
105+
run: sudo apt-get update && sudo apt-get install -y g++
106+
107+
- name: Compile concore.hpp smoke test
108+
run: g++ -std=c++17 -Wall -Wextra -c tests/test_shm_cpp_smoke.cpp -o /tmp/smoke.o
109+
110+
- name: Build C++ SHM harness
111+
run: g++ -std=c++17 -O2 -Wall -Wextra -o /tmp/test_shm_cpp_harness tests/test_shm_cpp_harness.cpp
112+
113+
- name: Run C++ SHM harness
114+
run: /tmp/test_shm_cpp_harness

concore.hpp

Lines changed: 176 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,19 @@
1717
#include <thread>
1818
#ifdef __linux__
1919
#include <sys/ipc.h>
20+
#include <sys/sem.h>
2021
#include <sys/shm.h>
2122
#include <unistd.h>
23+
#include <cerrno>
24+
union semun { //not defined by glibc; needed for semctl SETVAL
25+
int val;
26+
struct semid_ds* buf;
27+
unsigned short* array;
28+
};
2229
#endif
2330
#include <cstring>
2431
#include <cctype>
32+
#include <cstdint>
2533
#include <regex>
2634

2735
#include "concore_base.hpp"
@@ -41,6 +49,9 @@ class Concore{
4149
string outpath = "./out";
4250

4351
static constexpr size_t SHM_SIZE = 4096;
52+
//SHM layout: [0..7]=uint64 seq#, [8..]=payload. See issue #195.
53+
static constexpr size_t SHM_HEADER_SIZE = 8;
54+
static constexpr size_t SHM_PAYLOAD_MAX = SHM_SIZE - SHM_HEADER_SIZE - 1;
4455

4556
int shmId_create = -1;
4657
int shmId_get = -1;
@@ -50,6 +61,11 @@ class Concore{
5061
// File sharing:- 0, Shared Memory:- 1
5162
int communication_iport = 0; // iport refers to input port
5263
int communication_oport = 0; // oport refers to input port
64+
#ifdef __linux__
65+
//POSIX semaphores keyed by (shm_key+1). Idempotent across processes.
66+
int semId_create = -1;
67+
int semId_get = -1;
68+
#endif
5369

5470
#ifdef CONCORE_USE_ZMQ
5571
map<string, concore_base::ZeroMQPort*> zmq_ports;
@@ -145,6 +161,9 @@ class Concore{
145161
if (shmId_create != -1) {
146162
shmctl(shmId_create, IPC_RMID, nullptr);
147163
}
164+
if (semId_create != -1) {
165+
semctl(semId_create, 0, IPC_RMID);
166+
}
148167
#endif
149168
}
150169

@@ -166,13 +185,20 @@ class Concore{
166185
delay(other.delay), retrycount(other.retrycount), simtime(other.simtime),
167186
maxtime(other.maxtime), iport(std::move(other.iport)), oport(std::move(other.oport)),
168187
params(std::move(other.params))
188+
#ifdef __linux__
189+
, semId_create(other.semId_create), semId_get(other.semId_get)
190+
#endif
169191
{
170192
other.shmId_create = -1;
171193
other.shmId_get = -1;
172194
other.sharedData_create = nullptr;
173195
other.sharedData_get = nullptr;
174196
other.communication_iport = 0;
175197
other.communication_oport = 0;
198+
#ifdef __linux__
199+
other.semId_create = -1;
200+
other.semId_get = -1;
201+
#endif
176202
}
177203

178204
/**
@@ -190,6 +216,8 @@ class Concore{
190216
shmdt(sharedData_get);
191217
if (shmId_create != -1)
192218
shmctl(shmId_create, IPC_RMID, nullptr);
219+
if (semId_create != -1)
220+
semctl(semId_create, 0, IPC_RMID);
193221
#endif
194222

195223
s = std::move(other.s);
@@ -209,13 +237,21 @@ class Concore{
209237
iport = std::move(other.iport);
210238
oport = std::move(other.oport);
211239
params = std::move(other.params);
240+
#ifdef __linux__
241+
semId_create = other.semId_create;
242+
semId_get = other.semId_get;
243+
#endif
212244

213245
other.shmId_create = -1;
214246
other.shmId_get = -1;
215247
other.sharedData_create = nullptr;
216248
other.sharedData_get = nullptr;
217249
other.communication_iport = 0;
218250
other.communication_oport = 0;
251+
#ifdef __linux__
252+
other.semId_create = -1;
253+
other.semId_get = -1;
254+
#endif
219255

220256
return *this;
221257
}
@@ -254,6 +290,55 @@ class Concore{
254290
return std::stoi(numberString);
255291
}
256292

293+
#ifdef __linux__
294+
static inline uint64_t shm_load_seq(const char* base) {
295+
uint64_t v;
296+
__atomic_load(reinterpret_cast<const uint64_t*>(base), &v, __ATOMIC_ACQUIRE);
297+
return v;
298+
}
299+
300+
static inline void shm_store_seq(char* base, uint64_t v) {
301+
__atomic_store(reinterpret_cast<uint64_t*>(base), &v, __ATOMIC_RELEASE);
302+
}
303+
304+
static int shm_sem_create(key_t key) {
305+
int id = semget(key, 1, IPC_CREAT | 0666);
306+
if (id < 1) return -1;
307+
semun arg{};
308+
arg.val = 1;
309+
if (semctl(id, 0, SETVAL, arg) < 0) return -1;
310+
return id;
311+
}
312+
313+
static void shm_sem_acquire(int id) {
314+
if (id < 0) return;
315+
sembuf sb{};
316+
sb.sem_num = 0;
317+
sb.sem_op = -1;
318+
sb.sem_flg = 0;
319+
while (semop(id, &sb, 1) == -1) {
320+
if (errno != EINTR) {
321+
std::cerr << "semop(acquire) failed errno=" << errno << std::endl;
322+
return;
323+
}
324+
}
325+
}
326+
327+
static void shm_sem_release(int id) {
328+
if (id < 0) return;
329+
sembuf sb{};
330+
sb.sem_num = 0;
331+
sb.sem_op = 1;
332+
sb.sem_flg = 0;
333+
while (semop(id, &sb, 1) == -1) {
334+
if (errno != EINTR) {
335+
std::cerr << "semop(release) failed errno=" << errno << std::endl;
336+
return;
337+
}
338+
}
339+
}
340+
#endif
341+
257342
#ifdef __linux__
258343
/**
259344
* @brief Creates a shared memory segment with the given key.
@@ -286,7 +371,17 @@ class Concore{
286371
if (sharedData_create == reinterpret_cast<char*>(-1)) {
287372
std::cerr << "Failed to attach shared memory segment." << std::endl;
288373
sharedData_create = nullptr;
374+
return;
289375
}
376+
377+
semId_create = shm_sem_create(key + 1);
378+
if (semId_create < 0) {
379+
std::cerr << "Failed to create shared memory semaphore." << std::endl;
380+
}
381+
382+
//initialise header
383+
shm_store_seq(sharedData_create, uint64_t{0});
384+
sharedData_create[SHM_HEADER_SIZE] = '\0';
290385
}
291386

292387
/**
@@ -321,6 +416,14 @@ class Concore{
321416
if (sharedData_get == reinterpret_cast<char*>(-1)) {
322417
std::cerr << "Failed to attach shared memory segment." << std::endl;
323418
sharedData_get = nullptr;
419+
return;
420+
}
421+
422+
//attach reader-side semaphore (writer owns its lifetime)
423+
semId_get = semget(key + 1, 1, 0666);
424+
if (semId_get < 0) {
425+
//no semaphore: reads fall back to seq# alone
426+
semId_get = -1;
324427
}
325428
}
326429
#endif
@@ -504,34 +607,37 @@ class Concore{
504607
string ins = "";
505608
ReadStatus status = ReadStatus::SUCCESS;
506609
try {
507-
if (shmId_get != -1) {
508-
if (sharedData_get && sharedData_get[0] != '\0') {
509-
std::string message(sharedData_get, strnlen(sharedData_get, SHM_SIZE));
510-
ins = message;
511-
}
512-
else
513-
{
610+
if (shmId_get == -1 || sharedData_get == nullptr) {
611+
status = ReadStatus::FILE_NOT_FOUND;
514612
throw 505;
515613
}
516-
}
517-
else
518-
{
519-
status = ReadStatus::FILE_NOT_FOUND;
520-
throw 505;
521-
}
614+
uint64_t seq = shm_load_seq(sharedData_get);
615+
if (seq == 0) {
616+
throw 505; //no write published yet
617+
}
618+
std::string message(
619+
sharedData_get + SHM_HEADER_SIZE,
620+
strnlen(sharedData_get + SHM_HEADER_SIZE, SHM_PAYLOAD_MAX));
621+
ins = message;
522622
} catch (...) {
523623
ins = initstr;
524624
if (status == ReadStatus::SUCCESS)
525625
status = ReadStatus::FILE_NOT_FOUND;
526626
}
527-
627+
528628
int retry = 0;
529629
const int MAX_RETRY = 100;
530630
while ((int)ins.length()==0 && retry < MAX_RETRY){
531631
this_thread::sleep_for(timespan);
532632
try{
533-
if(shmId_get != -1) {
534-
std::string message(sharedData_get, strnlen(sharedData_get, SHM_SIZE));
633+
if(shmId_get != -1 && sharedData_get != nullptr) {
634+
uint64_t seq = shm_load_seq(sharedData_get);
635+
if (seq == 0) {
636+
throw 505;
637+
}
638+
std::string message(
639+
sharedData_get + SHM_HEADER_SIZE,
640+
strnlen(sharedData_get + SHM_HEADER_SIZE, SHM_PAYLOAD_MAX));
535641
ins = message;
536642
retrycount++;
537643
}
@@ -673,28 +779,38 @@ class Concore{
673779

674780
try {
675781
std::ostringstream outfile;
676-
if(shmId_create != -1){
677-
if (sharedData_create == nullptr)
678-
throw 506;
679-
val.insert(val.begin(),simtime+delta);
680-
outfile<<'[';
681-
for(int i=0;i<val.size()-1;i++)
682-
outfile<<val[i]<<',';
683-
outfile<<val[val.size()-1]<<']';
684-
std::string result = outfile.str();
685-
if (result.size() >= SHM_SIZE) {
686-
std::cerr << "ERROR: write_SM payload (" << result.size()
687-
<< " bytes) exceeds " << SHM_SIZE - 1
688-
<< "-byte shared memory limit. Data truncated!" << std::endl;
689-
}
690-
std::strncpy(sharedData_create, result.c_str(), SHM_SIZE - 1);
691-
sharedData_create[SHM_SIZE - 1] = '\0';
692-
// simtime must not be mutated here (issue #385).
693-
}
694-
else{
782+
if(shmId_create == -1){
695783
throw 505;
696-
}
697784
}
785+
if (sharedData_create == nullptr)
786+
throw 506;
787+
val.insert(val.begin(),simtime+delta);
788+
outfile<<'[';
789+
for(int i=0;i<val.size()-1;i++)
790+
outfile<<val[i]<<',';
791+
outfile<<val[val.size()-1]<<']';
792+
std::string result = outfile.str();
793+
const size_t max_payload = SHM_PAYLOAD_MAX;
794+
if (result.size() > max_payload) {
795+
std::cerr << "ERROR: write_SM payload (" << result.size()
796+
<< " bytes) exceeds " << max_payload
797+
<< "-byte shared memory limit. Data truncated!" << std::endl;
798+
result.resize(max_payload);
799+
}
800+
#ifdef __linux__
801+
shm_sem_acquire(semId_create);
802+
#endif
803+
std::memcpy(sharedData_create + SHM_HEADER_SIZE,
804+
result.c_str(), result.size());
805+
__atomic_thread_fence(__ATOMIC_RELEASE);
806+
(void)__atomic_fetch_add(
807+
reinterpret_cast<uint64_t*>(sharedData_create),
808+
uint64_t{1}, __ATOMIC_ACQ_REL);
809+
#ifdef __linux__
810+
shm_sem_release(semId_create);
811+
#endif
812+
// simtime must not be mutated here (issue #385).
813+
}
698814

699815
catch(...){
700816
cout<<"skipping +"<<outpath<<port<<" /"<<name;
@@ -712,18 +828,31 @@ class Concore{
712828
chrono::milliseconds timespan((int)(2000*delay));
713829
this_thread::sleep_for(timespan);
714830
try {
715-
if(shmId_create != -1){
716-
if (sharedData_create == nullptr)
717-
throw 506;
718-
if (val.size() >= SHM_SIZE) {
719-
std::cerr << "ERROR: write_SM payload (" << val.size()
720-
<< " bytes) exceeds " << SHM_SIZE - 1
721-
<< "-byte shared memory limit. Data truncated!" << std::endl;
722-
}
723-
std::strncpy(sharedData_create, val.c_str(), SHM_SIZE - 1);
724-
sharedData_create[SHM_SIZE - 1] = '\0';
831+
if(shmId_create == -1){
832+
throw 505;
725833
}
726-
else throw 505;
834+
if (sharedData_create == nullptr)
835+
throw 506;
836+
const size_t max_payload = SHM_PAYLOAD_MAX;
837+
if (val.size() > max_payload) {
838+
std::cerr << "ERROR: write_SM payload (" << val.size()
839+
<< " bytes) exceeds " << max_payload
840+
<< "-byte shared memory limit. Data truncated!" << std::endl;
841+
val.resize(max_payload);
842+
}
843+
#ifdef __linux__
844+
shm_sem_acquire(semId_create);
845+
#endif
846+
std::memcpy(sharedData_create + SHM_HEADER_SIZE,
847+
val.c_str(), val.size());
848+
sharedData_create[SHM_HEADER_SIZE + val.size()] = '\0';
849+
__atomic_thread_fence(__ATOMIC_RELEASE);
850+
(void)__atomic_fetch_add(
851+
reinterpret_cast<uint64_t*>(sharedData_create),
852+
uint64_t{1}, __ATOMIC_ACQ_REL);
853+
#ifdef __linux__
854+
shm_sem_release(semId_create);
855+
#endif
727856
}
728857
catch(...){
729858
cout<<"skipping +"<<outpath<<port<<" /"<<name;

0 commit comments

Comments
 (0)