question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Icon.Save(stream) each time returns different results

See original GitHub issue

Description

We use the following code to compare two icons:

private static bool IconEquals(Icon icon1, Icon icon2) {
                var stream1 = new System.IO.MemoryStream();
                icon1.Save(stream1);
                var stream2 = new System.IO.MemoryStream();
                icon2.Save(stream2);
                return System.Linq.Enumerable.SequenceEqual(stream1.ToArray(), stream2.ToArray());
        }

With the net6.0 release the stream.ToArray() method each time returns different results for the same Icon. You can modify the IconEquals as follows and it will return false:

private static bool IconEquals(Icon icon1) {
                var stream1 = new System.IO.MemoryStream();
                icon1.Save(stream1);
                var stream2 = new System.IO.MemoryStream();
                icon1.Save(stream2);
                return System.Linq.Enumerable.SequenceEqual(stream1.ToArray(), stream2.ToArray());
        }

Reproduction Steps

Launch the following code with any Icon:

private static bool IconEquals(Icon icon1) {
                var stream1 = new System.IO.MemoryStream();
                icon1.Save(stream1);
                var stream2 = new System.IO.MemoryStream();
                icon1.Save(stream2);
                return System.Linq.Enumerable.SequenceEqual(stream1.ToArray(), stream2.ToArray());
        }

Expected behavior

The IconEquals method should return true.

Actual behavior

The IconEquals method returns false.

Regression?

Works on net5.0

Known Workarounds

No response

Configuration

Windows 10

Other information

No response

Issue Analytics

  • State:open
  • Created 2 years ago
  • Comments:13 (12 by maintainers)

github_iconTop GitHub Comments

4reactions
eerhardtcommented, Dec 3, 2021

OK, I did some investigating. It turns out this was caused by converting to COMWrappers. But not in the way you’d think, there isn’t a mistake in the code. Instead, as far as I can tell, this worked before completely by chance.

Background

As I stated above, the only difference between the two byte[]s is byte index 9. I’m not 100% sure where the definitive documentation for the ICO file format is, but reading https://en.wikipedia.org/wiki/ICO_(file_format)#Outline, it says that byte index 9 is:

<html> <body>
3 1 Reserved. Should be 0.[Notes 2]
</body> </html>

and Note 2 says: " Although Microsoft’s technical documentation states that this value must be zero, the icon encoder built into .NET (System.Drawing.Icon.Save) sets this value to 255. It appears that the operating system ignores this value altogether."

So that byte is “reserved” and basically isn’t used.

The problem

The issue is the data being passed into our code from Windows. Looking at the code for IPicture::SaveAsFile that we are calling into, I see it is using uninitialized stack data and writing that into the stream. Basically, it looks something like this:

struct PictureDesc
{
    BYTE        width;
    BYTE        height;
    BYTE        colors;
    BYTE        reserved;
// other values
};

And in the code, it is creating one of these PictureDescs on the stack and not initializing the BYTE reserved; field. This means whatever happens to be on that stack in that location is what is getting written to the stream.

Why did the COMWrappers change cause this?

Here I can only speculate, but my understanding is that before the COMWrappers change, the data on the stack in this exact spot is always consistent. It is still “garbage”, but it is always the “same garbage” when calling these methods back-to-back. However, if you would write these two streams out to disk between 2 different processes, even without the COMWrapper change, the values can be different in this 9th byte.

I tried the above code on .NET Framework 4.8, and it also fails with the 9th byte being different.

I was able to prove that the stack is the problem locally by changing this code to look like:

image

Which fixes the problem - the reserved field is always zero’d out with that change.

However, if I change that code to zero the stack out before calling Marshal.QueryInterface:

image

The problem is still there. So something in Marshal.QueryInterface is changing the stack in such a way that the first time this is called, that one slot in the stack is getting different data than the next few times it is called.

.NET Framework

I took the above program and ran it on .NET Framework 4.8, and it also fails for the first call, on index 9 (if you run it without a debugger attached. For some reason attaching a debugger doesn’t make it happen.)

Potential solutions

Clearing the stack, as done above, is a hack just for investigation. I wouldn’t commit that into System.Drawing.Common. Instead, I can think of the following solutions:

  1. Get Windows to fix the uninitialized stack data by adding a single line to set reserved = 0.
  2. When comparing icon streams, skip this byte value, since it can’t consistently be relied upon. You can just slice the first 8 bytes, then skip 9, and slice the rest when comparing.
  3. If we need to work around this issue in System.Drawing, we could post-process the stream after calling SaveAsFile in Windows, and explicitly set this index to 0 ourselves for each image in the file.

For sure I’m going to write an issue for (1) to be fixed. It will just take some time, and a Windows update to get it fixed. Solution (2) would mean anyone who is comparing these streams would need to make that change. To really fix this issue, I think we should do (3).

1reaction
eerhardtcommented, Dec 8, 2021

I’ve logged Windows bug https://microsoft.visualstudio.com/OS/_workitems/edit/37327236 for (1) above.

Read more comments on GitHub >

github_iconTop Results From Across the Web

c# - Why is Image.Save(Stream, ImageFormat) throwing an ...
Just convert image to icon: Icon myIcon = Icon.FromHandle(((Bitmap)myImage).GetHicon()). and then save it using stream: myIcon.Save(myStream);.
Read more >
The Difference Between stream().forEach() and ...
stream().forEach() and Collection.forEach(). In most cases, both will yield the same results, but we'll look at some subtle differences.
Read more >
Save icon, is the floppy disk icon dead?
No. They will draw a steamie - just about every time. It is the archetype of its class. The same goes for icons....
Read more >
Java Stream collect() Method Examples
If the stream source is unordered such as Set, then the collect() method can produce different results in each invocation.
Read more >
Parallel vs Sequential Stream in Java
Using parallel streams, our code gets divide into multiple streams which can be executed parallelly on separate cores of the system and the ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found