fix: atomic etag validation for document PUT/POST to fix TOCTOU race condition - #504
fix: atomic etag validation for document PUT/POST to fix TOCTOU race condition#504chomatdam wants to merge 2 commits into
Conversation
|
Is there something we need to do for this PR to be considered/reviewed/accepted? More broadly, are the project maintainers interested in outside contributions, or is this a "one-way OSS" project? Please understand that there is no acrimony in the question. Your (project maintainer's) help calibrating expectations is appreciated. |
|
@cliffcaseyyet we are running a hard fork with this race condition fix since 6 months, any chance we could make progress on this PR ? (goes with yetanalytics/lrs#106) |
@chomatdam Yes, apologies we fell behind on some of these but we appreciate the contribution and are catching up today. I should have an answer for you soon. |
@emmanuel Hi, totally fair question. We fell behind on these contributions and we are working to catch up right now, this one we should have feedback or approval on shortly. We do appreciate outside contributions and will be more responsive in the future. |
|
@chomatdam
On our end we are planning to implement an in-db fix which involves a conditional update so that the SQL query itself is protecting against bad updates. We will tag you on the PRs as we make the appropriate changes in the LRS project and this one. |
|
@chomatdam if you didn't see it already, this is our approach to these issues. We will be merging and cutting a release soon as it goes through review. #520 |
Summary
Fixes a race condition where concurrent document writes could silently overwrite each other despite correct use of
If-Match/If-None-Matchconcurrency headers.Problem
The etag validation for document resources (state, agent profile, activity profile) happens in the
etags-preprocinterceptor — a separate stage that runs before the actual write in-set-document. Between the check and the write, another client can modify the document:etags-preprocreads document → etag is now "def456" → 412 ✓etags-preprocvalidates → pass-set-documentoverwrites blindly → data lossFix
Move the etag validation into
-set-documentitself, inside the samejdbc/with-transactionblock as the write:If-Match/If-None-Matchheaders from the Pedestal interceptor context (now passed through to-set-document)com.yetanalytics.lrs.util.hash/sha-1implementation){:error ex-info}with:com.yetanalytics.lrs.xapi.document/precondition-failedtype on failure, which the upstream route handler surfaces as HTTP 412When no concurrency headers are present, the write proceeds directly with no overhead.
Changes
src/main/lrsql/util/document.clj— New etag utility functions:compute-etag: SHA-1 hash matching upstream implementationparse-etag-header: Parse If-Match/If-None-Match header valuescheck-etag-precondition: Validate preconditions against current docsrc/main/lrsql/system/lrs.clj— Modified-set-document:_ctx→ctxto access request headersjdbc/with-transactionCompanion PR
Depends on yetanalytics/lrs#106 (fix/atomic-etag-validation) for 412 error handling in
put-responseandpost-response.Testing
Tested together with the companion lrs branch via
{:local/root "../lrs"}:make test-sqlite: 88 tests, 923 assertions, 0 failures, 0 errorsNew
src/test/lrsql/etag_race_test.cljreproduces the TOCTOU race condition with 10 concurrent PUT/POST requests sharing the same If-Match etag:main: 6 failures — all 10 writes go through, stale etags ignored,If-None-Match *doesn't prevent overwritesfix/atomic-etag-validation: 0 failures — exactly 1 write succeeds, 9 rejected with precondition failure