Commit Graph
2543 Commits
Author SHA1 Message Date
andrey 0e0dc0cd38 test: make regression tests fail cleanly on pre-fix code
- dispatcher tests: register the server before the client connects
  (Connect race), poll the Connected ack non-blocking with a deadline,
  keep the client socket non-blocking so a full buffer yields EAGAIN
  instead of a blocking send() past any deadline
- piethernet test: use a TCP client against a never-reading listener
  (server-object writeDevice is a no-op) and 32 MB to exceed the
  ~10 MB loopback buffers

With piethernet/pistreampacker reverted to master all four tests
fail (8 ms .. 40 s); with the fixes ctest is 342/342.
2026-08-24 17:20:49 +03:00
andrey 3abf0a0808 test: add regression tests for dispatcher write stalls and stream packer size cap
- PICloudDispatcher.ServerDisconnectWhileClientForwarding: a disconnecting
  logical server must not freeze the dispatcher while a client's read
  thread is jammed forwarding data to an unread server socket
- PICloudDispatcher.ClientRemovableWhileServerNotReading: the client's
  own Disconnect frame must be processed and the client removed even
  though the stall happens in the other connection's writeDevice()
- PIEthernet.WriteThreadStopsWhilePeerNotReading: the EAGAIN retry loop
  in writeDevice() must not prevent stopAndWait() from returning
- PIStreamPacker.RejectsOversizedPacket: a forged INT_MAX packet size
  from the stream must not be accumulated, and the packer must stay
  usable for valid packets
2026-08-24 15:26:34 +03:00
andrey 49dc3cefe7 fix(picloud-dispatcher): drop data to a connection whose write is stalled
While the peer does not drain its receive buffer, every forwarded packet
would re-stall the reader thread for up to writeTimeout(), so a queued
Disconnect frame could wait behind the whole backlog. Remember a stalled
target (send slower than 2 s) and drop data for a short cooldown instead
of stalling; keep dispatcher writes bounded at 3 s. Control frames
(Connected/Disconnected) are unaffected.
2026-08-24 15:14:45 +03:00
andrey a9e356810a fix(piethernet): bound the EAGAIN retry loop in writeDevice by writeTimeout
The loop may run in a thread that belongs to another device (the picloud
dispatcher forwards data from one connection's read thread to another
connection's socket), so isThreadedReadStopping() alone cannot stop it:
it sees the wrong device's flag. Stalling for at most writeTimeout()
without progress makes every write finite; the receiver resynchronizes
on the next packet signature.
2026-08-24 15:14:30 +03:00
andrey 22a09ddd1c fix(picloud-dispatcher): start read thread under the lock and drain rmrf_* in the destructor
newConnection pushed the client into clients while holding map_mutex
but started its read thread after releasing it; in that window the
client was a live member of clients with no read thread and could be
marked, closed and deleted by the drainer before start() ran. Start
the thread inside the locked section: the thread can only wait for
map_mutex after the function has released it, so this is not a
deadlock, and the 'everything in clients is started' invariant holds.

~DispatcherServer dropped clients/servers pending in rmrf_clients/
rmrf_servers without closing or deleting them (object leak of live
PIObjects). Collect them together with the main maps, then run the
same stop/close/delete sequence. The sets are disjoint with the main
maps by construction (items are removed from the main maps when
marked), so there are no duplicates.
2026-08-23 18:55:15 +03:00
andrey 1ec61a0127 fix(picloud-dispatcher): forward data to sub-client under mutex_clients
The dataReadedServer lambda looked up the sub-client under
mutex_clients but called sendData() after the unlock; the client could
be deleted by the drainer in between (same UAF pattern that was fixed
in PICloudServer::_readed). Keep the send inside the locked section:
sendData only takes mutex_send/prog_s_mutex downstream, so the lock
hold is bounded.
2026-08-23 18:55:00 +03:00
andrey 48d6ffaac1 Merge branch 'master' into fix_picloud_dispatcher 2026-08-23 17:19:06 +03:00
andrey cc8e8644f1 fix(picloud-dispatcher): do not close/delete clients under map_mutex — deadlock
cleanClients() (timer thread) called close()/stopAndWait() and
disconnectClient() (read thread) did the same while holding
map_mutex, while client read threads wait for the same lock in
registerServer/registerClient/disconnectClient — a guaranteed
deadlock (the -w watchdog aborts exactly this). disconnectClient
could also wait for its own read thread (self-wait in
waitThreadedReadFinished) and the destructor never stopped the timer
or drained the clients, leaving dangling lambda captures of this.

Restructure around a single drainer (the timer thread):
- cleanClients: under map_mutex only mark (move rmrf_* out, mark
  ping-timeout/orphaned clients); after the unlock close
  (stopAndWait) and delete the connections, then delete the servers
- disconnectClient: only mark under the lock, then request a
  non-blocking stop() on the affected connections; the drainer waits
  and deletes (no self-wait, nothing blocks while holding the lock)
- newConnection: start the read thread after releasing the lock
- ~DispatcherServer: stop watchdog/timer first, then close and delete
  all clients before the servers (raw-pointer order) and eth

The watchdog is now a PIThread member so the destructor can stop it;
setMaxConnections takes map_mutex like its readers.
2026-08-23 17:18:03 +03:00
andrey ddab6909b4 fix(picloud-dispatcher): guard CloudServer::stop() and last_ping with mutex_clients
stop() iterated clients while addClient()/removeClient() modified it
from other threads (reallocated vector iterators, data race). Copy the
list under mutex_clients and close the copy.

last_ping (PITimeMeasurer) was reset from the read thread
(dataReadedServer/pingReceived) without any lock while cleanClients
read it via lastPing() in the timer thread. Reset and read it under
mutex_clients (lock order map_mutex -> mutex_clients, consistent with
the registerClient path).
2026-08-23 17:17:53 +03:00
andrey d6d432f3c6 fix(picloud): take mutex_buff around buff in internalDisconnect and bytesAvailable
PICloudClient::internalDisconnect() cleared buff without mutex_buff
while the eth read thread appends to it concurrently (close() races
with in-flight Data frames), and bytesAvailable() read buff.size()
lock-free on both PICloudClient and PICloudServer::Client. Take the
lock in both places (mutex_buff made mutable for the const getter).
2026-08-23 17:17:47 +03:00
andrey 6810f40ad9 fix(picloudserver): keep logical client under clients_mutex in _readed
In the Data case the client pointer was looked up under clients_mutex
but used (pushBuffer) after the unlock; a user-initiated delete() of
the same client raced with the read thread and hit freed memory.
In the Disconnect case the client was stopped/closed and appended to
removed_clients_ (without the lock, unlike every other site) after
the unlock, with the same UAF window and a race on the vector itself.

Do both uses while holding clients_mutex: a concurrent delete() is
serialized with this block by clientDeleted() (the deleted() event),
so the client cannot be freed while it is in use. Setting
is_connected=false first also stops pushBuffer from buffering into a
dying client.
2026-08-23 17:17:41 +03:00
andrey 8bac29d655 fix(picloudtcp): snapshot header under mutex_send in send* and validate payload in parse*
sendStart/sendConnected/sendDisconnected/sendData/sendPing mutated
the shared header.type and serialized the header without holding
mutex_send (it only covered streampacker->send), so concurrent
senders (e.g. ping timer vs app thread on PICloudServer) could mix
type/role bytes of different frames. Snapshot the header into a
local copy under mutex_send in every send* and setRole now takes the
lock as well.

parseDataServer/parseConnect/parseDisconnect read a uint without
checking the payload size; PIByteArray does a partial memcpy on
undersized input, so a short frame produced a garbage client id.
Return the default value when the payload is shorter than sizeof(uint).
2026-08-23 17:17:34 +03:00
andrey 407c4bb141 fix(pistreampacker): use declared mutexes and cap payload size read from stream
prog_s_mutex/prog_r_mutex were declared but never locked, so
concurrent send()/received()/clear()/receivePacketProgress() raced on
shared buffers and fragments of different senders interleaved on the
wire. Lock the receive state (stream/packet) with prog_r_mutex in
received()/clear()/receivePacketProgress() and the whole emission of
one packet with prog_s_mutex in send().

Additionally the size field of a packet taken from the stream was
only checked for negativity: a remote peer could declare INT_MAX and
the packer accumulated memory unboundedly, swallowing all following
data as phantom payload (permanent desync, no resync). Add a
max_payload_size cap (config, default 64MB) and resynchronize byte by
byte when the declared size exceeds it, so valid packets after the
corrupted one survive.

Also drop frames whose size does not fit the 32-bit wire format
instead of wrapping the length field.
2026-08-23 17:17:27 +03:00
andrey c5fb880f1c fix(piethernet): EAGAIN retry loop in writeDevice now checks stop state
Socket is non-blocking while BlockingWrite is set for every PIEthernet
(construct()), so a stalled peer made ::send return EAGAIN and the
retry loop spun forever. The thread could no longer be stopped by
stop()/stopAndWait() (interrupt only wakes event waits), which turned
into a permanent hang for PIStreamPacker-based users such as the
picloud dispatcher. Break out of the loop when the device is
stopping and return -1.
2026-08-23 17:17:15 +03:00
peri4 b1cab18496 version 5.9.0 2026-08-17 20:36:43 +03:00
peri4 b1060fd572 PIEthernet fix: no more close outdated fd on disconnect (error at work with MHD) 2026-08-17 20:26:36 +03:00
andrey aed98059de Merge pull request 'add rpm support for deploy_tool' (#211) from rpm_support into master
Reviewed-on: #211
2026-08-12 17:05:48 +03:00
andrey 9214e805d9 add rpm support for deploy_tool 2026-08-11 19:04:33 +03:00
andrey 5fe03eaf96 fix ctest 2026-08-11 14:41:25 +03:00
andrey a81a450c44 Merge pull request 'Bugfixes part 3' (#209) from bugfixes3 into master
Reviewed-on: #209
2026-08-11 11:16:55 +03:00
andrey f2d692725a version 2026-08-11 11:15:57 +03:00
andrey c8f6fe7641 Reapply "fix: PISingleApplication TOCTOU — two shm reads replaced with single atomic read"
This reverts commit fc2e49b096.
2026-08-10 21:39:31 +03:00
andrey ace5960e70 fix(piusb): fix 4 bugs in USB read/write/enumeration/close
1. Missing break in readDevice() Bulk case: Bulk read result was
   overwritten by interrupt read, silently corrupting all bulk reads.

2. Wrong variable in writeDevice(): switch checked ep_read instead
   of ep_write; Interrupt case used ep_read.address for writes.

3. Interface mismatch in closeDevice(): always released interface 0
   (intefrace_) instead of the actually claimed interface
   (interface_claimed), leaking non-zero interface claims.

4. Wrong index in interface enumeration: used outer loop variable c
   (configuration index) instead of i (interface index), reading
   wrong interface descriptors or causing OOB access.
2026-08-10 16:26:30 +03:00
andrey 69673ff7c6 fix(curl_thread_pool): clean up clients_in_proc in destroy()
destroy() aborted and deleted clients in the queue but did not
delete clients that were currently being processed (clients_in_proc).
If a thread was terminated while processing a client, that client
object and its CURL handle were leaked.
2026-08-10 16:26:23 +03:00
andrey 9a4384a378 fix(piopencl): release context on command queue creation failure
If clCreateContext succeeded but clCreateCommandQueue failed,
the context handle was returned without being released, leaking
the OpenCL context and all associated GPU resources.
2026-08-10 16:26:18 +03:00
andrey 4d2c268710 fix(piluaprogram): add destructor to close Lua state
luaL_newstate() allocates a Lua state (~2-4 MB with libraries)
in the constructor, but there was no destructor to call lua_close().
Every PILuaProgram instance leaked its entire Lua state.
Add ~PILuaProgram() that calls lua_close(PRIVATE->lua_state).
2026-08-10 16:26:13 +03:00
andrey 9294cd7b7b fix(picrypt): use crypto_box_* constants for public-key decrypt
The decrypt() function for crypto_box (public-key) used
crypto_secretbox_NONCEBYTES and crypto_secretbox_MACBYTES
(secret-key constants) instead of crypto_box_* constants.
These happen to be equal in libsodium but are semantically
different APIs. Fix ensures consistency with the encrypt()
counterpart.
2026-08-10 16:26:08 +03:00
andrey 16adea05af fix(picompress): pass correct source size to uncompress()
piDecompress() appended original size as 8-byte ullong at the end
of compressed data, but passed zba.size() (including the 8 bytes)
to uncompress(). This fed trailing metadata as part of the zlib
stream. Fix: subtract sizeof(ullong) from source length.
2026-08-10 16:26:02 +03:00
andrey aa64eb4c7f fix(pibasetransfer): fix ABBA deadlock — enforce consistent mutex lock ordering
received() acquired mutex_header and mutex_session in different orders
depending on packet type:
  - pt_Data path:  mutex_header → mutex_session
  - pt_Start path: mutex_session → mutex_header

send_process() acquires mutex_session independently. When running
concurrently with received(), the reversed lock ordering creates a
classic ABBA deadlock scenario.

Fix: enforce consistent ordering (mutex_header → mutex_session) in
all code paths. Restructured pt_Start case and buildSession() to
always acquire mutex_header before mutex_session.
2026-08-10 15:48:40 +03:00
andrey c3bdc5464e fix(piconnection): prevent UAF in DevicePool::run() during connection deletion
The background run() thread copies allConnections() then iterates
each connection's diags_ map. If a PIConnection is deleted from
another thread between the copy and the iteration, dereferencing
the pointer is UAF. Add null check and protect diags_ iteration
with __device_pool__ lock.
2026-08-10 15:48:31 +03:00
andrey b180d91b1e fix(pibasetransfer): make shared flags std::atomic<bool> to eliminate data race
Flags break_, is_sending, is_receiving, is_pause are accessed from
both the send thread (send_process) and the read thread (received)
without synchronization. Plain bool reads/writes from multiple threads
is undefined behavior per C++ standard. Convert to std::atomic<bool>
with proper initializers.
2026-08-10 15:48:24 +03:00
andrey 1c9cdbba19 fix(picloudserver): use piMin<ssize_t> to avoid signed/unsigned UB in Client::readDevice
piMini(max_size, buff.size()) implicitly narrows size_t to int,
which is UB for large buffers. Replace with explicit piMin<ssize_t>
matching the correct pattern used in picloudclient.cpp.
2026-08-10 15:48:12 +03:00
andrey f9a846a3e9 Revert "fix(PIFFT_float): correct plan entry size from 4 to 8"
This reverts commit 2bb3884aee.
2026-08-10 14:59:18 +03:00
andrey fc2e49b096 Revert "fix: PISingleApplication TOCTOU — two shm reads replaced with single atomic read"
This reverts commit 1b23a258b8.
2026-08-10 14:59:13 +03:00
andrey 1a153eefbc fix(PISystemInfo): correct guard for df output parsing
The guard ml.size_s() < 2 allowed size==2, but ml[2] was then
accessed — OOB read on truncated or unusual df output lines.
2026-08-06 17:01:30 +03:00
andrey 9e280ef976 fix(PISerial): close fd when probe read fails in availableDevicesInfo
When the non-blocking probe read returned EIO, the loop continued
without closing the file descriptor, leaking one fd per rejected
port on each call.
2026-08-06 17:01:30 +03:00
andrey c8ad8ecaf4 fix(PISPI): close fd on ioctl error paths
Three ioctl calls after successful open() returned false without
closing the file descriptor, leaking one fd per failed open attempt.
2026-08-06 17:01:30 +03:00
andrey 1aa6f1af81 fix(PIByteArray): prevent OOB in fromBase64 with non-multiple-of-4 input
The buffer was sized as floor(sz/4)*3 but the loop ran ceil(sz/4)
times, writing 3 bytes per iteration. For sz%4!=0, this wrote past
the buffer end. Also guarded sz<4 to avoid processing trivially
short or malformed input.
2026-08-06 17:01:30 +03:00
andrey 5e4a55cc0c fix(PIThread): remove spurious unlock() in _waitForFinish
The function called unlock() on thread_mutex without a matching
lock(), causing UB on Windows (releasing an unowned critical
section) or silently dropping a user-held lock.
2026-08-06 17:01:30 +03:00
andrey 31ae52623d fix(PIConditionVariable): clamp negative timeout in waitFor
When elapsed time exceeded the timeout, the remaining milliseconds
expression went negative and was implicitly converted to DWORD,
wrapping to ~0xFFFFFFFF (5 days) — effectively INFINITE.
Now clamps to 0 and returns false when time has elapsed.
2026-08-06 17:01:30 +03:00
andrey 4d9cd20ac5 fix(PIDir): initialize list pointer and check scandir return
dirent** list was uninitialized, so scandir failure (returning -1)
left it with indeterminate value. The unconditional free(list) at
the end was then UB — typically heap corruption or crash.
2026-08-06 17:01:30 +03:00
andrey 3e72f4e533 fix(PIBitArray): guard pop_back() against empty array
pop_back() called resize(size_ - 1) without checking for empty.
On empty array, uint underflow produced UINT_MAX, causing
bytesInBits(UINT_MAX) overflow and subsequent OOB access.
pop_front() already had this guard; pop_back() was missing it.
2026-08-06 17:01:30 +03:00
andrey c3cd9496dd fix(PIVariant): add missing break in setValueFromString switch
Three complex type cases (pivComplexf, pivComplexd, pivComplexld)
lacked break statements, causing fallthrough to pivTime. Setting
a complex variant from string silently overwrote the value with
PITime::fromString(), corrupting the variant's type and data.
2026-08-06 17:01:30 +03:00
andrey ef8fdb30f4 fix(PISharedMemory): reset data and unlink on mmap failure
When mmap() returned MAP_FAILED, data stayed as (void*)-1, causing
subsequent read/write/close to pass the null guard and crash.
Now resets data=nullptr, and calls shm_unlink when owner=true to
avoid orphaned shared memory objects.
2026-08-06 17:01:30 +03:00
andrey 9c3690d371 fix(PIFFT_float): correct plan entry size from 4 to 8
The float FFT plan generator wrote 8 ints per entry (indices +0..+7)
but declared entrysize=4, causing heap overwrite into the next entry
and incorrect plan array sizing.
2026-08-06 17:01:30 +03:00
andrey 288c1b3575 fix: Sender::tick accesses parent->diags_ without lock — data race / use-after-free 2026-08-06 17:01:30 +03:00
andrey f5cf06c200 fix: addDevice deletes read thread without stopAndWait — race on dev close/open 2026-08-06 17:01:30 +03:00
andrey 4f601a9408 fix: removeAllFilters holds lock during delete — move destructors outside lock scope 2026-08-06 17:01:30 +03:00
andrey c3491cbf6f fix: PISingleApplication TOCTOU — two shm reads replaced with single atomic read 2026-08-06 17:01:30 +03:00
andrey 96f0221051 fix: PIStreamPacker division by zero when max_packet_size is 0 2026-08-06 17:01:30 +03:00