Skip to content

Fix five regressions on LTRData.DiscUtils-initial (registry, NTFS, dynamic-disk hang, sparse-stream)#69

Merged
Olof-Lagerkvist merged 5 commits into
LTRData:LTRData.DiscUtils-initialfrom
devedse:fix/registry-indirect-list-mixed-cells
Jun 23, 2026
Merged

Fix five regressions on LTRData.DiscUtils-initial (registry, NTFS, dynamic-disk hang, sparse-stream)#69
Olof-Lagerkvist merged 5 commits into
LTRData:LTRData.DiscUtils-initialfrom
devedse:fix/registry-indirect-list-mixed-cells

Conversation

@devedse

@devedse devedse commented Jun 19, 2026

Copy link
Copy Markdown

Fixes five regressions currently on LTRData.DiscUtils-initial. With all five, the full LibraryTests suite passes (581 passed, 1 skipped, 0 failed, no hangs). Without them the suite cannot build/create a hive (#2), fails NTFS/ExFat reads (#3, #5), and hangs indefinitely (#4).

1. SubKeyIndirectListCellInvalidCastException on mixed ri/li lists

LinkSubKey/UnlinkSubKey branched on ListType and hard-cast every entry, but a list can contain a mix of sublists and key nodes (the read paths already tolerate this). Deleting/adding a subkey under such a list threw InvalidCastException: KeyNodeCell -> ListCell. Fixed by dispatching per entry on the actual cell type, mirroring the read paths.

2. Bin.AllocateCell — 8-byte alignment check regressed to mod 7

Cells are 8-byte aligned, so the check must be (size & 0x7) != 0. It had been changed to (size % 0x7) != 0 (mod 7), rejecting ~6 of every 7 valid sizes including the ~80-byte root cell, so RegistryHive.Create() itself throws Invalid cell size. Restored to & 0x7. (Introducing commit in a comment below.)

3. NtfsFileSystem.GetFileLengthFileLengthFromDirectoryEntries option ignored

The default-data-stream shortcut returning the directory entry's RealSize was removed, making the option a no-op and breaking NtfsFileSystemTest.GetFileLength. Restored the shortcut. (Introducing commit in a comment below.)

4. DynamicStream — infinite loop in sector scan (the hang)

FindNextPresentSector/FindNextAbsentSector advance with pos += (8 - sectorInBlock & 0x7) * Sizes.Sector. By C# precedence that is (8 - sectorInBlock) & 0x7, not 8 - (sectorInBlock & 0x7): at a byte-aligned, empty bitmap byte the original 8 - sectorInBlock % 8 advanced by 8 sectors, but the rewrite yields 8 & 7 = 0, so pos never advances and extent enumeration / the dynamic VHD+VMDK builders loop forever. Parenthesized to restore the semantics. (Introducing commit in a comment below.)

5. SynchronizedSparseStreamPosition never advances

The synchronized wrapper set content.Position = Position before each op but never wrote the advanced position back, so Position stayed frozen and reads/writes through the wrapper re-processed the same offset (e.g. ExFat...ReadLongSparseLimited). Position now delegates to content.Position under the lock, fixing every read/write path at once. (Introducing commit in a comment below.)

…xed ri/li lists

SubKeyIndirectListCell backs both "ri" index-root lists (entries are subordinate
lists) and "li" leaf-index lists (entries are key nodes). The read paths
(Count, EnumerateKeyNames, EnumerateKeys, DoFindKey, KeyFinder) already tolerate a
single list whose entries are a mix of sublists and key nodes, by dispatching on the
actual cell type. But LinkSubKey/UnlinkSubKey still branched on ListType and hard-cast
every entry (GetCell<ListCell> for "ri", GetCell<KeyNodeCell> for "li").

On a real Windows SYSTEM hive whose "ri" list contains a direct key-node entry,
deleting a subkey (UnlinkSubKey) threw:
  System.InvalidCastException: Unable to cast object of type
  'DiscUtils.Registry.KeyNodeCell' to type 'DiscUtils.Registry.ListCell'.

Reading/enumerating the same key worked (those paths were already hardened); only
add/delete crashed.

Fix: LinkSubKey/UnlinkSubKey now dispatch on the actual cell type per entry
(GetCell<Cell> + "is ListCell"), exactly as the read paths do. Behaviour for
homogeneous "ri"/"li" lists is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
devedse added a commit to devedse/DiscUtils that referenced this pull request Jun 19, 2026
Includes the cherry-picked SubKeyIndirectListCell fix (InvalidCastException on
deleting/adding keys in mixed ri/li subkey lists). Triggers a fork build so
DevePXEBoot can consume the registry delete fix before it lands upstream (PR LTRData#69).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Registry hive cells are 8-byte aligned, so AllocateCell must reject sizes whose
low 3 bits are set: (size & 0x7) != 0. Commit f7bf755
("Minor code cleanups and optimizations") changed this from `size % 8 != 0` to
`(size % 0x7) != 0` (mod 7), which rejects ~6 of every 7 valid sizes - including the
~80-byte root key cell - so RegistryHive.Create() throws "Invalid cell size" and
every registry/BootConfig test fails at construction (and the test run hangs).
Restores the 8-byte alignment check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@devedse

devedse commented Jun 19, 2026

Copy link
Copy Markdown
Author

Note on fix #2 (Bin.AllocateCell): the (size % 0x7) regression was introduced in commit f7bf755 ("Minor code cleanups and optimizations", 2026-06-05), which changed size % 8 != 0(size % 0x7) != 0.

That is also where CI on LTRData.DiscUtils-initial started failing — the preceding commit ("Minor optimizations", 2026-06-02) was green (3/3), and f7bf755 went red (2/3). Because it makes RegistryHive.Create() throw on the root cell, it breaks every registry write on the current default branch, not just this PR.

@devedse devedse changed the title Fix InvalidCastException in SubKeyIndirectListCell write paths (mixed ri/li subkey lists) Fix two registry write-path bugs: SubKeyIndirectListCell cast + Bin.AllocateCell alignment regression Jun 19, 2026
@Olof-Lagerkvist

Copy link
Copy Markdown
Member

Oh that was a good find! I have not noticed that CI fails now because I did not get any notifications about it. I get notifications about it from other repositories though. Weird. I need to check what is happening here!

@devedse

devedse commented Jun 19, 2026

Copy link
Copy Markdown
Author

Yeah, my AI found it 😄

Also have a good review of the other fix I did as it's also AI authored and I don't really have the knowledge on this part to see if it makes sense.

@devedse

devedse commented Jun 19, 2026

Copy link
Copy Markdown
Author

Hmm it does seem the build is still failing even after this fix. I'll see if I can figure out what's going on.

@Olof-Lagerkvist

Copy link
Copy Markdown
Member

Yes, the bugs were mostly introduced by some AI optimizations. Though, they feel in most cases safe as long as CI tests still complete successfully. Which I thought they did. 😅

But I think I also need to add CI tests for Linux. There have been several cases lately where tests work in Windows but fall when I run them in Linux and need to address something. Usually just the rest itself, but sometimes it reveals issues deeper in the library.

…ies (regressed in a551635)

GetFileLength must honor the NtfsOptions.FileLengthFromDirectoryEntries option: when
set (the default), a default-data-stream length request returns the directory entry's
cached RealSize. Commit a551635 ("Bugfixes for
FileExist and GetFileLength") removed that shortcut, so GetFileLength always looked up
the data attribute and the option became a no-op - breaking NtfsFileSystemTest.GetFileLength
(and the async variant), which expects a hardlink's stale directory-entry size (14325)
by default and the live data-attribute size (50) only when the option is disabled.
Restores the shortcut. The FileExists fix in that commit is correct and left intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@devedse devedse changed the title Fix two registry write-path bugs: SubKeyIndirectListCell cast + Bin.AllocateCell alignment regression Fix three write-path regressions on LTRData.DiscUtils-initial (registry cast + cell alignment + NTFS GetFileLength) Jun 19, 2026
@devedse

devedse commented Jun 19, 2026

Copy link
Copy Markdown
Author

Note on fix #3 (NTFS GetFileLength): the regression came from commit a5516351132e259b5149030e000f4789f78894d2 ("Bugfixes for FileExist and GetFileLength"), which removed this shortcut from GetFileLength:

if (NtfsOptions.FileLengthFromDirectoryEntries && attributeName == null && attributeType == AttributeType.Data)
{
    return (long)dirEntry.Details.RealSize;
}

That made the FileLengthFromDirectoryEntries option a no-op. The FileExists change in the same commit is correct and is left intact here; only the GetFileLength shortcut is restored.

devedse and others added 2 commits June 19, 2026 16:19
FindNextPresentSector/FindNextAbsentSector advance with
pos += (8 - sectorInBlock & 0x7) * Sizes.Sector. C# precedence makes this
(8 - sectorInBlock) & 0x7, not 8 - (sectorInBlock & 0x7): when sectorInBlock is
byte-aligned and the block-bitmap byte is empty, the original (8 - sectorInBlock % 8)
advanced by 8 sectors, but the rewrite yields 8 & 7 = 0, so pos never advances and the
extent enumeration (LayerExtents/Extents and the dynamic VHD/VMDK builders) loops
forever. Commit f7bf755 ("Minor code cleanups and optimizations") introduced this by
rewriting % 8 to & 0x7 without parentheses. Parenthesized so the mask applies to
sectorInBlock (the 7 - x & 0x7 mask forms are coincidentally equivalent but were
parenthesized too for clarity).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…b5b75d)

The synchronized wrapper kept its own Position auto-property and set
content.Position = Position before each operation but never wrote the advanced position
back afterward, so Position stayed frozen and reads/writes through the wrapper
re-processed the same offset (e.g. ExFat ReadLongSparseLimited read the wrong data).
Delegate Position to content.Position under the lock - correct for this single-shared-
cursor synchronized wrapper and fixes every read/write path (sync and async) at once.
Introduced by 4b5b75d ("Synchronized wrappers for disks and streams").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@devedse devedse changed the title Fix three write-path regressions on LTRData.DiscUtils-initial (registry cast + cell alignment + NTFS GetFileLength) Fix five regressions on LTRData.DiscUtils-initial (registry, NTFS, dynamic-disk hang, sparse-stream) Jun 19, 2026
@devedse

devedse commented Jun 19, 2026

Copy link
Copy Markdown
Author

Introducing commits for fixes #4 and #5:

For #4 specifically, note the 7 - x & 0x7 mask forms in the same files are coincidentally fine ((7-x)&7 == 7-(x%8) for all x), but the 8 - x forms are not (8-(x%8) ranges 1..8 while (8-x)&7 ranges 0..7), which is why only the two 8 - sites needed the fix.

@devedse

devedse commented Jun 19, 2026

Copy link
Copy Markdown
Author

The build now succeeds so it all seems to work 😄

@Olof-Lagerkvist Olof-Lagerkvist merged commit 43df6ed into LTRData:LTRData.DiscUtils-initial Jun 23, 2026
1 check passed
@Olof-Lagerkvist

Copy link
Copy Markdown
Member

I merged the commits with some changes to avoid reintroducing some bugs that were fixed previously. I also added timeouts to CI tests to avoid this scenario in the future where new bugs that hang unit tests go unnoticed because they never finish and never report failure.

Closing this PR.

Thanks a lot for finding and solving these bugs!

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