ubuntu:20.04 has old compiler (g++-9) and mbedTLS 2.x.
ubuntu:22.04 ships mbedTLS 2.28.x; incompatible with our mbedTLS 3.x-only code.
ubuntu:24.04 has mbedTLS 3.x and libfuse3 3.16.x, covering the 3.12-3.17 range.
FUSE3 < 3.12 is verified locally but not representable in CI without fighting
old toolchains.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces single build job with four jobs covering all tested FUSE configs:
- fuse2: debian:trixie + libfuse-dev (FUSE_USE_VERSION=26)
- fuse3-legacy: ubuntu:20.04 + libfuse3-dev 3.9.x (fuse_loop_mt_31 path)
- fuse3-current: ubuntu:22.04 + libfuse3-dev 3.12.x
- fuse3-forky: debian:forky + libfuse3-dev 3.18.1
Each job verifies correct symbol linkage after build. Runtime mount tests
are outside CI scope (require privileged containers with /dev/fuse).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pfs_creat called pfs_create_file() but never set of->canmodify, leaving
it zero-initialized. pfs_write and pfs_ftruncate both check canmodify
and return -EACCES if false, making all writes to newly created files
fail with Permission denied.
All five open paths in pfs_open set canmodify correctly (added in #326),
but pfs_creat was missed. Fix by applying the same pattern immediately
after psql_unlock(), while fpath is still in scope.
Caught during FUSE 2/3 regression testing (Phase 2 write test).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The variable is declared extern in pfs.h and referenced in both
pfs.c (psync_signal_handler) and control_tools.cpp; the definition
must exist in pfs.c.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The function was static and had no remaining call sites after the
-ononempty/-ohard_remove args block was cleaned up, producing a
-Wunused-function warning.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use fuse_loop_mt_312 for FUSE 3.12+, fuse_loop_mt_31 for 3.0-3.11
- Conditionally add fuse_file_info param to getattr/chmod/chown/utimens
- Replace fuse_session_mount/unmount with fuse_mount/unmount
- Tested with FUSE 3.18 on Debian forky
Multi-stage detection strategy:
1. Try pkg-config (fastest, most reliable)
2. Search common header locations:
- /usr/include, /usr/local/include
- /opt/local/include, /opt/include
3. Compiler test as last resort:
- Attempts to preprocess #include <fuse.h>
- Tests both FUSE 3 and FUSE 2 versions
This handles:
- Minimal CI/CD environments (no pkg-config)
- Non-standard install locations (Homebrew, custom builds)
- Distro-specific paths (BSD, macOS, custom Linux)
The compiler test ensures detection works even when headers
are in unusual locations that gcc can find via its search paths.
- Remove hardcoded FUSE_USE_VERSION defines from source files
- Add FORCE_FUSE=2 or FORCE_FUSE=3 Makefile option to override detection
- Fix FUSE 2 channel tracking (psync_fuse_channel variable)
- Properly store and use channel in mount/unmount for FUSE 2
Usage:
make # Auto-detect (prefers FUSE 3)
make FORCE_FUSE=2 # Force FUSE 2 build
make FORCE_FUSE=3 # Force FUSE 3 build
Verified both FUSE 2 and FUSE 3 builds work correctly.
FUSE 3 init handler requires struct fuse_config* parameter.
This parameter provides access to high-level API configuration
options that can be modified during initialization.
Without this parameter, the init handler signature is incompatible
with FUSE 3, causing filesystem operations to fail.
- nonempty: Removed in FUSE 3.0, mounting on non-empty directories is now default behavior
- hard_remove: High-level API option not available in FUSE 3, immediate unlink is standard
These options are no longer recognized by FUSE 3 and cause mount errors.
- Update FUSE_USE_VERSION from 26 to 30
- Replace fuse_mount/fuse_unmount with fuse_session_mount/fuse_session_unmount
- Update fuse_new() to take args first, remove channel parameter
- Add flags parameter to readdir handler (enum fuse_readdir_flags)
- Add flags parameter to rename handler (for renameat2 support)
- Update filler function calls to include FUSE_FILL_DIR_PLUS flag
- Merge ftruncate into truncate handler (FUSE 3 combines them)
- Update fuse_loop_mt to fuse_loop_mt_31 with loop config
- Link against libfuse3 instead of libfuse
- Remove fuse_chan usage (deprecated in FUSE 3)
Addresses #342
* Fix debug build: compile error, false-positive abort, and crash DB lock
- psql_debug.c: add forward declaration for psql_do_prepare to fix
conflicting-types compile error (BUILD=debug was broken entirely)
- pfs_debug.c: change pfs_debug_check_lock_order from abort to log-only;
write paths legitimately take file lock before SQL and handle ordering
via psql_trylock()+relock in pfs_reopen_file_for_writing — no actual
deadlock risk, the check was a false positive
- psignal.c/h: add psignal_register_cleanup() hook mechanism; change
panic() to use _exit(1) instead of abort() so all file descriptors are
closed on crash, releasing SQLite WAL POSIX advisory locks immediately
and preventing ASan from hanging the process as a zombie
- psql.c: register psql_panic_cleanup() hook to close the DB on panic
(belt-and-suspenders alongside _exit fd cleanup)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix debug psql_trylock: missing strong override left lockctr unupdated
The weak psql_trylock() stub in psql.c called plocks_trywrlock() directly
without updating lockctr. In the debug build, psql_unlock() asserts
lockctr > 0, so when trylock succeeded (lock acquired, lockctr still 0)
the assert fired with SIGABRT on write ops via pfs_inc_writeid_locked.
Add a strong psql_trylock() override in psql_debug.c that delegates to
psql_do_trylock(), which properly acquires the rwlock and updates lockctr.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add lock ordering assertions for psql_lock → file mutex
* Add missing psql.h include to pfs.h
* Fix debug build conditional compilation for lock ordering assertions
* Fix function declaration order for pfs_debug_check_lock_order
---------
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
* Add psignal module for centralized signal handling
- Added pclsync/psignal.h with psignal_register() and psignal_check_pending() API
- Added pclsync/psignal.c with async-signal-safe handlers for SIGINT, SIGTERM, SIGHUP
- Implements flag-based deferred signal handling pattern
* Integrate psignal into main loop and remove conflicting handlers
- Added extern "C" linkage to psignal.h for C++ compatibility
- Integrated psignal_check_pending() into pclsync_lib.cpp main loop
- Integrated psignal_check_pending() into ptimer.c timer loop
- Removed conflicting signal handlers from pfs.c
* Migrate psync_set_signal to psignal module
- Added psignal_set_custom_handler() to psignal.h and psignal.c
- Replaced psync_set_signal() calls in pfs.c with psignal_set_custom_handler()
- Centralizes all signal handling in psignal module
---------
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
Removed IS_DEBUG conditional block from psql.h. Added psql_dump_locks() declaration. Now uses single API without debug/release variants. This eliminates the last remaining use of IS_DEBUG after PR #177.
Fixes#178
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
Removed weak attribute from debug hooks (psys_debug_abort_on_sqllock and psys_debug_configure_core_dump). Made functions static in psys.c and removed declarations from psys.h. Functions can no longer be overridden by external libraries, preventing potential code execution and security bypass.
Fixes#289
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
Renamed psync_delete_sync_by_folderid to pfolder_delete_sync_by_id to align with pfolder API naming conventions. Updated all callers.
Fixes#100
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
Separated assignment from condition so rep contains Call() return value.
Removed incorrect wrapper that prevented state checks from executing.
Now rep values 10/11/12 properly set state.
Fixes#180
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
Replace fopen() with open()+fdopen() for all log file creation
in pdbg.c to ensure files are created with mode 0600, preventing
world-readable logs that may contain sensitive information.
Fixes#290
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
* Fix pcl-dls.1: free errPtr between calls in ptools_set_backend_file_dates()
char *errPtr was already used instead of char msgErr[1024], but was
not freed between the two ptools_backend_call() invocations. If the
first call allocated errPtr, the second would overwrite the pointer
without freeing it. Add free(errPtr); errPtr = NULL; between the two
calls to eliminate the leak.
Ref GH #194.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add ptools_set_backend_file_dates() errPtr lifecycle tests (pcl-dls)
6 test cases using malloc/free wrapping covering both-succeed,
call1-error, call2-error, both-errors, pre-fix leak demonstration,
and no-double-free after mid-free NULLing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix pcl-aex.1: validate PCLOUD_LOG_PATH before use in psync_debug_path()
Add pdbg_path_is_safe() helper that rejects PCLOUD_LOG_PATH values
that are not absolute, contain '..' path components, or do not resolve
under the user HOME directory or /tmp. On rejection, fall back to the
default ~/.pcloud/debug.log path and emit a warning to stderr.
Also fix a pre-existing memory leak: ppath_home() return was not freed
in the default-path branch.
Ref GH #291.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add PCLOUD_LOG_PATH path-safety tests (pcl-aex)
28 test cases covering relative paths, '..' traversal, paths outside
HOME and /tmp, valid accepted paths, and psync_debug_path() env
fallback behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix pcl-aqb.1: eliminate function-pointer cast UB in start_thread() via union
Replace the single void* run field in thread_data with a union
{ thread0_run run0; thread1_run run1 } so each function pointer is
stored and retrieved at its correct type, eliminating the
thread0_run <-> thread1_run cast chain UB. Split start_thread() into
prun_thread() and prun_thread1() that each populate the appropriate
union member, backed by a shared start_thread_common() helper. Add
malloc NULL check with error log in both public functions.
pthread_create failure handling (log + free) was already present.
Ref GH #199.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add prun start_thread() resource-leak and UB tests (pcl-aqb)
8 test cases using linker interposition covering pthread_create
failure data-free, attr_destroy on failure, malloc failure graceful
return, union fn storage without cast, and success path accounting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix pcl-a1j.1: replace sprintf with snprintf and add strcat length check in ptools_create_backend_event()
Add paramname length check (> 254 bytes → skip with warning) before
snprintf into charBuff[i][258], and validate the snprintf return
value. Add explicit length check before strcat into keyParams to
prevent overflow when paramname exceeds remaining buffer space.
Eliminates buffer overflow from long paramname.
Ref GH #195.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix pcl-a1j.1: clamp pCnt to PTOOLS_MAX_PARAMS to prevent charBuff stack overflow
charBuff[30][258] is a fixed-size stack array but pCnt was unbounded,
allowing any caller with params->paramCnt > 30 to overflow the stack
via charBuff[i] access. Add PTOOLS_MAX_PARAMS (30) define, use it to
size charBuff, and clamp pCnt to PTOOLS_MAX_PARAMS with a warning log
before the loop.
Ref GH #195.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add ptools_create_backend_event() validation tests (pcl-a1j)
11 test cases covering pCnt clamping, paramname length guards,
snprintf boundary, keyParams overflow check, and comma-prefix
for subsequent params.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix pcl-6nb.1: readResponse buffer over-read in rpcclient.cpp
Replace malloc'd receive buffer with stack array, fix the broken
validation (msg->length compared against POVERLAY_BUFSIZE rather
than max_value_size which excluded the header), fix the payload-read
loop target (msg->length already includes the header, so the old
`header_size + msg->length` limit over-read), add the missing
`msg->length > total_read` guard before memcpy, and add EINTR
handling in the read loop. Eliminates heap over-read when the daemon
sends msg->length > POVERLAY_BUFSIZE.
Ref GH #184.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add readResponse validation tests (pcl-6nb)
Six socketpair-based test cases covering oversized msg->length,
total_read underrun, header underflow, truncated header, valid
message, and exact-boundary acceptance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix pcl-3la.1: saturating addition for bytestou in pstatus_upload_recalc
Replace unchecked uint64_t summation in pstatus_upload_recalc() with
overflow-safe saturating addition. Before adding each file's size,
check if the result would wrap around UINT64_MAX; if so, saturate to
UINT64_MAX instead of silently wrapping.
Ref GH #293.
* Fix pcl-3la.2: remove spurious fuse_loop_mt while-loop and exit(0)
fuse_loop_mt() already loops internally; wrapping it in an additional
while loop caused repeated re-entry after normal exit. Also removes
the exit(0) call in the shutdown path so fuse_destroy() and other
cleanup run properly on shutdown.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace indefinite pthread_cond_wait() calls in pstatus_wait() with
pthread_cond_timedwait() using a 5-second timeout computed via
clock_gettime(CLOCK_REALTIME). On timeout, the while loop re-evaluates
the wait condition, preventing indefinite hang when the state machine
stalls.
Ref GH #292.
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
- Use sig_atomic_t flag instead of calling unsafe functions in signal handlers
- Remove pdbg_logf() and exit() calls from psync_signal_handler
- Check shutdown_requested flag in main loops
- Export flag for cross-module access
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
- connect_res(): add SO_ERROR check after EINPROGRESS + wait_writable;
a refused/reset connection makes the socket writable with SO_ERROR
set — previously returned the socket as 'connected' when it was not.
Added psock_check_so_error() helper using getsockopt(SO_ERROR).
- wait_readable(), wait_writable(), wait_ssl_ready(): wrap select() in
do{...}while(errno==EINTR) with fd_set reinit on each iteration.
Signal delivery during blocking waits previously caused immediate
SOCKET_ERROR return, dropping live connections on any signal.
Linux updates tv on EINTR to reflect remaining time; reinitializing
fd_set before each retry ensures a clean select state.
- psync_socket_read_plain(), psync_socket_readall_plain(),
psync_socket_readall_plain_thread(): add EINTR to EAGAIN/EWOULDBLOCK
retry condition on read(). EINTR is a non-fatal interruption; retrying
is correct and consistent with the non-blocking socket model.
- psync_socket_read_noblock_plain(): return PSYNC_SOCKET_WOULDBLOCK on
EINTR rather than -1 (fatal error); callers handle WOULDBLOCK by
scheduling a retry.
- psync_socket_writeall_plain(), psync_socket_writeall_plain_thread():
add EINTR retry path on write() without a write-timeout wait, since
EINTR does not indicate the socket became unwritable.
- psock_connect(): add null check on malloc(sizeof(psock_t)); on OOM,
close the connected fd and free the SSL context before returning NULL.
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
prpc.c:
- on_request: fix total_size computation. The original formula
sizeof(uint32_t)+sizeof(uint64_t)+response->length had two bugs:
(1) sizeof(uint32_t)+sizeof(uint64_t)=12 but offsetof(rpc_message_t,value)=16
due to struct alignment padding between the uint32_t type and uint64_t length
fields; and (2) respond() was storing full message size in response->length,
double-counting the header. Fix: use offsetof(rpc_message_t,value)+response->length,
consistent with readResponse() in rpcclient.cpp which reads a fixed header_size
of offsetof(rpc_message_t,value) bytes then reads msg->length payload bytes.
- respond: store payload length only in response->length (value_length+1),
not full message size, to match the client protocol expectation.
- prpc_init: add null check on malloc return value.
- prpc_register: restore old handler table and return -1 if prpc_init fails.
papi.c:
- papi_result_thread: add missing MAX_API_RESPONSE_SIZE guard (present in
papi_result but absent here), preventing server-controlled unbounded malloc.
- papi_result, papi_result_thread: add null checks on malloc before passing
pointer to psock_readall.
- papi_result_async: add MAX_API_RESPONSE_SIZE check and malloc null check
on reader->respsize path.
- calc_ret_len: add _NEED_DATA(1) guard before ARRAY and HASH while-loop
conditions; empty containers with datalen=0 caused out-of-bounds read.
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
status_change_thread reads psync_status.filestodownload, filestoupload,
localisfull, and remoteisfull under statusmutex, while pstatus_download_recalc,
pstatus_upload_recalc, and pstatus_set write them without statusmutex (or under
a different mutex). TSan reports the race at pqevent.c:274.
Replace all writes of these four fields with __atomic_store_n and capture
atomic snapshots in status_change_thread before the condition, using the
snapshots for both comparison and the status_old update after struct copy.
Also use atomic loads in calc_status() for filestodownload and filestoupload.
Closes#335
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
psync_status.status is written in pstatus_set() (pstatus.c:263) after
releasing status_internal_mutex, while status_change() (pclsync_lib.cpp:435)
reads it concurrently from the callback thread without any lock. TSan
reports the race between T9 (write in pstatus_set) and the status callback
thread (read in status_change).
Replace all reads and writes of psync_status.status with
__atomic_load_n/__atomic_store_n (__ATOMIC_RELAXED) across pstatus.c,
pqevent.c, and pclsync_lib.cpp. In status_change_thread, capture the
atomic value once into cur_status before the condition to avoid multiple
inconsistent loads. In status_change(), capture cur_status at entry and
use it throughout, also propagating it to the copied status_ struct.
Closes#334
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
psync_current_time is a global time_t written by timer_thread without
holding timer_mutex, while ptimer_register reads it under timer_mutex.
TSan reports the race at ptimer.c:152 (write) and ptimer.c:216 (read).
Replace all reads with __atomic_load_n and all writes with
__atomic_store_n using __ATOMIC_RELAXED. This covers the race sites in
ptimer.c and all external readers in plocalscan.c, pnetlibs.c, and
psynclib.c that access psync_current_time without any mutex.
Closes#333
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Acquire mutex before reading tm->refcnt to prevent race with
psync_task_dec_refcnt(). Fixes ThreadSanitizer warning.
Fixes#332
Co-authored-by: Levi Neely <lkn@darkstar.example.net>
In psync_unlink(), putil_wipe(psync_my_pass, sizeof(psync_my_pass))
only wiped 8 bytes (pointer size) instead of the full password string.
Changed to strlen(psync_my_pass) and added NULL check.
Fixes pcl-zqv.9.1 (partial)
GH #276
Co-authored-by: Levi Neely <lkn@darkstar.example.net>