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

Can't retain a scalar that's inline in a mutable collection #223

Open
snej opened this issue Jun 6, 2024 · 0 comments
Open

Can't retain a scalar that's inline in a mutable collection #223

snej opened this issue Jun 6, 2024 · 0 comments

Comments

@snej
Copy link
Contributor

snej commented Jun 6, 2024

This is kind of an ugly architectural issue I discovered recently. I don't think it's been seen in the wild.

The bug

  1. Create a mutable Array or Dict
  2. Add a bool or a small (<2048) int to it. A very short string might do this too.
  3. Get item 0 from the collection as a Value
  4. Retain it

Result: UBSan warnings about unaligned pointers, followed by an ASan fatal buffer overrun error. If sanitizers aren't enabled, you probably crash with heap corruption sometime later.

What's Going On

A value in a mutable container is stored in a ValueSlot, either inline or as a pointer. Small scalars (7 bytes or less) are stored inline. On little-endian CPUs, inline values are offset by 1 byte so that the first byte can be a 0xFF tag which marks it as inline. (If it were a pointer, this would be the low byte, which would be even since it's malloced.) That means an inline scalar Value has an odd address.

Unfortunately, an odd address usually denotes a heap-allocated mutable Value, i.e. a larger scalar or a collection. The HeapValue class deliberately offsets its embedded Value by one byte to ensure this. This is how Value::isMutable works.

The problem is that retaining a value [in HeapValue::retain] first checks isMutable; if so, it casts it to HeapValue and calls retain() on that. This incorrectly happens when the value is an inline scalar; but since it isn't actually a HeapValue, this ends up incrementing a 32-bit int 5 bytes before the Value, which is guaranteed to corrupt something. Ouch.

Workaround

Don't call FLValue_Retain on a value that might be a small scalar in a mutable collection.

Band-Aid Fix

I have a patch that doesn't really fix the problem, but at least allows you to tell that an inline scalar isn't itself mutable, and will cause the Retain call to throw an exception instead of corrupting data.

  • Store a different tag byte value in a ValueSlot than in a HeapValue
  • Value::isMutable checks this byte, and returns false for an inline scalar value
  • The Retain call detects the inline scalar and throws an InvalidData exception with the message "Can't retain scalar Value %p that's inline in a mutable container". There is a lengthy explanation in a comment just above the line where this is thrown.

A Real Fix?

Instead of throwing an exception, the Retain call should cast the Value to a ValueSlot pointer, then (somehow) find the ValueSlot's owning mutable collection, then retain that.

The "(somehow)" is the hard part. ValueSlot is intentionally tiny, 8 bytes. Adding a pointer back to its owner would double its size. But there's not really any other way to find it! The ValueSlots are the elements of either a vector or an unordered_map owned by the HeapValue.

snej added a commit that referenced this issue Jun 6, 2024
This is a bandaid for a nasty architectural bug.
snej added a commit that referenced this issue Jun 7, 2024
This is a bandaid for a nasty architectural bug; see the Github issue.

Retaining still doesn't work, but it now throws an exception instead
of overwriting out-of-bounds memory.
snej added a commit that referenced this issue Aug 5, 2024
This is a bandaid for a nasty architectural bug; see the Github issue.

Retaining still doesn't work, but it now throws an exception instead
of overwriting out-of-bounds memory.
snej added a commit that referenced this issue Aug 7, 2024
This is a bandaid for a nasty architectural bug; see the Github issue.

Retaining still doesn't work, but it now throws an exception instead
of overwriting out-of-bounds memory.
jianminzhao pushed a commit that referenced this issue Oct 7, 2024
…224)

This is a bandaid for a nasty architectural bug; see the Github issue.

Retaining still doesn't work, but it now throws an exception instead
of overwriting out-of-bounds memory.
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

No branches or pull requests

1 participant