fix(transport): zero a bundle session's content when it expires, not only on apply/abandon (review 2026-10-21 04/C3)

This commit is contained in:
Joseph Doherty
2026-10-22 09:18:37 -04:00
parent fad08f7ce7
commit 55cebf2397
3 changed files with 76 additions and 4 deletions
@@ -130,6 +130,57 @@ public sealed class BundleSessionStoreTests
Assert.Null(sut.Get(dropB.SessionId));
}
/// <summary>
/// Review 2026-10-21 04/C3: a session dropped by TTL expiry on the <c>Get</c> path
/// has its content buffer zeroed, matching the zero-then-remove discipline that
/// <c>ApplyAsync</c>/<c>Abandon</c> already follow.
/// </summary>
[Fact]
public void Get_after_TTL_zeroes_the_content_buffer()
{
var clock = new TestTimeProvider(DateTimeOffset.UtcNow);
var sut = new BundleSessionStore(Options(), clock);
var content = new byte[] { 0x11, 0x22, 0x33 };
var session = SessionWithContent(clock.GetUtcNow().AddMinutes(30), content);
sut.Open(session);
clock.Advance(TimeSpan.FromMinutes(31));
Assert.Null(sut.Get(session.SessionId));
Assert.All(content, b => Assert.Equal(0, b));
}
/// <summary>
/// Review 2026-10-21 04/C3: <c>EvictExpired</c> zeroes the content buffer of each
/// session it drops, and leaves a live session's buffer untouched.
/// </summary>
[Fact]
public void EvictExpired_zeroes_the_content_of_expired_sessions_only()
{
var clock = new TestTimeProvider(DateTimeOffset.UtcNow);
var sut = new BundleSessionStore(Options(), clock);
var expiredContent = new byte[] { 0x11, 0x22 };
var liveContent = new byte[] { 0x33, 0x44 };
sut.Open(SessionWithContent(clock.GetUtcNow().AddMinutes(5), expiredContent));
var live = SessionWithContent(clock.GetUtcNow().AddMinutes(60), liveContent);
sut.Open(live);
clock.Advance(TimeSpan.FromMinutes(10));
sut.EvictExpired();
Assert.All(expiredContent, b => Assert.Equal(0, b));
Assert.Equal(new byte[] { 0x33, 0x44 }, liveContent);
Assert.NotNull(sut.Get(live.SessionId));
}
private static BundleSession SessionWithContent(DateTimeOffset expiresAt, byte[] content) => new()
{
SessionId = Guid.NewGuid(),
Manifest = StubManifest(),
DecryptedContent = content,
ExpiresAt = expiresAt,
};
/// <summary>Verifies that the legacy failed-unlock-attempts field still round-trips through the shared reference.</summary>
[Fact]
public void Legacy_FailedUnlockAttempts_field_still_round_trips_via_shared_reference()