Repository navigation
BUG: fix undefined behavior flagged by UBSAN - #65831
Conversation
| return <int64_t>(<uint64_t>a - <uint64_t>b) | ||
|
|
||
|
|
||
| cdef int64_t wrap_add(int64_t a, int64_t b) noexcept: |
There was a problem hiding this comment.
This doesn't fix anything - it just suppresses what UBSAN is warning about. Can we properly raise when bounds are exceeded?
There was a problem hiding this comment.
Can we properly raise when bounds are exceeded?
I'll be honest with you, until now I don't understand what would be the correct way to manage the overflow in this module, so I asked gemini to look at the call stack to decide on how it should be handled.
Here is its raw message on why not raise an error directly or assign an invalid value:
The choice to use wrapping arithmetic rather than immediate errors or NaT assignments is driven by the existing architecture of the pandas timestamp library and the
requirement to maintain specific error behaviors.
1. Preserving the "Wrap-and-Catch" Pattern
The current implementation of tz_localize and tz_convert follows a specific pattern:
1. Localize: Perform the arithmetic to add/subtract the timezone offset.
2. Validate: Call check_overflows (in conversion.pyx) on the result.
If the arithmetic overflows and wraps around (e.g., a date in 1677 wraps to 2262), the check_overflows function detects that the resulting year is physically impossible for
that timestamp and raises a highly specific OutOfBoundsDatetime error with a formatted message (e.g., "Converting 1677-09-21 00:12:43 underflows past 1677-09-21
00:12:43.145224193").
2. Why not immediate errors?
Raising the error immediately inside _get_utc_bounds would require:
- Duplicating complex error-formatting logic into a low-level Cython function.
- Ensuring the error message perfectly matches what users and existing tests expect.
- Changing a design pattern that is used consistently across the tslibs module.
3. Why not NaT?
Assigning NaT (or NPY_NAT) would actually be a behavior change:
- Expectation: Localization that pushes a value out of bounds is required to raise OutOfBoundsDatetime in pandas. Returning NaT would silently swallow the error.
- Ambiguity: NaT is also used for "non-existent" times (like the hour skipped during DST). If we returned NaT for overflows, we would lose the ability to distinguish
between a "missing hour" and a "value out of bounds."
4. Satisfying UBSAN
Signed integer overflow is Undefined Behavior (UB) in C. Even if it "works" on most CPUs by wrapping, compilers are allowed to optimize based on the assumption that it
never happens, which can lead to subtle bugs.
By using unsigned casts for wrapping, we transform Undefined Behavior into Defined Behavior. The result is identical at the bit level, but it tells the compiler (and UBSAN)
that the wrap-around is intentional. This fixes the sanitizer warning while keeping the library's logic intact and performant.
There was a problem hiding this comment.
Cython has an overflowcheck directive you can add as a wrapper that should do the right thing and raise.
In older versions of Cython you could argue this was "slow" but I think in the past few years they implemented it using the compiler built-in's, so its negligible overhead
There was a problem hiding this comment.
AFAIU, we shouldn't be raising in this function. For example, in this test:
pandas/pandas/tests/scalar/timestamp/methods/test_tz_localize.py
Lines 26 to 27 in ad4695c
It undeflows in _get_utc_bounds on val - ppd when handling the left side
pandas/pandas/_libs/tslibs/tzconversion.pyx
Lines 487 to 506 in ad4695c
but computes correct results for the right side, and seems to rely on it, but I need to give it a better look.
I think that wrapping is unintentional and should be changed, but raising an error seems the wrong way to go.
There was a problem hiding this comment.
Sounds good. Open to refactors; ideally we never have UB as it is not something we can "rely on"
There was a problem hiding this comment.
Not sure if it's correct, but with the last modification I changed the behavior on overflow to the following:
- For the
val ± ppd, I assign isl or isr to one of the extremas. - for the
val ± delta, I assign an status code indicating overflow/underflow. If it happens on both sides, I raise an error. If at least one of the sides remain valid, then the other side will beNPY_NATand the valid side will be used here:
pandas/pandas/_libs/tslibs/tzconversion.pyx
Lines 365 to 369 in 39d4a02
2ccec87 to
71b5bc6
Compare
eb92568 to
45261d6
Compare
| BE_OVERFLOW | ||
|
|
||
|
|
||
| cdef void raise_out_of_bounds( |
There was a problem hiding this comment.
I think with cdef void you pay a performance penalty because you have to check the global Python error indicator. For high performance code I'd stick to the convention of returning an int where a non-zero value represents an error
There was a problem hiding this comment.
I don't think that performance matters here. This is only called when an overflow happens on both sides and the goal is to raise a Python exception.
|
|
||
| delta_l = deltas[isl] | ||
| if delta_l > 0 and val < NPY_NAT + 1 + delta_l: | ||
| v_l_err = BE_UNDERFLOW |
There was a problem hiding this comment.
In the case of UNDERFLOW / OVERFLOW should we just return early?
There was a problem hiding this comment.
The other side can be valid, so it's not correct to return early.
| return str(ts) | ||
|
|
||
|
|
||
| cdef enum BoundaryError: |
There was a problem hiding this comment.
| cdef enum BoundaryError: | |
| cdef enum BoundaryStatus: |
Semantics nit but I guess BE_OK isn't an error
| isr = 0 | ||
|
|
||
| delta_r = deltas[isr] | ||
| if delta_r > 0 and val < NPY_NAT + 1 + delta_r: |
There was a problem hiding this comment.
Do we have to hard-code all of these conditions or can we not just have the built-in overflow detection? The latter would definitely be easier to maintain and in many cases is more performant than hand-rolled checks
There was a problem hiding this comment.
I found a little bit convoluted and misleading to use function-like macros in Cython. I am not sure if they provide a built-in check that won't cause a Python exception.
There was a problem hiding this comment.
Hmm yea that's kind of challenging to express in Cython. Any chance this macro syntax to accept variadic arguments helps? Seems like we could at the very least call the generic macros we already have in portable.h (?)
| accum[lab, j] *= val | ||
| if int64float_t is int64_t: | ||
| # Use uint64_t to avoid UB on signed integer overflow. | ||
| accum[lab, j] = <int64_t>(<uint64_t>accum[lab, j] * |
There was a problem hiding this comment.
Not sure I understand this change - this seems to just mask an issue via a cast (?)
There was a problem hiding this comment.
Wrapping on overflow is intentional in the groupby.
pandas/pandas/tests/groupby/test_cumulative.py
Lines 66 to 80 in 9d38953
| newarr = np.asarray(arr, dtype=object) | ||
| for i in range(n): | ||
| val = newarr[i] | ||
| val = arr[i] |
There was a problem hiding this comment.
What UB is this avoiding? This seems like it should be a separate change / performance boost to elide a cast to object (?)
There was a problem hiding this comment.
misaligned load
pandas/_libs/lib.cpython-314-x86_64-linux-gnu.so.p/pandas/_libs/lib.pyx.c:30057:30: runtime error: load of misaligned address 0x7c513cf51815 for type 'PyObject *' (aka 'struct _object *'), which requires 8 byte alignment
0x7c513cf51815: note: pointer points here
00 00 00 00 10 a1 13 3d 71 7b 00 00 00 d8 d2 05 bf d8 d2 05 bf 01 00 00 00 02 00 00 00 00 00 00
45261d6 to
491f6ba
Compare
ci(sanitizers): make misaligned unrecoverable fix: fix unaligned load in sas fix: fix misaligned load from lib.pyx
976a631 to
166af4c
Compare
|
Thanks @Alvaro-Kothe |
…-comparison * upstream/main: (137 commits) DOC: add inline examples for Multiindexing/Levels section (pandas-dev#64867) PERF: read_csv integer fast path using fast_float's digit helpers (pandas-dev#66487) PERF: single-pass composite-key factorization for multi-key merges (pandas-dev#66124) PERF: merge_asof with by (pandas-dev#66121) CI: remove pyarrow 24.0 restriction for typing (pandas-dev#66563) PERF: read_csv string columns, cutting per-column-chunk GIL work (pandas-dev#66619) CI: enable python3.15 job (pandas-dev#66473) BUG: read_csv parallel default ignored CPU affinity and cgroup limits (pandas-dev#66613) PERF: fix performance regression in string construction from pandas-devGH-65831 (pandas-dev#66351) DEPR: Series.dt.freq returning a string instead of DateOffset (pandas-dev#64701) DOC: Improve pull request guideline wording (pandas-dev#66526) DOC: Fix Series.str.zfill docstring incorrectly stating it differs from str.zfill (pandas-dev#66460) BUG: IntervalArray/IntervalIndex only validated left endpoint dtype, allowing dtype.subtype to disagree with right.dtype (pandas-dev#66530) PERF: add an Arrow/NumPy hybrid groupby path for decimal and string types. (pandas-dev#63416) TST: speed up test_string_fast_path_matches_object_path (pandas-dev#66610) CLN: remove unreachable ArrowDtype exclusion in _factorize_keys (pandas-dev#66337) BUG: skew/kurt on ArrowDtype returned the biased statistic; kurt raised (pandas-dev#66336) PERF: recover read_csv string-column speed after pandas-devGH-66415 (pandas-dev#66426) BUG: datetimelike arithmetic landing on the NaT sentinel returned NaT (GH#66549) (pandas-dev#66602) CI: Update pixi.lock (pandas-dev#66598) ...
stacked on top of #65812.