Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix reading EXR images on 32bit Windows with OpenEXR 3.3 #1952

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

ChristopherSchwartzXRITE
Copy link

@ChristopherSchwartzXRITE ChristopherSchwartzXRITE commented Jan 10, 2025

This PR fixes an issue with atomic_compare_exchange_strong in chunk.c due to inappropriate type on 32 bit platforms.

Context:

When switching from OpenEXR 3.2.4 to OpenEXR 3.3.2, I noticed that reading image content from file (via ImfInputFile::readPixels(int, int)) failed most of the time (but not always) in 32 bit Windows.

I found that OpenEXR 3.3 apparently changed the reading routine to use extract_chunk_table from OpenEXRCore's chunk.c.

Here, two variables eptr and nptr are declared to be of type uintptr_t in

uintptr_t eptr = 0, nptr = 0;

However, in

if (!atomic_compare_exchange_strong (
EXR_CONST_CAST (atomic_uintptr_t*, &(part->chunk_table)),
&eptr,
nptr))

, they are used in an atomic_compare_exchange_strong call as uint64_t* and uint64_t respectively.

See

static inline int
atomic_compare_exchange_strong (
uint64_t volatile* object, uint64_t* expected, uint64_t desired)
{
uint64_t prev =
(uint64_t) InterlockedCompareExchange64 (object, desired, *expected);
if (prev == *expected) return 1;
*expected = prev;
return 0;
}

While the latter isn't a problem per-se, the usage of &eptr as uint64_t* lets atomic_compare_exchange_strong interpret whatever is at the address of eptr as an 8 byte long number.
However, the actual type uintptr_t is only 4 bytes long.

So, whatever is in the next four bytes will garble the value originally stored in eptr.

As a result in the comparison of prev (which holds "0" if the exchange worked as expected) does not compare to the expected and declare value of uintptr_t eptr = 0 from line 568 but any number that is in the respective full 8 bytes of memory will fail and set the full 8 bytes to 0.

if (prev == *expected) return 1;
*expected = prev;
return 0;

This not only misleads the calling code to assume that it failed to thread-safe exchange the pointers but also overwrites the ctable pointer with the (now 8 bytes of zeros) addresss and eventually (falsely) report "out of memory".

if (nptr != UINTPTR_MAX) ctxt->free_fn (ctable);
ctable = (uint64_t*) eptr;
if (ctable == NULL)
return ctxt->standard_error (ctxt, EXR_ERR_OUT_OF_MEMORY);

My proposed minimal fix simply changes the type of eptr (and nptr) to always be an unsigned 64 bit value, which should work for both, 32 and 64 bit platforms.

Copy link

linux-foundation-easycla bot commented Jan 10, 2025

CLA Signed

The committers listed above are authorized under a signed CLA.

  • ✅ login: ChristopherSchwartzXRITE / name: Christopher Schwartz (6d8e291)

@meshula
Copy link
Contributor

meshula commented Jan 10, 2025

Thanks for finding this!

Would you mind doing the CLA business noted in the message above?

Copy link
Contributor

@meshula meshula left a comment

Choose a reason for hiding this comment

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

This looks good to me, but I'll defer to @kdt3rd for a second approval.

@ChristopherSchwartzXRITE
Copy link
Author

Would you mind doing the CLA business noted in the message above?

I reached out to my company's legal department but won't be able to sign off the CLA before monday.
I hope that's soon enough.

@meshula
Copy link
Contributor

meshula commented Jan 10, 2025

That's perfectly fine :) Thanks for looking into it.

@kdt3rd
Copy link
Contributor

kdt3rd commented Jan 12, 2025

hrm, it should have been uintptr_t in the msvc block at lines 35-48, this is more likely my poor win32 programming substituting in for not having stdatomic.h properly implemented in msvc at the time I did it - there should be a 32 vs 64 bit switch depending on what size uintptr_t is - it is always pointing to a uint64 as you note (it's an offset into the file), but might be a 32-bit pointer (4-byte pointer) pointing to that array of 8-byte values, so what is there seems not quite right to me, although will work, but not as efficiently as it could on a 32-bit machine, the implementations of the atomic function for msvc should instead check #if sizeof(void *) == 4 or so and it should deal with uintptr_t...

@kdt3rd
Copy link
Contributor

kdt3rd commented Jan 12, 2025

concretely, those two functions at line 35 should likely be #ifdef _WIN64 then use Interlocked64 or use Interlocked32 and take uintptr_t values... thanks for finding that

@ChristopherSchwartzXRITE
Copy link
Author

concretely, those two functions at line 35 should likely be #ifdef _WIN64 then use Interlocked64 or use Interlocked32 and take uintptr_t values... thanks for finding that

I'll take a look at implementing it more efficent using the proposed #ifdef

@ChristopherSchwartzXRITE
Copy link
Author

ChristopherSchwartzXRITE commented Jan 13, 2025

I did take another look and tried the following:

static inline int
atomic_compare_exchange_strong (
    atomic_uintptr_t volatile* object, uintptr_t* expected, uintptr_t desired)
{
    uintptr_t prev =
#    ifdef _WIN64
        (uintptr_t) InterlockedCompareExchange64 (object, desired, *expected);
#    else
        (uintptr_t) InterlockedCompareExchange ((uintptr_t volatile*) object, desired, *expected);
#    endif
    if (prev == *expected) return 1;
    *expected = prev;
    return 0;
}

This seemlingly worked on 32 and 64 bits and allowed me to revert eptr and nptr to be uintptr_t.

However, please note that this code now has a conversion of pointer to a 64-bit value atomic_uintptr_t volatile* object to a pointer to a 32-bit value via (uintptr_t volatile*) object.

The reason is the define for atomic_uintptr_t here:

/* yeah, yeah, might be a 32-bit pointer, but if we make it the same, we
* can write less since we know support is coming (eventually) */
typedef uint64_t atomic_uintptr_t;

This would introduce a similar error as the one I wanted to fix. However, instead of falsely reading additional bytes, here the four additional bytes wouldn't be correclty overwritten in the scope of atomic_compare_exchange_strong and could thus result in a different value when interpreted as a 64-bit value on the outside scope again.

I also briefly tried changing the atomic_uintptr_t typedef to be a 32 bit value on 32 bit systems, but this did give me incompatible type warnings all over the place.

TL;DR

I would rather keep the proposed solution to use 64 bit values for eptr and nptr instead of revising types all over the OpenEXRCore library and potentially introducing new bugs.

…pe on 32 bit platforms

Signed-off-by: Christopher Schwartz <[email protected]>
@ChristopherSchwartzXRITE
Copy link
Author

Would you mind doing the CLA business noted in the message above?

It took a few more days, but now it's signed.

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.

3 participants