Fix/colorjitter aliasing and params validation - #1063
Conversation
- get_testable_objects() previously only recursed into modules and classes, so any docstring on a standalone function or method was silently skipped and never tested. - Also skip (rather than trivially pass) docstrings with zero >>> examples, so 'no examples found' is distinguishable from 'examples ran and passed'.
|
PR Summary by QodoFix fused ColorJitter UB and validate Python params; improve doctest collection
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
Code Review by Qodo
1. apply_saturation_inplace unsafe no SAFETY
|
| unsafe { | ||
| let r = *buf.get_unchecked(base) as f32; | ||
| let g = *buf.get_unchecked(base + 1) as f32; | ||
| let b = *buf.get_unchecked(base + 2) as f32; | ||
| let gray = r * LW[0] + g * LW[1] + b * LW[2]; | ||
| let gw = gray * inv_sat; | ||
| *buf.get_unchecked_mut(base) = (r * saturation + gw).clamp(0.0, 255.0) as u8; | ||
| *buf.get_unchecked_mut(base + 1) = (g * saturation + gw).clamp(0.0, 255.0) as u8; | ||
| *buf.get_unchecked_mut(base + 2) = (b * saturation + gw).clamp(0.0, 255.0) as u8; | ||
| } |
There was a problem hiding this comment.
1. apply_saturation_inplace unsafe no safety 📘 Rule violation ⛨ Security
apply_saturation_inplace and apply_hue_inplace introduce unsafe blocks using unchecked indexing (get_unchecked/get_unchecked_mut) without an immediately preceding // SAFETY: justification comment. This violates the unsafe-justification requirement and makes the soundness assumptions harder to audit and maintain.
Agent Prompt
## Issue description
`apply_saturation_inplace` and `apply_hue_inplace` use `unsafe` (`get_unchecked` / `get_unchecked_mut`) for unchecked indexing without an immediately preceding `// SAFETY:` comment that explains why the indexing is sound.
## Issue Context
Both functions rely on invariants such as `npixels * 3 <= buf.len()` and correct per-pixel indexing where each loop iteration accesses `base`, `base+1`, and `base+2` within bounds. These assumptions should be documented directly above each `unsafe` block so future edits don’t accidentally invalidate the memory-safety invariants and so the code meets PR Compliance ID 7’s unsafe-justification requirement.
## Fix Focus Areas
- kornia-py/src/augmentations.rs[169-190]
- kornia-py/src/augmentations.rs[314-341]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| /// Returns a `PyKeyError` (rather than panicking) if the key is missing, | ||
| /// since `params` dicts come from arbitrary user code and a malformed dict | ||
| /// should surface as a normal Python exception, not a Rust panic. | ||
| fn dict_get<'py, T>(d: &Bound<'py, PyDict>, key: &str) -> PyResult<T> | ||
| where | ||
| T: for<'a> FromPyObject<'a, 'py, Error = PyErr>, | ||
| { | ||
| d.get_item(key)?.unwrap().extract::<T>() | ||
| d.get_item(key)? | ||
| .ok_or_else(|| { | ||
| PyErr::new::<pyo3::exceptions::PyKeyError, _>(format!( | ||
| "missing required key {key:?}" | ||
| )) | ||
| })? | ||
| .extract::<T>() |
There was a problem hiding this comment.
2. No tests for new pyerrs 📘 Rule violation ☼ Reliability
The PR changes ColorJitter params parsing to return PyKeyError/PyValueError instead of panicking, but adds no regression tests that assert these exceptions are raised for malformed params. Without explicit tests, the behavior can regress back into panics or incorrect exception types.
Agent Prompt
## Issue description
The PR adds new error handling behavior for malformed `params` dicts (`PyKeyError` for missing keys, `PyValueError` for invalid op indices) but does not add regression tests validating these cases.
## Issue Context
Existing Python tests cover valid `ColorJitter.sample()`/`params=` usage but do not cover malformed dictionaries (missing required keys) or invalid `order` indices.
## Fix Focus Areas
- kornia-py/src/augmentations.rs[47-63]
- kornia-py/src/augmentations.rs[641-657]
- kornia-py/tests/test_image.py[699-726]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| for v in buf.iter_mut() { | ||
| *v = (*v as f32 + b).clamp(0.0, 255.0) as u8; | ||
| } | ||
| } else { | ||
| let lut = build_linear_lut(b, c, m); | ||
| apply_lut(src, dst, &lut); | ||
| for v in buf.iter_mut() { | ||
| *v = lut[*v as usize]; | ||
| } |
There was a problem hiding this comment.
3. Brightness quantization mismatch 🐞 Bug ≡ Correctness
flush_linear_inplace applies brightness via float add + as u8 truncation, but the non-inplace path uses apply_brightness_sat which rounds the offset first. When jitter op order causes the mixed path to flush brightness in-place, results can differ by 1 intensity level per channel compared to the src→dst flush behavior.
Agent Prompt
### Issue description
`flush_linear_inplace` currently applies brightness using floating-point addition followed by a cast to `u8`, which truncates. The non-in-place linear flush uses `apply_brightness_sat`, which rounds the brightness offset and uses saturating integer add/sub. This introduces inconsistent brightness semantics depending on whether the mixed ColorJitter path flushes linear ops in-place.
### Issue Context
- Mixed-path ColorJitter flushes pending linear ops in-place when `dst_init` is true.
- The out-of-place brightness-only linear flush uses `apply_brightness_sat`, which rounds.
### Fix Focus Areas
- kornia-py/src/augmentations.rs[491-501]
- kornia-py/src/image.rs[457-469]
- kornia-py/src/augmentations.rs[456-463]
### Suggested fix
Update the `c == 1.0` branch in `flush_linear_inplace` to match `apply_brightness_sat` behavior:
- Compute `off_i16 = b.round() as i16` (same rounding rule).
- Apply `saturating_add(off)` / `saturating_sub(off)` in-place over `buf.iter_mut()`.
This keeps the no-aliasing guarantee while preserving existing brightness semantics.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
edgarriba
left a comment
There was a problem hiding this comment.
This pr seems a mix of things. Please one fix or feature per pr
|
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs within 7 days. Thank you for your contributions! |
Summary
This PR fixes two correctness issues:
Eliminates undefined behavior in the fused ColorJitter path caused by aliasing a shared (&[u8]) and mutable (&mut [u8]) reference to the same buffer.
Replaces panic paths for malformed Python params dictionaries with proper Python exceptions.
Details
Fix aliasing UB in fused ColorJitter
The fused ColorJitter implementation previously fabricated a &[u8] (current_src) pointing into the same memory as the live &mut [u8] destination buffer. Call sites then passed both references simultaneously to functions such as apply_saturation(input, dst, ...), creating aliased shared and mutable references to the same memory.
Although the implementation only performed read-before-write operations, this violates Rust's aliasing rules and is undefined behavior, with the potential for miscompilation under optimization or auto-vectorization.
This change:
Removes current_src.
Adds apply_saturation_inplace, apply_hue_inplace, and flush_linear_inplace.
Branches on dst_init to invoke either the in-place or out-of-place implementation, avoiding any aliased borrows.
Handle malformed parameter dictionaries gracefully
Malformed params dictionaries supplied from Python previously caused Rust panics:
dict_get used .unwrap() when a required key was missing.
dict_to_params indexed factors[op as usize] without validating the user-provided operation order.
These cases now return Python exceptions instead:
Missing required keys raise PyKeyError.
Invalid operation indices raise PyValueError.
Result
Removes undefined behavior from the fused ColorJitter implementation.
Converts panic paths into proper Python exceptions.
Improves robustness when handling invalid user input from Python.