Skip to content

Conversation

@zivarah
Copy link
Contributor

@zivarah zivarah commented Feb 14, 2025

Fixes #12951

Proposed changes

  • ComManagedStream will ensure that the stream wrapping a non-seekable source stream has a position of 0 at the end of its constructor.

Customer Impact

  • Image.FromStream with a non-seekable stream should no longer throw an exception when loading .emf and .wmf images.

Regression?

  • Yes

Risk

  • Risk seems relatively low, in particular because this constructor created a stream with a position of 0 in .NET Framework and .NET Core 3.1. It seems unlikely that consumers are relying on a non-zero position.

Test methodology

  • Added unit tests ensuring that the stream returned by ComManagedStream.GetDataStream() has the expected state.
Microsoft Reviewers: Open in CodeFlow

Until .NET 5, `ComManagedStream` (then `GPStream`) would wrap a
non-seekable stream in a `MemoryStream` using the `MemoryStream(Byte[])`
constructor. This results in a stream with a position of 0.

dotnet/runtime commit 136527537e6 (Improve perfromance for loading from
Stream on Windows (dotnet/corefx#31142), 2018-07-20) updated this logic
to instead use `CopyTo` to populate the wrapping `MemoryStream`. This
results in a stream with a non-zero position.

It seems that this non-zero position causes some issues in downstream
code when using `Image.FromStream` to load `.emf` and `.wmf` files,
resulting in `LoadGdipImageFromStream` returning a status of 2 and thus
an exception.

Seek the wrapping `MemoryStream` back to the beginning after copying the
source stream into it to prevent this exception.

Fixes dotnet#12951
@zivarah
Copy link
Contributor Author

zivarah commented Feb 14, 2025

@dotnet-policy-service agree

@codecov
Copy link

codecov bot commented Feb 14, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 75.98329%. Comparing base (12d120b) to head (6fc354b).
Report is 10 commits behind head on main.

Additional details and impacted files
@@                 Coverage Diff                 @@
##                main      #12953         +/-   ##
===================================================
+ Coverage   75.97906%   75.98329%   +0.00422%     
===================================================
  Files           3265        3265                 
  Lines         643272      643273          +1     
  Branches       47426       47426                 
===================================================
+ Hits          488752      488780         +28     
+ Misses        150962      150935         -27     
  Partials        3558        3558                 
Flag Coverage Δ
Debug 75.98329% <100.00000%> (+0.00422%) ⬆️
integration 18.02996% <0.00000%> (+0.01357%) ⬆️
production 49.88590% <100.00000%> (+0.01028%) ⬆️
test 96.95378% <ø> (-0.00057%) ⬇️
unit 47.33428% <100.00000%> (+0.00088%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

@zivarah zivarah marked this pull request as ready for review February 14, 2025 08:18
@zivarah zivarah requested a review from a team as a code owner February 14, 2025 08:18
@zivarah
Copy link
Contributor Author

zivarah commented Feb 14, 2025

I will note that it seems likely there are some other related issues at play here:

  • It seems odd that only certain formats seem to fail (.jpg for example does not have any issues even without this fix)
  • When the stream passed to the ComManagedStream constructor is seekable, we are not doing anything to ensure its position either (and weren't in .NET Framework/.NET Core 3.1)

So this fix should likely be regarded as a partial fix to restore pre-.NET 5 behavior and address that regression, but it may make sense for someone to follow up with more investigation.

@lucienmaloney
Copy link

Yeah, the image formats aside from .emf and .wmf seem to be seeking to the beginning unconditionally, which causes a problem if the stream is intentionally offset. So for example this code works:

MemoryStream memoryStream = new MemoryStream();
pngImageStream.CopyTo(memoryStream);
memoryStream.Position = 0;
Image.FromStream(memoryStream);

But this code fails when it shouldn't:

MemoryStream memoryStream = new MemoryStream();
memoryStream.Write(new byte[200], 0, 200);
pngImageStream.CopyTo(memoryStream);
memoryStream.Position = 200;
Image.FromStream(memoryStream);

Though fixing that won't be as easy as just not resetting the stream position for seekable streams, since it'd be a breaking change for callers. I guess same for making .emf and .wmf seek to the beginning, since that would break them from passing the second case above, even if it would make them behave consistently. I do wonder then, out of all the formats supported, which unconditionally seek to the start, and which don't.

Copy link
Member

@JeremyKuhne JeremyKuhne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for digging into this!

@JeremyKuhne JeremyKuhne added the 🚫 * NO-MERGE * The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 18, 2025
@JeremyKuhne
Copy link
Member

Marking as no merge temporarily as I try and write a test that repros the problem with WMF. Will address this today.

@JeremyKuhne JeremyKuhne added servicing-consider .NET Shiproom label indicating a PR seeks to enter into a branch under Tell-Mode criteria and removed 🚫 * NO-MERGE * The PR is not ready for merge yet (see discussion for detailed reasons) labels Feb 18, 2025
@JeremyKuhne JeremyKuhne merged commit cdc3579 into dotnet:main Feb 18, 2025
8 checks passed
@JeremyKuhne JeremyKuhne removed the servicing-consider .NET Shiproom label indicating a PR seeks to enter into a branch under Tell-Mode criteria label Feb 18, 2025
@dotnet-policy-service dotnet-policy-service bot added this to the 10.0 Preview2 milestone Feb 18, 2025
@zivarah zivarah deleted the com-managed-stream-position-fix branch February 18, 2025 22:14
JeremyKuhne pushed a commit to JeremyKuhne/winforms that referenced this pull request Feb 18, 2025
…otnet#12953)

Until .NET 5, `ComManagedStream` (then `GPStream`) would wrap a
non-seekable stream in a `MemoryStream` using the `MemoryStream(Byte[])`
constructor. This results in a stream with a position of 0.

dotnet/runtime commit 136527537e6 (Improve perfromance for loading from
Stream on Windows (dotnet/corefx#31142), 2018-07-20) updated this logic
to instead use `CopyTo` to populate the wrapping `MemoryStream`. This
results in a stream with a non-zero position.

It seems that this non-zero position causes some issues in downstream
code when using `Image.FromStream` to load `.emf` and `.wmf` files,
resulting in `LoadGdipImageFromStream` returning a status of 2 and thus
an exception.

Seek the wrapping `MemoryStream` back to the beginning after copying the
source stream into it to prevent this exception.

Fixes dotnet#12951
JeremyKuhne added a commit to JeremyKuhne/winforms that referenced this pull request Feb 18, 2025
This is a manual port of dotnet#12953. The code was refactored in .NET 9 so the fix needs manually applied.
LeafShi1 pushed a commit to LeafShi1/winforms that referenced this pull request Mar 6, 2025
…otnet#12953)

Until .NET 5, `ComManagedStream` (then `GPStream`) would wrap a
non-seekable stream in a `MemoryStream` using the `MemoryStream(Byte[])`
constructor. This results in a stream with a position of 0.

dotnet/runtime commit 136527537e6 (Improve perfromance for loading from
Stream on Windows (dotnet/corefx#31142), 2018-07-20) updated this logic
to instead use `CopyTo` to populate the wrapping `MemoryStream`. This
results in a stream with a non-zero position.

It seems that this non-zero position causes some issues in downstream
code when using `Image.FromStream` to load `.emf` and `.wmf` files,
resulting in `LoadGdipImageFromStream` returning a status of 2 and thus
an exception.

Seek the wrapping `MemoryStream` back to the beginning after copying the
source stream into it to prevent this exception.

Fixes dotnet#12951
Tanya-Solyanik added a commit that referenced this pull request Mar 11, 2025
This is a manual port of #12953. The code was refactored in .NET 9 so
the fix needs manually applied.
###### Microsoft Reviewers: [Open in
CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/dotnet/winforms/pull/12971)
Tanya-Solyanik added a commit that referenced this pull request Mar 11, 2025
#12970)

…#12953)

PORTS #12953 - DO NOT SQUASH

Until .NET 5, `ComManagedStream` (then `GPStream`) would wrap a
non-seekable stream in a `MemoryStream` using the `MemoryStream(Byte[])`
constructor. This results in a stream with a position of 0.

dotnet/runtime commit 136527537e6 (Improve perfromance for loading from
Stream on Windows (dotnet/corefx#31142), 2018-07-20) updated this logic
to instead use `CopyTo` to populate the wrapping `MemoryStream`. This
results in a stream with a non-zero position.

It seems that this non-zero position causes some issues in downstream
code when using `Image.FromStream` to load `.emf` and `.wmf` files,
resulting in `LoadGdipImageFromStream` returning a status of 2 and thus
an exception.

Seek the wrapping `MemoryStream` back to the beginning after copying the
source stream into it to prevent this exception.


###### Microsoft Reviewers: [Open in
CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/dotnet/winforms/pull/12970)
@github-actions github-actions bot locked and limited conversation to collaborators Mar 21, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Image.FromStream where stream's CanSeek is false crashes with "Parameter not valid" for .wmf/.emf images

3 participants