FreeAndNil commented on PR #323:
URL: https://github.com/apache/logging-log4net/pull/323#issuecomment-6121926943

   Implementation plan for #323, kept here for the record. Revised after the 
merge so that it describes the change as it landed, including the review rounds.
   
   <details>
   <summary>Plan</summary>
   
   # PR #323: the event and context cluster
   
   ## Context
   
   Four findings of the 2026-08-29 scan in the event and context subsystem, to 
land as PR #323 on
   top of `Feature/322-reconfiguration-file-lock` (that branch already carries 
every #319 commit).
   
   Checked against the published documents on `logging-site` `main` at 
`67d4ea65`, tree clean:
   
   - **f021 is out of scope.** `_threat-model-common.adoc` "Destination 
integrity
     (operator-controlled)" makes a path interpolated from a lookup the 
operator's decision, and
     `security/faq.adoc` "Problematic contexts" names the 
context-lookup-into-a-path case and closes
     with "It is the user's responsibility to ensure that the sources of any 
interpolated values used
     in file paths are trustworthy". The sentence `tmd-2` calls factually wrong 
lives in the scan's own
     `THREAT_MODEL.md`; the published model never mentions `PatternString`.
   - **f006, f039, f044 need no adversary at all**, so under "Adversary 
capabilities" none is a
     vulnerability. The whole cluster is reliability work: no security language 
anywhere, finding ids
     cited as provenance only.
   
   Two of the four get code, two get prose, and the f044 fix frees a public 
delegate, which gets a
   deprecation of its own. Five commits.
   
   ## f021: context values in a PatternString-typed config value
   
   No code change. `PropertyPatternConverter.Convert` reads the ambient context 
of the configuring
   thread and `PatternStringConverter.ConvertTo` expands it once at config-bind 
time
   (`XmlHierarchyConfigurator.SetParameter:711-715`), which is exactly the 
operator decision the model
   puts on the deployer. The scan's patch sanitizes `/`, `\`, `:` and `..` 
inside the converter behind
   a process-wide static: content mangling in a converter, against the 
sink-owns-escaping rule.
   
   1. `src/log4net/Util/PatternStringConverters/PropertyPatternConverter.cs`, 
above the
      `CompositeProperties` build in `Convert` (:64): two lines saying context 
values may hold
      user-supplied data, so a path built from `%property` is the operator's 
choice, linking
      `https://logging.apache.org/security/faq.html#path-traversal`. That FAQ 
entry is the one that
      settles the finding and links to the model itself, so the model URL is 
not repeated: two links
      on a two line comment is more pointer than point.
   2. `src/site/antora/modules/ROOT/pages/manual/faq.adoc`, the 
`#dynamic-patterns` entry (:180-195):
      one sentence that `%property` expands context values, which may carry 
user-supplied data, so a
      path built from one is only as trustworthy as that value, with the same 
FAQ link.
   3. No changelog entry (comment and docs only).
   
   ## f006: buffered events reach another request's trace
   
   No code change. `AspNetTraceAppender.Append` 
(`src/log4net/Appender/AspNetTraceAppender.cs:60-78`)
   resolves `HttpContext.Current` on the calling thread, and 
`BufferingAppenderSkeleton` delivers the
   whole buffer synchronously on whichever thread triggers the flush
   (`BufferingForwardingAppender.SendBuffer:77-84`). A `LoggingEvent` carries 
no request handle, so
   nothing at the sink can tell the events apart. The scan's timestamp guard 
drops any event older than
   `HttpContext.Current.Timestamp`, which trades the mix-up for event loss and 
still admits a later
   concurrent request.
   
   1. Class remarks on `AspNetTraceAppender` (:44-54), one sentence as its own 
`<para>`: writes to the
      request current on the appending thread, so it must not be wrapped in a 
buffering appender.
   2. 
`src/site/antora/modules/ROOT/pages/manual/configuration/appenders.adoc:48`, 
the same sentence
      as a second paragraph in the existing cell. A blank line inside a cell 
opens a paragraph, a
      cell ends only at the next `|` at the start of a line, so the table keeps 
its 3 tables and 30
      rows; render it with the Asciidoctor in `node_modules` if a reviewer 
doubts it.
   3. No Windows round is needed, because nothing behavioural changes. The file 
is
      `#if NET462_OR_GREATER`, so still compile-check it with
      `dotnet build src/log4net.Tests/log4net.Tests.csproj -f net462 
-p:TargetFrameworks=net462`.
   4. No changelog entry.
   
   ## f044: AutoPopStackFrame.Dispose resurrects captured frames
   
   Real defect, and the deterministic case needs no misuse: 
`LogicalThreadContext.Stacks[x].Clear()`
   registers a fresh empty stack (`LogicalThreadContextStack.cs:96`), then 
disposing a frame pushed
   before the `Clear` re-registers the pre-`Clear` snapshot, because `Dispose` 
(:314-329) rebuilds from
   `_logicalThreadContextStack._stack` captured at `Push` time and never looks 
at what is registered
   now.
   
   1. **Reproduce first**, in 
`src/log4net.Tests/Context/LogicalThreadContextTest.cs`:
      `DisposingAFrameAfterClearKeepsTheStackEmpty`, push two frames, 
`Clear()`, dispose the inner
      frame, assert `Count` is 0. Watch it fail.
   2. Give the stack its owner instead of a callback. 
`LogicalThreadContextStack` holds a
      `LogicalThreadContextStacks _owner` in place of the `TwoArgAction` 
`_registerNew` field, the
      internal constructor takes the owner, and `LogicalThreadContextStacks` 
gets two internal
      members: `RegisterNew(string, LogicalThreadContextStack)`, the former 
private method, and
      `GetCurrent(string)`, which returns `_properties[name] as 
LogicalThreadContextStack`, so a
      foreign value under the key reads as null. `Clear`, `Pop` and `Push` call 
`_owner.RegisterNew`.
   3. Fix `Dispose`: return when `_frameDepth` is negative; read the stack 
registered now through
      `owner.GetCurrent(propertyKey)` and return when it is null or its count 
is already at or below
      `_frameDepth`; otherwise copy it, pop down to `_frameDepth` and register 
the trimmed copy. Trim
      only, never grow, so a stale snapshot can no longer be injected, and a 
non-stack value under the
      key is left alone rather than overwritten.
   4. Further tests in the same fixture, assertions outside any task body:
      `DisposingAFrameAfterPoppingBelowItKeepsTheStackEmpty` (two pops, then 
dispose the inner
      frame), `DisposingAFrameTrimsTheStackToItsDepth` (the nested happy path, 
which passes before
      and after the fix and is the guard that the trim still happens), and
      `DisposingAFrameInAnotherFlowDoesNotResurrectIt`, where a `Task.Run` 
clears, disposes and
      returns the count it saw, asserted as 0 outside, with the outer flow 
still at 2 as the flow
      isolation guard. The fixture's `[TearDown]` removes the property from 
every context, so the
      frames the tests leave pushed do not leak.
   5. Residual to state in the commit message, not the source: disposing a 
foreign flow's frame while
      the current flow holds a deeper stack still trims the current flow. That 
is context loss, never
      context injection, and it is the trade the trim-only rule buys.
   6. Measure before and after, 1M operations, best of five, Release on 
net10.0: push and dispose at
      depth 1 and depth 3 (expected the same within noise, same bytes), dispose 
after `Clear`
      (expected faster and smaller, since it no longer rebuilds the snapshot). 
Table into the commit
      message and the PR body.
   7. Changelog `src/changelog/3.5.0/323-logical-stack-dispose.xml`, 
`type="fixed"`, closing
      `(audit da18b6fd-f044, fixed by @FreeAndNil)`.
   
   ## TwoArgAction: deprecate the delegate the owner reference replaces
   
   After step 2 above nothing in the tree uses `log4net.Util.TwoArgAction<T1, 
T2>`. It is public, so
   it stays until version 4.
   
   1. Own commit `deprecate TwoArgAction #323`: summary reduced to what it is, 
"Delegate type for an
      action taking two arguments", and `[Obsolete("No longer used by log4net 
and removed in
      version 4.")]`. The solution still builds with 0 warnings, since no call 
site remains.
   2. Changelog `323-deprecate-twoargaction.xml`, `type="changed"`, closing
      `(implemented by @FreeAndNil)`.
   
   ## f039: the event cache unlocks on a redundant Fix
   
   `FixVolatileData` (`src/log4net/Core/LoggingEvent.cs:1138-1225`) sets 
`_cacheUpdatable = true` at
   :1143 unconditionally, while the fixing work is guarded by `updateFlags > 0` 
at :1147 and the
   re-lock at :1224 is unconditional. `_cacheUpdatable` is a plain `bool` 
(:1451) and every lazy getter
   gates on `field is null && _cacheUpdatable` (Identity :902, ThreadName :655, 
LocationInfo :483,
   Domain :947). A redundant `Fix` therefore reopens capture for a concurrent 
reader.
   
   **The first deliverable is a measurement, not a patch**, and it fires.
   
   1. Scratch harness, not a committed test: one thread setting `Fix = 
FixFlags.Partial` on an
      already-fixed event in a loop, another reading `Identity`, which 
`Partial` does not cover.
      Expect a non-null `Identity` after 56000 to 63000 calls, five runs of 
five, and none in
      2000000 once the patch is in. Both numbers go into the commit message and 
the PR body.
   2. Patch: move `_cacheUpdatable = true` inside the `if (updateFlags > 0)` 
block with a one line
      comment, and make the field `volatile`. The late `UserName` capture at 
:1215-1221 writes
      `_data.UserName` directly and does not pass a gated getter, so it is 
unaffected; the
      unconditional re-lock at the end stays, so a `Fix` with nothing to do 
still ends locked.
      `UserNameFixingTest` and `FixingTest` stay green.
   3. Hammer test `ARedundantFixDoesNotLetAnotherThreadCacheItsIdentity` in
      `src/log4net.Tests/Core/FixingTest.cs`: fix the event `Partial`, start a 
background reader that
      sets its own `GenericPrincipal`, signals a `TaskCompletionSource<bool>` 
once running and spins
      on `Identity is null` counting its passes; wait on the gate, assign `Fix 
= FixFlags.Partial`
      500000 times, stop the reader, join with a timeout, then assert the pass 
count is greater than
      zero and `Identity` is null. The gate is a `TaskCompletionSource<bool>` 
and not a
      `ManualResetEventSlim`: a timed out `Wait` would dispose the event and a 
late reader would then
      `Set` a disposed object, which takes the test host down on a background 
thread. The spin stays
      without a yield, since the reader has to read as often as possible to see 
a hit once per 60000
      calls; the whole test takes about 34 ms. Mutation-verify in both 
directions: the reverted fix
      fails the identity assertion, a reader that exits at once fails the pass 
count assertion.
   4. Changelog `323-logging-event-fix-cache.xml`, `type="fixed"`, closing
      `(audit da18b6fd-f039, fixed by @FreeAndNil)`.
   
   ## House rules that bite here
   
   - No test method underscores, no `BugFNNN` fixture names: the new tests go 
into the existing
     `LogicalThreadContextTest` and `FixingTest` fixtures, each with a doc 
comment.
   - NUnit 4 constraint model, expression bodies for single-statement tests, 
generous timeouts on any
     gate. The test assembly declares no parallelism, so `[NonParallelizable]` 
adds nothing here.
   - Every `<issue>` needs `id` and `link`; the description is prose only and 
does not repeat the pull
     request link.
   - Stage only. Commit messages go to `.git/plans/323/`, one file per commit, 
the maintainer commits
     and signs.
   
   ## Verification
   
   ```
   dotnet build src/log4net.sln                        # 0 warnings, both TFMs
   dotnet build src/log4net.Tests/log4net.Tests.csproj -f net462 
-p:TargetFrameworks=net462
   dotnet test src/log4net.Tests/log4net.Tests.csproj -f net10.0
   dotnet test src/log4net.Ext.Mail.Tests/log4net.Ext.Mail.Tests.csproj
   ```
   
   Redirect build output to a file and read the whole file. With both 
production changes reverted,
   four of the five new tests fail and `DisposingAFrameTrimsTheStackToItsDepth` 
passes, as the happy
   path guard it is. Restore before reporting anything green.
   
   </details>
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to