See: story index | Full details: security_audits
19 findings: 4 Critical, 4 High, 3 Medium, 4 Low, 4 Info.
Critical findings fixed:
• C1 — Newline injection in text IPC protocol. The original lock daemon protocol used "LOCK <url>\n". A URL containing \n injected extra commands into the child's command loop. Fixed: replaced with length-prefixed binary frames.
• C2 — Cache dir 0755. create_directories() inherits the process umask (often 0755). Any local user could read the cached (encrypted) database. Fixed: explicit chmod(dir, 0700) after create_directories().
• C3 — TOCTOU plugin load. The loader called access() then open() then dlopen() on the same path — three separate kernel operations. A race between any two allowed substituting a malicious library. Fixed: one open(O_RDONLY | O_NOFOLLOW) → mmap → dlopen via /proc/self/fd/<n>.
• C4 — Cross-protocol curl redirect. CURLOPT_FOLLOWLOCATION was enabled with no protocol restriction. A malicious server could 302 a PUT to file:///etc/shadow. Fixed: CURLOPT_PROTOCOLS_STR = "https,http", FOLLOWLOCATION = 0.
16 findings: 0 Critical, 4+ High, 6 Medium, 4 Low, 2 Info. All critical issues already fixed after audit 1. Notable new findings fixed:
• recv_string unbounded allocation (High) — child allocated a std::string of len bytes before checking the value of len. Fixed: if (len > max_len) return false before the allocation; MAX_IPC_URL = 8192, MAX_IPC_PATH = 4096.
• EINTR not handled in send_all/recv_all (Medium) — write/read can return -1 with EINTR on a signal. Fixed: if (errno == EINTR) continue;.
• OPTIONS probe missing curl hardening (Medium) — server_supports_locking() used curl_easy_init() directly, bypassing protocol restrictions and SSL verification. Fixed: now uses make_curl(url).
• Cache file permissions (Medium) — webdav_fetch created the cache file with fopen("wb") — inheriting the umask, typically producing 0644. Fixed: open(O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0600) + fdopen().
3 findings: 0 Critical, 0 High, 1 Medium, 2 Low.
• SOCK_CLOEXEC missing (Medium) — socketpair without SOCK_CLOEXEC. Any child process spawned by the GUI (browser, file manager) would inherit the parent's socket end, preventing the daemon from ever seeing EOF and holding the server lock indefinitely. Fixed: SOCK_STREAM | SOCK_CLOEXEC.
• header_cb DAV: check half-case-insensitive (Low) — the DAV: check used line.substr(0,4) == "DAV:" (case-sensitive) after a partial case-insensitive first-char check. "Dav:" or "dav:" would fail. Fixed: strncasecmp(line.c_str(), "DAV:", 4) == 0.
• Parent URL guard inconsistent with child limit (Low) — pws_lockd_release() checked ulen > 65535 before sending; the child's recv_string rejected anything > MAX_IPC_URL (8192). A URL between 8193 and 65535 bytes would be sent by the parent and immediately rejected by the child, causing the daemon to _exit(1). Fixed: parent guard changed to if (ulen > MAX_IPC_URL) return;.
• All Critical and High findings fixed
• Most Medium findings fixed
• Known architectural limitations documented: cache filename collision (no hash), daemon crash is unrecoverable (EIO), libcurl post-fork state technically undefined (safe in practice), blocking I/O in signal handlers via pws_lockd_release (acceptable; the write is async-signal-safe and very fast in practice)