Skip to content

Handle three documented limitations: same-thread two instances, auto layout offsets, Global\ names - #6

Merged
zacala1 merged 4 commits into
mainfrom
fix/remaining-limitations
Oct 22, 2026
Merged

zacala1 merged 4 commits into
mainfrom
fix/remaining-limitations

Conversation

@zacala1

@zacala1 zacala1 commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Summary

Three things that 4.0.0 only documented. Each is its own commit, with tests.

  1. Two instances of one region on one thread (72c6a2b). Locks are reentrant only through the instance that took them, so a thread that asked for a lock through a second instance waited for the whole timeout (for ever with an infinite one) for a write lock that it holds itself. The write lock records its owner by process, thread and PID namespace, so StructuredMemory<T> and SharedArray<T> now check it before they wait and throw InvalidOperationException with an explanation. A read lock held through another instance cannot be recognised (read locks have no owner) and still waits; the raw MemoryRegion.TryAcquire* methods are unchanged. Two existing SharedArray tests used a second instance on the test thread as a stand-in for another holder; they ask from another thread now.
  2. Auto layout offsets in the fingerprint (52243e6). The fingerprint of a generic struct (ValueTuple, KeyValuePair, ...) did not say where the runtime put each field of a LayoutKind.Auto type. With net8.0 and net10.0 both shipping (Target net10.0 as well as net8.0 #5) two processes could agree on the fingerprint and read each other's fields from the wrong places. The offsets are measured with a dynamic method (Marshal.OffsetOf rejects these types) and are part of the descriptor. Types the marshaller accepts keep their 3.0.0 fingerprint (pinned by a test), and the generic fallback has never been released. Fingerprints are cached per type.
  3. Global\ and Local\ in names (86a7c83). A name may start with Global\ or Local\ (any case); what follows must be flat. Windows gets the full name, so a service and a desktop process can share Global\name. Linux has no sessions and drops the prefix from the file name, so Global\x, Local\x and x are one region there and one name works on both platforms; MemoryRegion.Remove uses the same file name.

README and CHANGELOG (a 4.1.0 section) describe all three, including what stays unsupported: creating a Global\ section needs SeCreateGlobalPrivilege, the section keeps the default access control of its creator, so other accounts may be refused (run both sides under one account for now).

Verification

Local (Linux, NUnit shim): 581 passed, 2 failed (the two known shim-only ones), 6 skipped (Windows-only and explicit). Run on a Windows runner by CI: Local\ and Global\ sections (the Global\ test is ignored when the runner lacks the privilege). Run against the previous code the new behaviour tests fail (a second instance waits for its timeout; Global\x is rejected).

Stacking: independent of #4 and #5; all three touch the top of CHANGELOG.md, so the ones that merge later need a trivial conflict resolution (this one adds the 4.1.0 section, #5 adds an entry to the same section).


Generated by Claude Code

…tance

Locks are reentrant only through the instance that took them. A second
instance of the same region on the same thread (two components that each open
the region) waited for the whole timeout for a write lock that its own thread
holds, for ever with an infinite timeout. The write lock records its owner by
process, thread and PID namespace, so StructuredMemory and SharedArray now
check it before they wait and throw InvalidOperationException with an
explanation. A read lock held through another instance cannot be recognised
(read locks have no owner), and the raw MemoryRegion.TryAcquire* methods keep
returning false.
The fingerprint of a generic struct (ValueTuple, KeyValuePair, ...) was built
from field names, types and explicit FieldOffset values, so for LayoutKind.Auto
it said nothing about where the runtime put each field. Two processes on
runtimes that lay a type out differently (net8.0 and net10.0 now both ship)
would have agreed on the fingerprint and read each other's fields from the
wrong places. The offsets are measured with a dynamic method (Marshal.OffsetOf
rejects these types) and are part of the descriptor. Types the marshaller
accepts keep their 3.0.0 fingerprint, and the generic fallback has never been
released, so no existing region is affected. Fingerprints are cached per type
now, since measuring is not free.
Without a prefix a Windows section lives in the namespace of the session of the
process that creates it, so a service (session 0) and a desktop process could
not share a region, and any backslash was rejected. A name may now start with
Global\ or Local\ (in any case); what follows must still be flat. Windows gets
the full name; Linux has no sessions and drops the prefix from the file name, so
Global\x, Local\x and x are one region there and one name works on both
platforms. Remove uses the same file name.

The two SharedArray tests that used a second instance on the test thread as a
stand-in for "another holder" ask from another thread now, since the same
thread is reported at once (previous commit).
@zacala1
zacala1 merged commit 39073e4 into main Oct 22, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants