fix(observability): let the sink write to the file it read - #57
Merged
Conversation
OpenJSONLSink opened the destination to scan the existing history, closed it, and opened it again to append. A rename between the two left the recovered duplicate-identity and size state describing the first file while every write went to the second, so the sink's own idea of what it had already recorded no longer belonged to the file it was extending. The SameFile check proved the path equalled the descriptor at the second open — not that it was the file scanned. The sink now opens once with O_RDWR|O_APPEND|O_CREATE and holds that descriptor for its lifetime, reading the history from it. O_APPEND puts writes at the end regardless of where reading left the offset, so validation and extension are one object by construction. A destination whose name no longer resolves to the object just opened is refused rather than followed. The window itself is closed by construction rather than covered by a test. Reaching it from outside would need a hook that exists only to be hooked, and the accompanying test says so plainly instead of implying it reproduces the defect; what it does is fail if the single-descriptor construction is undone. The two remaining check-then-use gaps are recorded in the security model rather than closed. Workspace resolution and executable resolution both validate a pathname and act on it a moment later, so an actor with concurrent write access to the running machine can move the used object away from the checked one — which is also why executable_path records what was inspected. Both need write access to a machine that is already running the Task, which the stated trust boundary excludes: manifests and their commands are trusted, reviewed inputs, and a party who can rewrite a PATH directory can do so with or without a race. The entry names what would make them real and what the fix would be, so the deferral is a decision with a trigger rather than an omission.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect
OpenJSONLSinkopened the destination to scan the existing history, closed it, and opened it again to append. A rename between the two left the recovered duplicate-identity and size state describing the first file while every write went to the second.The
SameFilecheck that looked like it covered this proved the path equalled the descriptor at the second open — not that it was the file scanned.The fix
One descriptor for the sink's lifetime:
O_RDWR|O_APPEND|O_CREATE, validated, then read from and appended to.O_APPENDputs writes at the end regardless of the read offset, so validation and extension are the same object by construction. A destination whose name no longer resolves to the object just opened is refused rather than followed.What the test does and does not do
The window lived inside
OpenJSONLSink. Reaching it from outside would need a hook that exists only to be hooked — the kind of control this repository is trying to remove — so it is closed by construction and the test says so plainly rather than implying it reproduces the defect.I wrote a behavioural test first and then found it did not discriminate: it renamed the path after the sink was open, which the old code survives too, because by then it also held a descriptor. The comment was corrected. What it now pins is that the construction cannot be silently undone.
Scope, deliberately narrowed
You confirmed untrusted commands are not a goal. The other two check-then-use surfaces — workspace resolution and executable resolution — need an actor with concurrent write access to the machine already running the Task, which the stated trust boundary excludes: manifests and commands are trusted reviewed inputs, and someone who can rewrite a PATH directory can do so with or without a race.
So they are recorded in
SECURITY.md, not closed, including the consequence thatexecutable_pathrecords what was inspected, what an attacker would need, and the trigger that would make them real — running commands that are not fully trusted — along with the fix that would then apply. A stated gap is worth more than a control that does not remove it.Verification
gofmt,go vet,staticcheck v0.7.0, full suite underumask 002,-race, all six contract checkers. All green. The now-deadscanJSONLwrapper is removed.