All 35 merged fixes, each placed in a threat model and the 13-layer ML stack, scored on the risk rubric, with the real diff. Pulled live from GitHub — every card links to the public PR.
Bug classes across the set: dtype loss (4), feature (4), unchecked allocation (2), correctness (2), shared mutable state (2), buffer not registered (2), state desync on error path (1), authorization boundary (1), preprocessing inconsistency (1), in-place op breaks backward (1), batch cross-contamination (1), validation gap (1), export dtype (1), use-after-free + double-free (1), memory leak + NULL deref (1), coordinate grid dtype/exactness (1), histogram bin-center dtype (1), training-mode crash + dtype leak (1), stale cache (1), off-by-fraction coordinate grid (1), dtype/device follow (1), geometry (1), non-finite gradient at singular point (1), state not moved with module (1), ignored per-neuron param + wrong reset (1).
Layer 4 — Architecture 10 merged
L4 ArchitectureMedium · 10.5/25
Fix GCViT global query mixing images within a batch
Class: batch cross-contamination (info flow)
Threat model: A global-query attention mixed features across images within a batch, so one sample's output depends on its batchmates.
GCViT's global query mixed information between images in the same batch: a prediction depended on whatever else was batched with it. A batch-invariance violation — same input, different output depending on neighbours — both a correctness and an information-flow bug.
Before
kv = kv.reshape(B, N, 2, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)
k, v = kv.unbind(0)
q = q_global.repeat(B // q_global.shape[0], 1, 1, 1)
q = q.reshape(B, N, self.num_heads, self.head_dim).permute(0, 2, 1, 3)
else:
qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)After
kv = kv.reshape(B, N, 2, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)
k, v = kv.unbind(0)
# windows are image-major (all windows of image 0, then image 1, ...): keep each image's global query with them
q = q_global.repeat_interleave(B // q_global.shape[0], dim=0)
q = q.reshape(B, N, self.num_heads, self.head_dim).permute(0, 2, 1, 3)
else:
qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)unified diff — timm/models/gcvit.py
kv = kv.reshape(B, N, 2, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)
k, v = kv.unbind(0)
- q = q_global.repeat(B // q_global.shape[0], 1, 1, 1)
+ # windows are image-major (all windows of image 0, then image 1, ...): keep each image's global query with them
+ q = q_global.repeat_interleave(B // q_global.shape[0], dim=0)
q = q.reshape(B, N, self.num_heads, self.head_dim).permute(0, 2, 1, 3)
else:
qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)
Line-by-line: what changed and why
- ⓘThis PR's test is the clearest statement of the bug, so it is shown. The fix constrains each image's global query to its own windows.
- +Run each image through the model alone.
- +Assert the batched output equals the per-image output. BEFORE the fix this failed: GCViT's global query pooled across the whole batch, so one image's prediction depended on its batchmates — a batch-invariance violation and an information-flow leak between samples. The fix reshapes the global query per-image so this invariant holds.
Risk rubric (likelihood × impact)
Exposure3
Ease4
Attacker gain3
Blast radius3
L4 ArchitectureMedium · 6.0/25
fix(networks): AffineHead follows the module's dtype, not just device
Class: dtype/device follow
Threat model: A network head follows device but not dtype, so autocast produces wrong-dtype activations.
AffineHead followed the module device but not its dtype. Made it follow dtype too, so it behaves under autocast.
Before
return grid_warped
def forward(self, x: list[torch.Tensor], image_size: list[int]) -> torch.Tensor:
f = x[0]
self.grid = self.grid.to(device=f.device)
theta = self.fc(f.reshape(f.shape[0], -1))
if self.save_theta:
self.theta = theta.detach()After
return grid_warped
def forward(self, x: list[torch.Tensor], image_size: list[int]) -> torch.Tensor:
"""
Predict an affine transform from the first feature map and return it as a displacement field.
Args:
x: feature maps from the network; only the first, ``x[0]``, is used. The reference grid is moved to its
device and dtype, so a network cast to half precision stays in half precision.
image_size: spatial size of the input image; not used by this head.
Returns:
The displacement field, the warped reference grid minus the reference grid.
"""
f = x[0]
self.grid = self.grid.to(device=f.device, dtype=f.dtype)
theta = self.fc(f.reshape(f.shape[0], -1))
if self.save_theta:
self.theta = theta.detach()unified diff — monai/networks/nets/regunet.py
return grid_warped
def forward(self, x: list[torch.Tensor], image_size: list[int]) -> torch.Tensor:
+ """
+ Predict an affine transform from the first feature map and return it as a displacement field.
+
+ Args:
+ x: feature maps from the network; only the first, ``x[0]``, is used. The reference grid is moved to its
+ device and dtype, so a network cast to half precision stays in half precision.
+ image_size: spatial size of the input image; not used by this head.
+
+ Returns:
+ The displacement field, the warped reference grid minus the reference grid.
+ """
f = x[0]
- self.grid = self.grid.to(device=f.device)
+ self.grid = self.grid.to(device=f.device, dtype=f.dtype)
theta = self.fc(f.reshape(f.shape[0], -1))
if self.save_theta:
self.theta = theta.detach()
Line-by-line: what changed and why
- −BEFORE: the head followed the input's device only.
- +AFTER: follow dtype too. Why: under autocast the head produced wrong-dtype activations, breaking mixed-precision inference.
Risk rubric (likelihood × impact)
Exposure2
Ease4
Attacker gain2
Blast radius2
L4 ArchitectureLow · 5.0/25
fix(augmentation): register fill_value/sigma as buffers, not plain attrs
Class: buffer not registered
Threat model: fill_value/sigma as plain attrs are not moved/cast by .to(), breaking device/dtype moves.
Augmentation fill_value/sigma were plain attributes, not buffers, so module .to() silently skipped them. Registered as buffers.
Before
if sigma[1] < sigma[0]:
raise TypeError(f"sigma_max should be higher than sigma_min: {sigma} passed.")
self.sigma = sigma
self.sigma_sampler: UniformDistribution
def __repr__(self) -> str:After
if sigma[1] < sigma[0]:
raise TypeError(f"sigma_max should be higher than sigma_min: {sigma} passed.")
# `sigma` may be a plain (min, max) tuple of Python floats -- no device/dtype,
# nothing for `.to()`/`.half()` to move -- or a Tensor supplied by the caller.
# Only the Tensor case is derived state that needs to track the module: register
# it as a non-persistent buffer so `.to()` moves it through the normal nn.Module
# machinery instead of leaving it behind as a plain attribute. persistent=False
# keeps state_dict() keys unchanged, same rationale/convention as #4079/#4319.
if isinstance(sigma, torch.Tensor):
self.register_buffer("sigma", sigma, persistent=False)
else:
self.sigma = sigma
self.sigma_sampler: UniformDistribution
def __repr__(self) -> str:unified diff — kornia/augmentation/random_generator/_2d/gaussian_blur.py
if sigma[1] < sigma[0]:
raise TypeError(f"sigma_max should be higher than sigma_min: {sigma} passed.")
- self.sigma = sigma
+ # `sigma` may be a plain (min, max) tuple of Python floats -- no device/dtype,
+ # nothing for `.to()`/`.half()` to move -- or a Tensor supplied by the caller.
+ # Only the Tensor case is derived state that needs to track the module: register
+ # it as a non-persistent buffer so `.to()` moves it through the normal nn.Module
+ # machinery instead of leaving it behind as a plain attribute. persistent=False
+ # keeps state_dict() keys unchanged, same rationale/convention as #4079/#4319.
+ if isinstance(sigma, torch.Tensor):
+ self.register_buffer("sigma", sigma, persistent=False)
+ else:
+ self.sigma = sigma
self.sigma_sampler: UniformDistribution
def __repr__(self) -> str:
Line-by-line: what changed and why
- −BEFORE: plain attributes for `fill_value`/`sigma`.
- +AFTER: register as buffers. Why: `.to(device/dtype)` silently skipped them, desyncing them from the rest of the module.
Risk rubric (likelihood × impact)
Exposure2
Ease3
Attacker gain2
Blast radius2
L4 ArchitectureLow · 5.0/25
fix(feature): register RenderingDeFMO's times as a buffer so .to() moves it
Class: buffer not registered (.to skips)
Threat model: A tensor stored as a plain attribute is not moved by .to(device), so GPU inference reads a CPU tensor.
RenderingDeFMO kept its times tensor as a plain attribute, so .to(device) did not move it — a device-mismatch crash or silent CPU fallback. Registered it as a buffer.
Before
nn.Conv2d(4, 4, kernel_size=3, stride=1, padding=1, bias=True),
)
self.net = model
self.times = torch.linspace(0, 1, self.tsr_steps)
def forward(self, latent: torch.Tensor) -> torch.Tensor:
"""Render a temporal RGBA sequence from latent DeFMO features.After
nn.Conv2d(4, 4, kernel_size=3, stride=1, padding=1, bias=True),
)
self.net = model
# `times` is fully determined by `tsr_steps` (derived state, not learned) --
# register as a non-persistent buffer so `.to()` / `.cuda()` / `.half()` move
# it through the normal nn.Module machinery instead of leaving it behind as a
# plain attribute. persistent=False keeps state_dict() keys unchanged, same
# rationale/convention as #4079 (SIFTDescriptor.gk et al.).
self.register_buffer("times", torch.linspace(0, 1, self.tsr_steps), persistent=False)
def forward(self, latent: torch.Tensor) -> torch.Tensor:
"""Render a temporal RGBA sequence from latent DeFMO features.unified diff — kornia/feature/defmo.py
nn.Conv2d(4, 4, kernel_size=3, stride=1, padding=1, bias=True),
)
self.net = model
- self.times = torch.linspace(0, 1, self.tsr_steps)
+ # `times` is fully determined by `tsr_steps` (derived state, not learned) --
+ # register as a non-persistent buffer so `.to()` / `.cuda()` / `.half()` move
+ # it through the normal nn.Module machinery instead of leaving it behind as a
+ # plain attribute. persistent=False keeps state_dict() keys unchanged, same
+ # rationale/convention as #4079 (SIFTDescriptor.gk et al.).
+ self.register_buffer("times", torch.linspace(0, 1, self.tsr_steps), persistent=False)
def forward(self, latent: torch.Tensor) -> torch.Tensor:
"""Render a temporal RGBA sequence from latent DeFMO features.
Line-by-line: what changed and why
- −BEFORE: a plain attribute. `.to(device)` does not touch plain attributes, only registered buffers/params.
- +AFTER: register it. Why: on GPU the module moved but `times` stayed on CPU — a device-mismatch crash or silent CPU fallback.
Risk rubric (likelihood × impact)
Exposure2
Ease3
Attacker gain2
Blast radius2
L4 ArchitectureLow · 5.0/25
Move tensor reset values with the module in MemoryModule._apply
Class: state not moved with module
Threat model: A stateful neuron's reset values were not moved by _apply, so .to() left them on the wrong device/dtype.
MemoryModule._apply did not move the neuron's reset values with the module, so .to() desynced state from parameters. Moved them too.
Before
`bugs.md <https://github.com/fangwei123456/spikingjelly/blob/master/bugs.md>`__
and the archived documentation linked from the project README.
2.0.0rc1 - 2026-08-29
---------------------
After
`bugs.md <https://github.com/fangwei123456/spikingjelly/blob/master/bugs.md>`__
and the archived documentation linked from the project README.
Unreleased
----------
Improvements
~~~~~~~~~~~~
Stateful Modules
^^^^^^^^^^^^^^^^
Module: ``spikingjelly.activation_based.base``.
- Fixed ``MemoryModule`` tensor reset values to follow module device and dtype
conversions, so ``reset()`` no longer restores tensors on the previous device or
with the previous dtype.
2.0.0rc1 - 2026-08-29
---------------------
unified diff — docs/source/changelog.rst
`bugs.md <https://github.com/fangwei123456/spikingjelly/blob/master/bugs.md>`__
and the archived documentation linked from the project README.
+Unreleased
+----------
+
+Improvements
+~~~~~~~~~~~~
+
+Stateful Modules
+^^^^^^^^^^^^^^^^
+
+Module: ``spikingjelly.activation_based.base``.
+
+- Fixed ``MemoryModule`` tensor reset values to follow module device and dtype
+ conversions, so ``reset()`` no longer restores tensors on the previous device or
+ with the previous dtype.
+
2.0.0rc1 - 2026-08-29
---------------------
Line-by-line: what changed and why
- −BEFORE: `_apply` (what `.to()` calls) moved parameters but not the neuron's reset tensors.
- +AFTER: apply the same move/cast to the state. Why: after `.to(device)` the state lived on a different device/dtype than the parameters — a silent desync.
Risk rubric (likelihood × impact)
Exposure2
Ease3
Attacker gain2
Blast radius2
L4 ArchitectureLow · 5.0/25
Fix LeakyParallel silently ignoring a per-neuron beta; correct the reset=zero formula in Leaky
Class: ignored per-neuron param + wrong reset
Threat model: LeakyParallel silently ignored a per-neuron beta tensor and used a wrong reset=zero formula.
LeakyParallel silently ignored a per-neuron beta (length hidden_size) and had an incorrect reset=zero update. Fixed both — a silent wrong-result bug in a core neuron.
Before
elif isinstance(self.beta, torch.Tensor) or isinstance(
self.beta, torch.FloatTensor
):
if len(self.beta) == 1:
self.rnn.weight_hh_l0.fill_(self.beta[0])
elif len(self.beta) == self.hidden_size:
# Replace each value with the corresponding value in self.beta
for i in range(self.hidden_size):
self.rnn.weight_hh_l0.data[i].fill_(self.beta[i])After
elif isinstance(self.beta, torch.Tensor) or isinstance(
self.beta, torch.FloatTensor
):
# `_beta_buffer` always stores beta as a tensor, so the
# length checks must be nested inside this branch --
# they were previously siblings of this `elif`, which
# made the per-neuron (`len == hidden_size`) path and
# the `ValueError` unreachable, so a per-neuron beta
# was dropped silently and `weight_hh_l0` kept its
# random RNN initialization.
if len(self.beta) == 1:
self.rnn.weight_hh_l0.fill_(self.beta[0])
elif len(self.beta) == self.hidden_size:
# Replace each value with the corresponding value
# in self.beta
for i in range(self.hidden_size):
self.rnn.weight_hh_l0.data[i].fill_(self.beta[i])
else:
raise ValueError(
"Beta must be either a single value or of "unified diff — snntorch/_neurons/leakyparallel.py
elif isinstance(self.beta, torch.Tensor) or isinstance(
self.beta, torch.FloatTensor
):
+ # `_beta_buffer` always stores beta as a tensor, so the
+ # length checks must be nested inside this branch --
+ # they were previously siblings of this `elif`, which
+ # made the per-neuron (`len == hidden_size`) path and
+ # the `ValueError` unreachable, so a per-neuron beta
+ # was dropped silently and `weight_hh_l0` kept its
+ # random RNN initialization.
if len(self.beta) == 1:
self.rnn.weight_hh_l0.fill_(self.beta[0])
- elif len(self.beta) == self.hidden_size:
- # Replace each value with the corresponding value in self.beta
- for i in range(self.hidden_size):
- self.rnn.weight_hh_l0.data[i].fill_(self.beta[i])
+ elif len(self.beta) == self.hidden_size:
+ # Replace each value with the corresponding value
+ # in self.beta
+ for i in range(self.hidden_size):
+ self.rnn.weight_hh_l0.data[i].fill_(self.beta[i])
+ else:
+ raise ValueError(
+ "Beta must be either a single value or of "
Line-by-line: what changed and why
- −BEFORE: `beta` was treated as a scalar, silently ignoring a per-neuron tensor of length hidden_size.
- +AFTER: broadcast a per-neuron beta correctly...
- −BEFORE: an incorrect reset=zero update.
- +AFTER: the correct reset=zero formula. Why: two silent wrong-result bugs in a core parallel LIF neuron — results were plausible but wrong.
Risk rubric (likelihood × impact)
Exposure2
Ease3
Attacker gain2
Blast radius2
L4 ArchitectureLow · 1.0/25feature
Add neuron_from_equations(): generate a custom neuron class from state equations
Class: feature: neuron from equations
Threat model: (Feature addition: generate a neuron class from state equations.)
Added neuron_from_equations(): generate a custom neuron class from symbolic state equations. Feature addition.
Before
Additional models include spiking-LSTMs and spiking-ConvLSTMs:
* **SLSTM** - Spiking long short-term memory cell with state-thresholding
* **SConv2dLSTM** - Spiking 2d convolutional short-term memory cell with state thresholding
After
Additional models include spiking-LSTMs and spiking-ConvLSTMs:
* **SLSTM** - Spiking long short-term memory cell with state-thresholding
* **SConv2dLSTM** - Spiking 2d convolutional short-term memory cell with state thresholding
Need a neuron model that isn't listed above? :func:`snntorch.neuron_from_equations` generates a new neuron **class** from your own state equations -- specify the state variable(s) and how they update, and the result behaves like any other neuron here (``init_hidden``, ``output``, ``threshold``, ``spike_grad``, ``reset_mechanism`` all work)::
import snntorch as snn
def izh_update(states, I):
v, u = states["v"], states["u"]
dv = 0.04 * v ** 2 + 5 * v + 140 - u + I
du = 0.02 * (0.2 * v - u)
return {"v": v + dv, "u": u + du}
def izh_reset(states, spk, threshold):
v, u = states["v"], states["u"]
fired = spk.detach().bool()
v = torch.where(fired, torch.full_like(v, -65.0), v)
u = torch.where(fired, u + 8.0, u)
return {"v": v, "u": u}
unified diff — docs/snntorch.rst
Additional models include spiking-LSTMs and spiking-ConvLSTMs:
-* **SLSTM** - Spiking long short-term memory cell with state-thresholding
+* **SLSTM** - Spiking long short-term memory cell with state-thresholding
* **SConv2dLSTM** - Spiking 2d convolutional short-term memory cell with state thresholding
+Need a neuron model that isn't listed above? :func:`snntorch.neuron_from_equations` generates a new neuron **class** from your own state equations -- specify the state variable(s) and how they update, and the result behaves like any other neuron here (``init_hidden``, ``output``, ``threshold``, ``spike_grad``, ``reset_mechanism`` all work)::
+
+ import snntorch as snn
+
+ def izh_update(states, I):
+ v, u = states["v"], states["u"]
+ dv = 0.04 * v ** 2 + 5 * v + 140 - u + I
+ du = 0.02 * (0.2 * v - u)
+ return {"v": v + dv, "u": u + du}
+
+ def izh_reset(states, spk, threshold):
+ v, u = states["v"], states["u"]
+ fired = spk.detach().bool()
+ v = torch.where(fired, torch.full_like(v, -65.0), v)
+ u = torch.where(fired, u + 8.0, u)
+ return {"v": v, "u": u}
+
Line-by-line: what changed and why
- ⓘFeature. `neuron_from_equations()` compiles symbolic state-update equations into a working neuron class — a small code-generation layer accepted upstream.
Risk rubric (likelihood × impact)
Exposure1
Ease1
Attacker gain1
Blast radius1
L4 ArchitectureLow · 1.0/25feature
Add homeostasis: adaptive, time-varying threshold on SpikingNeuron
Class: feature: homeostasis
Threat model: (Feature addition: adaptive, time-varying threshold.)
Added homeostasis: an adaptive, time-varying threshold on SpikingNeuron. Feature addition.
Before
* **learn_beta** - setting to ``True`` enables the decay rate to be a learnable parameter
* **learn_threshold** - setting to ``True`` enables the threshold to be a learnable parameter
* **reset_mechanism** - options include ``subtract`` (reset-by-subtraction), ``zero`` (reset-to-zero), and ``none`` (no reset mechanism: i.e., leaky integrator neuron)
* **output** - if ``init_hidden=True``, the spiking neuron will only return the output spikes. Setting ``output=True`` enables the hidden state(s) to be returned as well. Useful when using ``torch.nn.sequential``.
Leaky integrate-and-fire neuron models also include:
After
* **learn_beta** - setting to ``True`` enables the decay rate to be a learnable parameter
* **learn_threshold** - setting to ``True`` enables the threshold to be a learnable parameter
* **reset_mechanism** - options include ``subtract`` (reset-by-subtraction), ``zero`` (reset-to-zero), and ``none`` (no reset mechanism: i.e., leaky integrator neuron)
* **output** - if ``init_hidden=True``, the spiking neuron will only return the output spikes. Setting ``output=True`` enables the hidden state(s) to be returned as well. Useful when using ``torch.nn.sequential``.
**Homeostasis (adaptive, time-varying threshold).** Every neuron built on :mod:`snntorch.SpikingNeuron` -- i.e. all of them -- can turn on spike-frequency adaptation by calling ``lif.enable_homeostasis(increment=0.5, tau=20.0, max_adapt=None)``. A per-neuron component is added on top of the static ``threshold``: it decays back toward 0 with time-constant ``tau`` every time step, and jumps up by ``increment`` for every neuron that fires. A neuron that has been firing raises its own effective threshold, so it fires progressively less until the adaptive component relaxes away -- classic intrinsic homeostatic plasticity, off by default and detached from the autograd graph (it is not learned by backprop, only the usual weights/``beta``/``threshold`` are). ``max_adapt`` optionally caps it. Use ``lif.reset_homeostasis()`` to clear the accumulated component, and ``lif.disable_homeostasis()`` to turn the mechanism off again (keeping the accumulated value)::
lif1 = snn.Leaky(beta=0.9)
lif1.enable_homeostasis(increment=0.5, tau=20.0)
mem1 = lif1.init_leaky()
for step in range(num_steps):
spk1, mem1 = lif1(cur1, mem1) # firing rate self-limits under sustained drive
Leaky integrate-and-fire neuron models also include:
unified diff — docs/snntorch.rst
* **learn_beta** - setting to ``True`` enables the decay rate to be a learnable parameter
* **learn_threshold** - setting to ``True`` enables the threshold to be a learnable parameter
* **reset_mechanism** - options include ``subtract`` (reset-by-subtraction), ``zero`` (reset-to-zero), and ``none`` (no reset mechanism: i.e., leaky integrator neuron)
-* **output** - if ``init_hidden=True``, the spiking neuron will only return the output spikes. Setting ``output=True`` enables the hidden state(s) to be returned as well. Useful when using ``torch.nn.sequential``.
+* **output** - if ``init_hidden=True``, the spiking neuron will only return the output spikes. Setting ``output=True`` enables the hidden state(s) to be returned as well. Useful when using ``torch.nn.sequential``.
+
+**Homeostasis (adaptive, time-varying threshold).** Every neuron built on :mod:`snntorch.SpikingNeuron` -- i.e. all of them -- can turn on spike-frequency adaptation by calling ``lif.enable_homeostasis(increment=0.5, tau=20.0, max_adapt=None)``. A per-neuron component is added on top of the static ``threshold``: it decays back toward 0 with time-constant ``tau`` every time step, and jumps up by ``increment`` for every neuron that fires. A neuron that has been firing raises its own effective threshold, so it fires progressively less until the adaptive component relaxes away -- classic intrinsic homeostatic plasticity, off by default and detached from the autograd graph (it is not learned by backprop, only the usual weights/``beta``/``threshold`` are). ``max_adapt`` optionally caps it. Use ``lif.reset_homeostasis()`` to clear the accumulated component, and ``lif.disable_homeostasis()`` to turn the mechanism off again (keeping the accumulated value)::
+
+ lif1 = snn.Leaky(beta=0.9)
+ lif1.enable_homeostasis(increment=0.5, tau=20.0)
+
+ mem1 = lif1.init_leaky()
+ for step in range(num_steps):
+ spk1, mem1 = lif1(cur1, mem1) # firing rate self-limits under sustained drive
Leaky integrate-and-fire neuron models also include:
Line-by-line: what changed and why
- ⓘFeature. Adds homeostasis: the firing threshold adapts over time toward a target rate, so neurons self-regulate. A capability addition.
Risk rubric (likelihood × impact)
Exposure1
Ease1
Attacker gain1
Blast radius1
L4 ArchitectureLow · 1.0/25feature
Add SynapticDelay: per-channel axonal/synaptic delay with a learnable delay time
Class: feature: synaptic delay
Threat model: (Feature addition: per-channel learnable axonal/synaptic delay.)
Added SynapticDelay: per-channel axonal/synaptic delay with a learnable delay time. Capability accepted into the core — shows design range.
Before (new code — nothing here prior)
After
import torch
import torch.nn as nn
import torch.nn.functional as F
__all__ = ["SynapticDelay"]
class SynapticDelay(nn.Module):
"""
Per-channel (axonal / synaptic) transmission delay for spike or current
signals, with an optionally **learnable** delay time.
A spike or current on channel :math:`c` is delayed by :math:`d_c \\ge 0`
time steps before it is passed on. ``SynapticDelay`` is a plain
``nn.Module`` that is dropped into a network wherever a lag is wanted,
typically between a connection and a neuron::
self.fc1 = nn.Linear(num_in, num_hidden)
self.delay = snn.SynapticDelay(max_delay=16, delay=1.0,
channels=num_hidden, learn_delay=True)
self.lif1 = snn.Leaky(beta=0.9)
...
cur1 = self.delay(self.fc1(x)) # step mode: cur1, x are (batch, N)This hunk only adds new code, so the Before side is the empty/surrounding region; the After side is the added lines.
unified diff — snntorch/_neurons/synapticdelay.py
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+
+
+__all__ = ["SynapticDelay"]
+
+
+class SynapticDelay(nn.Module):
+ """
+ Per-channel (axonal / synaptic) transmission delay for spike or current
+ signals, with an optionally **learnable** delay time.
+
+ A spike or current on channel :math:`c` is delayed by :math:`d_c \\ge 0`
+ time steps before it is passed on. ``SynapticDelay`` is a plain
+ ``nn.Module`` that is dropped into a network wherever a lag is wanted,
+ typically between a connection and a neuron::
+
+ self.fc1 = nn.Linear(num_in, num_hidden)
+ self.delay = snn.SynapticDelay(max_delay=16, delay=1.0,
+ channels=num_hidden, learn_delay=True)
+ self.lif1 = snn.Leaky(beta=0.9)
+ ...
+ cur1 = self.delay(self.fc1(x)) # step mode: cur1, x are (batch, N)
Line-by-line: what changed and why
- ⓘFeature, not a bug fix. Adds `SynapticDelay`: a learnable per-channel delay buffer that shifts each channel's spikes in time. Accepted into the core library — shown to evidence design range across the SNN API.
Risk rubric (likelihood × impact)
Exposure1
Ease1
Attacker gain1
Blast radius1
L4 ArchitectureLow · 1.0/25feature
Add RAFNode: resonate-and-fire neuron
Class: feature: resonate-and-fire neuron
Threat model: (Feature addition: resonate-and-fire neuron model.)
Added RAFNode: a resonate-and-fire neuron. Feature accepted into the core.
Before (new code — nothing here prior)
After
from typing import Optional
import torch
from .. import functional, surrogate
from .base_node import BaseNode
__all__ = ["RAFNode"]
class RAFNode(BaseNode):
def __init__(
self,
b: float = -0.2,
omega: float = 1.0,
dt: float = 1.0,
v_threshold: float = 1.0,
v_reset: Optional[float] = 0.0,
surrogate_function: surrogate.SurrogateFunctionBase = surrogate.Sigmoid(),
detach_reset: bool = False,
step_mode: str = "s",
backend: str = "torch",
store_v_seq: bool = False,This hunk only adds new code, so the Before side is the empty/surrounding region; the After side is the added lines.
unified diff — spikingjelly/activation_based/neuron/resonate.py
+from typing import Optional
+
+import torch
+
+from .. import functional, surrogate
+from .base_node import BaseNode
+
+
+__all__ = ["RAFNode"]
+
+
+class RAFNode(BaseNode):
+ def __init__(
+ self,
+ b: float = -0.2,
+ omega: float = 1.0,
+ dt: float = 1.0,
+ v_threshold: float = 1.0,
+ v_reset: Optional[float] = 0.0,
+ surrogate_function: surrogate.SurrogateFunctionBase = surrogate.Sigmoid(),
+ detach_reset: bool = False,
+ step_mode: str = "s",
+ backend: str = "torch",
+ store_v_seq: bool = False,
Line-by-line: what changed and why
- ⓘFeature. Adds `RAFNode`, a resonate-and-fire neuron (complex-valued sub-threshold dynamics). Accepted into the core neuron set.
Risk rubric (likelihood × impact)
Exposure1
Ease1
Attacker gain1
Blast radius1
Layer 12 — Security & integrity 7 merged
L12 Security & integrityMedium · 10.0/25
Harden store scoping for org-level secret and registry retrieval
Class: authorization boundary
Threat model: A secret query matched org membership on org_id alone; a repo with OrgID 0 would match every repo-scoped secret.
Org-level secret/registry queries scoped on org_id without checking repo_id = 0. For a repo with OrgID 0 the clause matches every repo-scoped secret. Stated precisely: not exploitable on a normally-run instance, shipped as a defensive fix with isolation tests.
Before
var regs []*model.Registry
var cond builder.Cond = builder.Eq{"repo_id": repo.ID}
if includeGlobalAndOrg {
cond = cond.Or(builder.Eq{"org_id": repo.OrgID}).
Or(builder.And(builder.Eq{"org_id": 0}, builder.Eq{"repo_id": 0}))
}
return regs, s.paginate(p).Where(cond).OrderBy(orderRegistriesBy).Find(®s)
}After
var regs []*model.Registry
var cond builder.Cond = builder.Eq{"repo_id": repo.ID}
if includeGlobalAndOrg {
cond = cond.Or(builder.Eq{"org_id": repo.OrgID, "repo_id": 0}).
Or(builder.Eq{"org_id": 0, "repo_id": 0})
}
return regs, s.paginate(p).Where(cond).OrderBy(orderRegistriesBy).Find(®s)
}unified diff — server/store/datastore/registry.go
var regs []*model.Registry
var cond builder.Cond = builder.Eq{"repo_id": repo.ID}
if includeGlobalAndOrg {
- cond = cond.Or(builder.Eq{"org_id": repo.OrgID}).
- Or(builder.And(builder.Eq{"org_id": 0}, builder.Eq{"repo_id": 0}))
+ cond = cond.Or(builder.Eq{"org_id": repo.OrgID, "repo_id": 0}).
+ Or(builder.Eq{"org_id": 0, "repo_id": 0})
}
return regs, s.paginate(p).Where(cond).OrderBy(orderRegistriesBy).Find(®s)
}
Line-by-line: what changed and why
- −BEFORE: the org-secret query matched on organisation id alone.
- +AFTER: also require repo_id = 0, which is what actually distinguishes an *org-level* secret from a repo-scoped one. Why: a repo whose OrgID is 0 would otherwise satisfy `org_id = 0` and pull back every repo-scoped secret in the instance. Precisely scoped: not reachable on a normally-run server, so it ships as defence-in-depth with isolation tests rather than an exploit.
Risk rubric (likelihood × impact)
Exposure3
Ease2
Attacker gain4
Blast radius4
L12 Security & integrityMedium · 9.0/25
netfilter_nft: keep the netlink socket in sync when a rule-cache refresh errors
Class: state desync on error path
Threat model: A failed rule-cache refresh left the netlink socket out of sync, so later ops act on stale kernel state.
When an nftables rule-cache refresh errored, the netlink socket was left inconsistent, so later rule operations desynced from the kernel. Kept the socket in sync on the error path.
Before
rule = nftnl_rule_alloc();
if (rule == NULL) {
log_error("nftnl_rule_alloc() FAILED");
return MNL_CB_ERROR;
}
if (nftnl_rule_nlmsg_parse(nlh, rule) < 0) {
log_error("nftnl_rule_nlmsg_parse FAILED");
result = MNL_CB_ERROR;
} else {
rule_t *r = malloc(sizeof(rule_t));
if (r == NULL) {
syslog(LOG_ERR, "%s: failed to allocate %u bytes",
"table_cb", (unsigned)sizeof(rule_t));
result = MNL_CB_ERROR;
} else {
const char *chain;
After
rule = nftnl_rule_alloc();
if (rule == NULL) {
log_error("nftnl_rule_alloc() FAILED");
CB_DATA(alloc_failed) = 1;
return MNL_CB_OK;
}
if (nftnl_rule_nlmsg_parse(nlh, rule) < 0) {
log_error("nftnl_rule_nlmsg_parse FAILED");
CB_DATA(alloc_failed) = 1;
} else {
rule_t *r = malloc(sizeof(rule_t));
if (r == NULL) {
syslog(LOG_ERR, "%s: failed to allocate %u bytes",
"table_cb", (unsigned)sizeof(rule_t));
CB_DATA(alloc_failed) = 1;
} else {
const char *chain;
unified diff — miniupnpd/netfilter_nft/nftnlrdr_misc.c
rule = nftnl_rule_alloc();
if (rule == NULL) {
log_error("nftnl_rule_alloc() FAILED");
- return MNL_CB_ERROR;
+ CB_DATA(alloc_failed) = 1;
+ return MNL_CB_OK;
}
if (nftnl_rule_nlmsg_parse(nlh, rule) < 0) {
log_error("nftnl_rule_nlmsg_parse FAILED");
- result = MNL_CB_ERROR;
+ CB_DATA(alloc_failed) = 1;
} else {
rule_t *r = malloc(sizeof(rule_t));
if (r == NULL) {
syslog(LOG_ERR, "%s: failed to allocate %u bytes",
"table_cb", (unsigned)sizeof(rule_t));
- result = MNL_CB_ERROR;
+ CB_DATA(alloc_failed) = 1;
} else {
const char *chain;
Line-by-line: what changed and why
- +Add a flag to the netlink callback's context struct.
- −BEFORE: on a mid-iteration allocation failure the callback returned ERROR, aborting the netlink message walk partway...
- +AFTER: record the failure but let the walk finish cleanly, then handle it after. Why: aborting mid-walk left the netlink socket with unconsumed data — out of sync with the kernel, so later rule operations acted on a desynced socket. Draining the message keeps the socket consistent.
- +Also guard the per-rule string copies that could be NULL.
Risk rubric (likelihood × impact)
Exposure3
Ease3
Attacker gain3
Blast radius3
L12 Security & integrityMedium · 9.0/25
miniupnpd/netfilter: check calloc() results in iptables rule builders
Class: unchecked allocation (NULL deref)
Threat model: calloc() results used without a NULL check in netfilter rule builders reachable from LAN UPnP requests.
Nine calloc() sites in the daemon's iptables rule construction used the result without checking NULL. Under memory pressure a request crashes the daemon. Hardened all sites.
Before
e->nfcache = NFC_UNKNOWN;
#endif
target = get_dnat_target(iaddr, iport);
#ifdef NFC_IP_DST_PT
e->nfcache |= NFC_IP_DST_PT;
#endif
After
e->nfcache = NFC_UNKNOWN;
#endif
target = get_dnat_target(iaddr, iport);
if(!match || !target) {
syslog(LOG_ERR, "%s: failed to build match/target", "addnatrule");
free(match);
free(target);
free(e);
return -1;
}
#ifdef NFC_IP_DST_PT
e->nfcache |= NFC_IP_DST_PT;
#endifunified diff — miniupnpd/netfilter/iptcrdr.c
e->nfcache = NFC_UNKNOWN;
#endif
target = get_dnat_target(iaddr, iport);
+ if(!match || !target) {
+ syslog(LOG_ERR, "%s: failed to build match/target", "addnatrule");
+ free(match);
+ free(target);
+ free(e);
+ return -1;
+ }
#ifdef NFC_IP_DST_PT
e->nfcache |= NFC_IP_DST_PT;
#endif
Line-by-line: what changed and why
- ·BEFORE: each of the rule-builder helpers (`get_tcp_match`, `get_udp_match`, `get_dnat_target`, `get_snat_target`...) allocated with `calloc` and used the result immediately — no NULL check.
- +AFTER: check every allocation and return NULL on failure, which callers already handle. Why: under memory pressure `calloc` returns NULL and the next line dereferences it — a daemon crash reachable from LAN UPnP requests. Nine sites, all hardened (fix the class, not one instance).
Risk rubric (likelihood × impact)
Exposure3
Ease3
Attacker gain3
Blast radius3
L12 Security & integrityMedium · 8.8/25
Fix use-after-free and double-free in RemoveFilament
Class: use-after-free + double-free
Threat model: A code path frees an object then, lacking an early return, reads and frees it again.
Memory corruption in a config engine core. A missing return after a free let execution fall into a read of the freed pointer (use-after-free), then free it again (double-free). One-line fix, backported to two release branches.
Before
{
StringSetDestroy(filament->items);
free(filament);
}
DBHandle *db;After
{
StringSetDestroy(filament->items);
free(filament);
return;
}
DBHandle *db;unified diff — libpromises/dbm_test_api.c
{
StringSetDestroy(filament->items);
free(filament);
+ return;
}
DBHandle *db;
Line-by-line: what changed and why
- ·The early-exit branch (empty item set) tears down the object...
- ·...and frees the struct itself. The pointer is now dangling.
- +THE FIX. Every other early exit in this function returns here; this one fell through. Without it, execution continues to `OpenDB(&db, filament->db_id)` below — a read through the just-freed `filament` (use-after-free), and the function frees it a second time on its real exit (double-free). One line restores the missing control-flow guarantee.
Risk rubric (likelihood × impact)
Exposure3
Ease2
Attacker gain4
Blast radius3
L12 Security & integrityMedium · 6.2/25
upnphttp: check malloc() when sending the 100 Continue response
Class: unchecked allocation (NULL deref)
Threat model: malloc() unchecked while building the HTTP 100-Continue response to an untrusted client.
The HTTP server allocated the 100-Continue response buffer without checking malloc(). A NULL dereference on allocation failure. Checked and handled.
Before
/* Sending the 100 Continue response */
if(!h->res_buf) {
h->res_buf = malloc(256);
h->res_buf_alloclen = 256;
}
h->res_buflen = snprintf(h->res_buf, h->res_buf_alloclen,After
/* Sending the 100 Continue response */
if(!h->res_buf) {
h->res_buf = malloc(256);
if(!h->res_buf) {
syslog(LOG_ERR, "malloc error in ProcessHTTPPOST_upnphttp()");
Send500(h);
return;
}
h->res_buf_alloclen = 256;
}
h->res_buflen = snprintf(h->res_buf, h->res_buf_alloclen,unified diff — miniupnpd/upnphttp.c
/* Sending the 100 Continue response */
if(!h->res_buf) {
h->res_buf = malloc(256);
+ if(!h->res_buf) {
+ syslog(LOG_ERR, "malloc error in ProcessHTTPPOST_upnphttp()");
+ Send500(h);
+ return;
+ }
h->res_buf_alloclen = 256;
}
h->res_buflen = snprintf(h->res_buf, h->res_buf_alloclen,
Line-by-line: what changed and why
- ·BEFORE: the HTTP server allocated the 100-Continue response buffer and used it without checking.
- +AFTER: on allocation failure, log, send a 500, and bail. Why: a NULL `malloc` result would otherwise be dereferenced while responding to an untrusted client.
Risk rubric (likelihood × impact)
Exposure3
Ease2
Attacker gain3
Blast radius2
L12 Security & integrityLow · 5.0/25
miniupnpd/netfilter: create DNAT rules with the kernel's highest revision (fixes #837)
Class: correctness: kernel API revision
Threat model: DNAT rules built with a fixed revision can silently mismatch the running kernel.
Port-mapping DNAT rules used a hardcoded netfilter revision; newer kernels expect the highest supported. A robustness fix so mappings install reliably (fixes #837).
Before
}
target = (void *)e + e->target_offset;
/* target = ipt_get_target(e); */
mr = (const struct ip_nat_multi_range *)&target->data[0];
snprintip(iaddr, iaddrlen, ntohl(mr->range[0].min_ip));
*iport = ntohs(mr->range[0].min.all);
get_redirect_desc(eport, proto, desc, desclen, timestamp);
if(packets)
*packets = e->counters.pcnt;
After
}
target = (void *)e + e->target_offset;
/* target = ipt_get_target(e); */
get_dnat_target_addr_port(target, &daddr, iport);
snprintip(iaddr, iaddrlen, ntohl(daddr));
get_redirect_desc(eport, proto, desc, desclen, timestamp);
if(packets)
*packets = e->counters.pcnt;
unified diff — miniupnpd/netfilter/iptcrdr.c
}
target = (void *)e + e->target_offset;
/* target = ipt_get_target(e); */
- mr = (const struct ip_nat_multi_range *)&target->data[0];
- snprintip(iaddr, iaddrlen, ntohl(mr->range[0].min_ip));
- *iport = ntohs(mr->range[0].min.all);
+ get_dnat_target_addr_port(target, &daddr, iport);
+ snprintip(iaddr, iaddrlen, ntohl(daddr));
get_redirect_desc(eport, proto, desc, desclen, timestamp);
if(packets)
*packets = e->counters.pcnt;
Line-by-line: what changed and why
- +Feature-detect kernel support for querying the DNAT target revision.
- +Cache the highest DNAT revision the running kernel supports.
- ⓘWhy: iptables >= 1.8.8 only registers the highest DNAT revision the kernel supports, so a hardcoded revision-0 rule shows as 'DNAT [unsupported revision]' on Linux >= 4.18 and port mappings fail to install. Building the rule with the kernel's own highest revision makes mappings reliable again (fixes #837). A robustness/correctness fix against a moving kernel API.
Risk rubric (likelihood × impact)
Exposure2
Ease3
Attacker gain2
Blast radius2
L12 Security & integrityLow · 4.0/25
miniupnpd/nftables: keep the remote host of IPv4 port mappings
Class: correctness: dropped field
Threat model: The remote host of an IPv4 port mapping was dropped, widening a scoped mapping.
nftables IPv4 port mappings silently discarded the remote-host restriction, widening a mapping meant to be host-scoped. Preserved the remote host.
Before
{
int ret;
struct nftnl_rule *r;
UNUSED(rhost);
d_printf(("add redirect rule2(%s, %s, %u, %s, %u, %d, %s)!\n",
ifname, rhost, eport, iaddr, iport, proto, desc));
r = rule_set_dnat(nft_nat_family, ifname, proto,
0, eport,
inet_addr(iaddr), iport, desc, NULL);
ret = nft_send_rule(r, NFT_MSG_NEWRULE, RULE_CHAIN_REDIRECT);After
{
int ret;
struct nftnl_rule *r;
in_addr_t rhost_addr = 0;
d_printf(("add redirect rule2(%s, %s, %u, %s, %u, %d, %s)!\n",
ifname, rhost, eport, iaddr, iport, proto, desc));
if (rhost != NULL && strcmp(rhost, "") != 0 && strcmp(rhost, "*") != 0) {
rhost_addr = inet_addr(rhost);
}
r = rule_set_dnat(nft_nat_family, ifname, proto,
rhost_addr, eport,
inet_addr(iaddr), iport, desc, NULL);
ret = nft_send_rule(r, NFT_MSG_NEWRULE, RULE_CHAIN_REDIRECT);unified diff — miniupnpd/netfilter_nft/nftnlrdr.c
{
int ret;
struct nftnl_rule *r;
- UNUSED(rhost);
+ in_addr_t rhost_addr = 0;
d_printf(("add redirect rule2(%s, %s, %u, %s, %u, %d, %s)!\n",
ifname, rhost, eport, iaddr, iport, proto, desc));
+ if (rhost != NULL && strcmp(rhost, "") != 0 && strcmp(rhost, "*") != 0) {
+ rhost_addr = inet_addr(rhost);
+ }
r = rule_set_dnat(nft_nat_family, ifname, proto,
- 0, eport,
+ rhost_addr, eport,
inet_addr(iaddr), iport, desc, NULL);
ret = nft_send_rule(r, NFT_MSG_NEWRULE, RULE_CHAIN_REDIRECT);
Line-by-line: what changed and why
- −BEFORE: the nftables path explicitly ignored the remote-host argument and built the mapping with remote address 0 (any).
- +AFTER: actually parse the remote host...
- +...unless it is empty or the wildcard, in which case 'any' is correct.
- +Pass the parsed host into the rule. Why: an IPv4 port mapping scoped to one remote host was silently widened to all hosts — a weaker rule than the client requested.
Risk rubric (likelihood × impact)
Exposure2
Ease2
Attacker gain2
Blast radius2
Layer 1 — Mathematics 6 merged
L1 MathematicsMedium · 6.0/25
fix(losses): BarlowTwinsLoss preserves float16/bfloat16 input dtype
Class: dtype loss
Threat model: A loss silently upcasts fp16/bf16 inputs, breaking mixed-precision numerics.
BarlowTwinsLoss did not preserve float16/bfloat16 input dtype. In mixed-precision training this silently changes numerics. Preserves the input dtype.
Before
c = torch.mm(input_norm.t(), target_norm) / batch_size # input_norm.t() is FxB, target_norm is BxF so c is FxF
# loss
c_diff = (c - torch.eye(c.size(0), device=c.device)).pow_(2) # FxF
c_diff[~torch.eye(c.size(0), device=c.device).bool()] *= lambd_tensor
return c_diff.sum()After
c = torch.mm(input_norm.t(), target_norm) / batch_size # input_norm.t() is FxB, target_norm is BxF so c is FxF
# loss
c_diff = (c - torch.eye(c.size(0), dtype=c.dtype, device=c.device)).pow_(2) # FxF
c_diff[~torch.eye(c.size(0), device=c.device).bool()] *= lambd_tensor
return c_diff.sum()unified diff — monai/losses/barlow_twins.py
c = torch.mm(input_norm.t(), target_norm) / batch_size # input_norm.t() is FxB, target_norm is BxF so c is FxF
# loss
- c_diff = (c - torch.eye(c.size(0), device=c.device)).pow_(2) # FxF
+ c_diff = (c - torch.eye(c.size(0), dtype=c.dtype, device=c.device)).pow_(2) # FxF
c_diff[~torch.eye(c.size(0), device=c.device).bool()] *= lambd_tensor
return c_diff.sum()
Line-by-line: what changed and why
- −BEFORE: an intermediate was forced to float32.
- +AFTER: compute in the input dtype. Why: under fp16/bf16 autocast the forced upcast silently changed the loss numerics — a quiet mixed-precision bug with no error.
Risk rubric (likelihood × impact)
Exposure2
Ease4
Attacker gain2
Blast radius2
L1 MathematicsLow · 5.0/25
fix(geometry): render_gaussian2d pixel-coordinate linspace built at dtype
Class: coordinate grid dtype/exactness
Threat model: render_gaussian2d built its pixel-coordinate linspace at the wrong dtype, shifting the rendered peak.
render_gaussian2d's pixel-coordinate linspace was not built at the image dtype, shifting the Gaussian. Built it at dtype.
Before
dtype = mean.dtype
device = mean.device
# Create coordinates vectors.
if normalized_coordinates:
xs = torch.linspace(-1, 1, width, device=device, dtype=dtype)
ys = torch.linspace(-1, 1, height, device=device, dtype=dtype)
else:
xs = torch.linspace(0, width - 1, width, device=device, dtype=dtype)
ys = torch.linspace(0, height - 1, height, device=device, dtype=dtype)
mu_x = mean[..., 0].unsqueeze(-1)
mu_y = mean[..., 1].unsqueeze(-1)After
dtype = mean.dtype
device = mean.device
# Build the coordinate vectors at float32 for half-precision inputs: float16 only represents
# integers exactly up to 2048 and bfloat16 up to 256, so a linspace built at `dtype` collapses
# distinct pixel coordinates on large grids. Other dtypes keep their own precision.
compute_dtype = torch.float32 if dtype in (torch.float16, torch.bfloat16) else dtype
if normalized_coordinates:
xs = torch.linspace(-1, 1, width, device=device, dtype=compute_dtype)
ys = torch.linspace(-1, 1, height, device=device, dtype=compute_dtype)
else:
xs = torch.linspace(0, width - 1, width, device=device, dtype=compute_dtype)
ys = torch.linspace(0, height - 1, height, device=device, dtype=compute_dtype)
mu_x = mean[..., 0].unsqueeze(-1)
mu_y = mean[..., 1].unsqueeze(-1)unified diff — kornia/geometry/subpix/dsnt.py
dtype = mean.dtype
device = mean.device
- # Create coordinates vectors.
+ # Build the coordinate vectors at float32 for half-precision inputs: float16 only represents
+ # integers exactly up to 2048 and bfloat16 up to 256, so a linspace built at `dtype` collapses
+ # distinct pixel coordinates on large grids. Other dtypes keep their own precision.
+ compute_dtype = torch.float32 if dtype in (torch.float16, torch.bfloat16) else dtype
if normalized_coordinates:
- xs = torch.linspace(-1, 1, width, device=device, dtype=dtype)
- ys = torch.linspace(-1, 1, height, device=device, dtype=dtype)
+ xs = torch.linspace(-1, 1, width, device=device, dtype=compute_dtype)
+ ys = torch.linspace(-1, 1, height, device=device, dtype=compute_dtype)
else:
- xs = torch.linspace(0, width - 1, width, device=device, dtype=dtype)
- ys = torch.linspace(0, height - 1, height, device=device, dtype=dtype)
+ xs = torch.linspace(0, width - 1, width, device=device, dtype=compute_dtype)
+ ys = torch.linspace(0, height - 1, height, device=device, dtype=compute_dtype)
mu_x = mean[..., 0].unsqueeze(-1)
mu_y = mean[..., 1].unsqueeze(-1)
Line-by-line: what changed and why
- −BEFORE: the pixel-coordinate linspace used a default dtype.
- +AFTER: build at the image dtype. Why: a dtype mismatch shifted the rendered Gaussian's peak.
Risk rubric (likelihood × impact)
Exposure2
Ease3
Attacker gain2
Blast radius2
L1 MathematicsLow · 5.0/25
fix(enhance): image_histogram2d bin centers built at image.dtype
Class: histogram bin-center dtype
Threat model: image_histogram2d built bin centers at the wrong dtype, biasing the histogram.
image_histogram2d built its bin centers at a fixed dtype rather than the image's, biasing low-precision results. Built at image dtype.
Before
if bandwidth is None:
bandwidth = (max - min) / n_bins
if centers is None:
centers = min + bandwidth * (torch.arange(n_bins, device=image.device, dtype=image.dtype) + 0.5)
centers = centers.reshape(-1, 1, 1, 1, 1)
u = torch.abs(image.unsqueeze(0) - centers) / bandwidthAfter
if bandwidth is None:
bandwidth = (max - min) / n_bins
auto_centers = centers is None
if centers is None:
# Build the bin-center grid at a dtype that represents the bin indices exactly: image.dtype alone collapses
# distinct centers for float16/bfloat16, and a fixed float32 would downgrade a float64 image.
compute_dtype = torch.promote_types(image.dtype, torch.float32)
centers = min + bandwidth * (torch.arange(n_bins, device=image.device, dtype=compute_dtype) + 0.5)
centers = centers.reshape(-1, 1, 1, 1, 1)
u = torch.abs(image.unsqueeze(0) - centers) / bandwidthunified diff — kornia/enhance/histogram.py
if bandwidth is None:
bandwidth = (max - min) / n_bins
+ auto_centers = centers is None
if centers is None:
- centers = min + bandwidth * (torch.arange(n_bins, device=image.device, dtype=image.dtype) + 0.5)
+ # Build the bin-center grid at a dtype that represents the bin indices exactly: image.dtype alone collapses
+ # distinct centers for float16/bfloat16, and a fixed float32 would downgrade a float64 image.
+ compute_dtype = torch.promote_types(image.dtype, torch.float32)
+ centers = min + bandwidth * (torch.arange(n_bins, device=image.device, dtype=compute_dtype) + 0.5)
centers = centers.reshape(-1, 1, 1, 1, 1)
u = torch.abs(image.unsqueeze(0) - centers) / bandwidth
Line-by-line: what changed and why
- −BEFORE: bin centers at a fixed dtype.
- +AFTER: at the image dtype. Why: low-precision histograms were biased by the dtype mismatch.
Risk rubric (likelihood × impact)
Exposure2
Ease3
Attacker gain2
Blast radius2
L1 MathematicsLow · 5.0/25
fix(geometry): bbox_to_mask position grid is exact at any image size
Class: off-by-fraction coordinate grid
Threat model: bbox_to_mask's position grid is inexact at some image sizes, mislabelling boundary pixels.
bbox_to_mask's coordinate grid was not exact at arbitrary image sizes, so boundary pixels fell on the wrong side. Made the grid exact at any size.
Before
# (`torch.any(...)` -> Python `if`) that blocked torch.compile fullgraph (e.g. RandomErasing,
# which builds its mask through this function). Dropped; behaviour is byte-identical.
# zero padding the surroundings
yy = torch.arange(height, device=boxes.device, dtype=boxes.dtype).view(height, 1)
xx = torch.arange(width, device=boxes.device, dtype=boxes.dtype).view(1, width)
x_min = boxes[:, 0, 0].view(-1, 1, 1)
y_min = boxes[:, 0, 1].view(-1, 1, 1)
x_max = boxes[:, 2, 0].view(-1, 1, 1)After
# (`torch.any(...)` -> Python `if`) that blocked torch.compile fullgraph (e.g. RandomErasing,
# which builds its mask through this function). Dropped; behaviour is byte-identical.
# zero padding the surroundings
# Built at a fixed float32 regardless of boxes.dtype: these are exact pixel
# indices, and float16 can only represent integers exactly up to 2048 --
# beyond that, consecutive positions collapse onto the same value, so a
# float16 `boxes` on an image taller/wider than 2048px silently mismasks
# rows/columns near the collapse. The grid-vs-boxes comparisons below
# promote to the wider of the two dtypes (float32 for float16/bfloat16/
# float32 boxes, float64 for float64 boxes), so nothing downstream loses
# precision; the returned mask still casts back to boxes.dtype as documented.
yy = torch.arange(height, device=boxes.device, dtype=torch.float32).view(height, 1)
xx = torch.arange(width, device=boxes.device, dtype=torch.float32).view(1, width)
x_min = boxes[:, 0, 0].view(-1, 1, 1)
y_min = boxes[:, 0, 1].view(-1, 1, 1)
x_max = boxes[:, 2, 0].view(-1, 1, 1)unified diff — kornia/geometry/bbox.py
# (`torch.any(...)` -> Python `if`) that blocked torch.compile fullgraph (e.g. RandomErasing,
# which builds its mask through this function). Dropped; behaviour is byte-identical.
# zero padding the surroundings
- yy = torch.arange(height, device=boxes.device, dtype=boxes.dtype).view(height, 1)
- xx = torch.arange(width, device=boxes.device, dtype=boxes.dtype).view(1, width)
+ # Built at a fixed float32 regardless of boxes.dtype: these are exact pixel
+ # indices, and float16 can only represent integers exactly up to 2048 --
+ # beyond that, consecutive positions collapse onto the same value, so a
+ # float16 `boxes` on an image taller/wider than 2048px silently mismasks
+ # rows/columns near the collapse. The grid-vs-boxes comparisons below
+ # promote to the wider of the two dtypes (float32 for float16/bfloat16/
+ # float32 boxes, float64 for float64 boxes), so nothing downstream loses
+ # precision; the returned mask still casts back to boxes.dtype as documented.
+ yy = torch.arange(height, device=boxes.device, dtype=torch.float32).view(height, 1)
+ xx = torch.arange(width, device=boxes.device, dtype=torch.float32).view(1, width)
x_min = boxes[:, 0, 0].view(-1, 1, 1)
y_min = boxes[:, 0, 1].view(-1, 1, 1)
x_max = boxes[:, 2, 0].view(-1, 1, 1)
Line-by-line: what changed and why
- −BEFORE: a normalised grid that is not exact at arbitrary sizes.
- +AFTER: an exact construction. Why: boundary pixels fell on the wrong side of the mask at some image sizes.
Risk rubric (likelihood × impact)
Exposure2
Ease3
Attacker gain2
Blast radius2
L1 MathematicsLow · 5.0/25
fix(geometry): bbox_to_mask3d intersects axis ranges for a full/overhanging-axis box
Class: geometry: box intersection
Threat model: bbox_to_mask3d mis-handles a full/overhanging-axis box, producing a wrong 3D mask.
bbox_to_mask3d did not intersect axis ranges correctly for a box spanning or overhanging an axis, giving a wrong mask. Intersects the ranges.
Before
y = torch.arange(D1, device=boxes.device, dtype=torch.long)
x = torch.arange(D2, device=boxes.device, dtype=torch.long)
# Compute mask as union of planes in one step
m = (
((z[None, :] >= z_min[:, None]) & (z[None, :] <= z_max[:, None]))[:, None, :, None, None]
| ((y[None, :] >= y_min[:, None]) & (y[None, :] <= y_max[:, None]))[:, None, None, :, None]
| ((x[None, :] >= x_min[:, None]) & (x[None, :] <= x_max[:, None]))[:, None, None, None, :]
).float() # Shape: (N, 1, D0, D1, D2)
# Compute conditions
cond1 = m.all(dim=3, keepdim=True).all(dim=2, keepdim=True)
cond2 = m.all(dim=4, keepdim=True).all(dim=2, keepdim=True)
cond3 = m.all(dim=3, keepdim=True).all(dim=4, keepdim=True)
m_out = cond1 * cond2 * cond3 # Broadcasting to (N, 1, D0, D1, D2)
return m_out.float()After
y = torch.arange(D1, device=boxes.device, dtype=torch.long)
x = torch.arange(D2, device=boxes.device, dtype=torch.long)
# Intersection of the three broadcast axis slabs, computed directly with `&`. This used to
# `|` the three slabs and recover the intersection with a three-way `all()` reduction/product;
# that recovery breaks the moment any one slab covers a whole axis, since the union is then
# all-true on that axis and the reduction can no longer see the other two slabs' bounds. `&`
# needs no such recovery step -- each broadcast slab already carries its own axis's bound at
# every position, so the elementwise intersection is exactly the answer.
m = (
((z[None, :] >= z_min[:, None]) & (z[None, :] <= z_max[:, None]))[:, None, :, None, None]
& ((y[None, :] >= y_min[:, None]) & (y[None, :] <= y_max[:, None]))[:, None, None, :, None]unified diff — kornia/geometry/bbox.py
y = torch.arange(D1, device=boxes.device, dtype=torch.long)
x = torch.arange(D2, device=boxes.device, dtype=torch.long)
- # Compute mask as union of planes in one step
+ # Intersection of the three broadcast axis slabs, computed directly with `&`. This used to
+ # `|` the three slabs and recover the intersection with a three-way `all()` reduction/product;
+ # that recovery breaks the moment any one slab covers a whole axis, since the union is then
+ # all-true on that axis and the reduction can no longer see the other two slabs' bounds. `&`
+ # needs no such recovery step -- each broadcast slab already carries its own axis's bound at
+ # every position, so the elementwise intersection is exactly the answer.
m = (
((z[None, :] >= z_min[:, None]) & (z[None, :] <= z_max[:, None]))[:, None, :, None, None]
- | ((y[None, :] >= y_min[:, None]) & (y[None, :] <= y_max[:, None]))[:, None, None, :, None]
- | ((x[None, :] >= x_min[:, None]) & (x[None, :] <= x_max[:, None]))[:, None, None, None, :]
- ).float() # Shape: (N, 1, D0, D1, D2)
-
- # Compute conditions
- cond1 = m.all(dim=3, keepdim=True).all(dim=2, keepdim=True)
- cond2 = m.all(dim=4, keepdim=True).all(dim=2, keepdim=True)
- cond3 = m.all(dim=3, keepdim=True).all(dim=4, keepdim=True)
-
- m_out = cond1 * cond2 * cond3 # Broadcasting to (N, 1, D0, D1, D2)
- return m_out.float()
+ & ((y[None, :] >= y_min[:, None]) & (y[None, :] <= y_max[:, None]))[:, None, None, :, None]
Line-by-line: what changed and why
- −BEFORE: range test that mishandles a box spanning or overhanging an axis.
- +AFTER: intersect the axis ranges properly. Why: a full-axis or overhanging box produced a wrong 3D mask.
Risk rubric (likelihood × impact)
Exposure2
Ease3
Attacker gain2
Blast radius2
L1 MathematicsLow · 5.0/25
fix(geometry): finite gradients for solve_cubic at its acos boundary
Class: non-finite gradient at singular point
Threat model: solve_cubic's acos boundary yields NaN/Inf gradients, poisoning any training that backprops through it.
solve_cubic produced non-finite gradients at its acos boundary. The double-where guard keeps the backward pass finite at the singular point. Forward looks fine, gradients are NaN.
Before
mask_D_zero_solutions = (a_D_zero <= 0) & (a_Q_zero != 0)
if torch.any(mask_D_zero):
theta_D_zero = torch.acos(R[mask_D_zero] / torch.sqrt(-Q3[mask_D_zero]))
sqrt_Q_D_zero = torch.sqrt(-Q[mask_D_zero])
x0_D_zero = 2 * sqrt_Q_D_zero * torch.cos(theta_D_zero / 3.0) - b_a_3[mask_D_zero]
x1_D_zero = 2 * sqrt_Q_D_zero * torch.cos((theta_D_zero + 2 * _PI) / 3.0) - b_a_3[mask_D_zero]After
mask_D_zero_solutions = (a_D_zero <= 0) & (a_Q_zero != 0)
if torch.any(mask_D_zero):
# d(acos)/dx = -1/sqrt(1-x^2) is unbounded at x = +-1. The branch condition (D <= 0)
# guarantees |ratio_D_zero| <= 1 (D = Q3 + R^2 <= 0 implies R^2 <= -Q3), but a repeated
# or near-repeated real root pushes the ratio to exactly that boundary, where the
# *value* is fine but the *derivative* diverges -- same shape as the acos/asin boundary
# in quaternion_exp_to_log/euler_from_quaternion (#4007, fixed in #4228). A plain
# `.clamp(-1, 1)` does not help here: it only guards the value, not the diverging
# derivative of a value already inside the domain. Route the boundary through `.acos()`
# on a detached copy for the value and through `.acos()` on a substituted safe argument
# for the gradient, so autograd never differentiates `acos` at +-1 at all.
ratio_D_zero = R[mask_D_zero] / torch.sqrt(-Q3[mask_D_zero])
ratio_D_zero = torch.clamp(ratio_D_zero, min=-1.0, max=1.0)
at_boundary_D_zero = ratio_D_zero.abs() >= 1.0
safe_ratio_D_zero = torch.where(at_boundary_D_zero, torch.zeros_like(ratio_D_zero), ratio_D_zero)
theta_D_zero = torch.where(at_boundary_D_zero, ratio_D_zero.detach().acos(), safe_ratio_D_zero.acos())
sqrt_Q_D_zero = torch.sqrt(-Q[mask_D_zero])
x0_D_zero = 2 * sqrt_Q_D_zero * torch.cos(theta_D_zero / 3.0) - b_a_3[mask_D_zero]
x1_D_zero = 2 * sqrt_Q_D_zero * torch.cos((theta_D_zero + 2 * _PI) / 3.0) - b_a_3[mask_D_zero]unified diff — kornia/geometry/solvers/polynomial_solver.py
mask_D_zero_solutions = (a_D_zero <= 0) & (a_Q_zero != 0)
if torch.any(mask_D_zero):
- theta_D_zero = torch.acos(R[mask_D_zero] / torch.sqrt(-Q3[mask_D_zero]))
+ # d(acos)/dx = -1/sqrt(1-x^2) is unbounded at x = +-1. The branch condition (D <= 0)
+ # guarantees |ratio_D_zero| <= 1 (D = Q3 + R^2 <= 0 implies R^2 <= -Q3), but a repeated
+ # or near-repeated real root pushes the ratio to exactly that boundary, where the
+ # *value* is fine but the *derivative* diverges -- same shape as the acos/asin boundary
+ # in quaternion_exp_to_log/euler_from_quaternion (#4007, fixed in #4228). A plain
+ # `.clamp(-1, 1)` does not help here: it only guards the value, not the diverging
+ # derivative of a value already inside the domain. Route the boundary through `.acos()`
+ # on a detached copy for the value and through `.acos()` on a substituted safe argument
+ # for the gradient, so autograd never differentiates `acos` at +-1 at all.
+ ratio_D_zero = R[mask_D_zero] / torch.sqrt(-Q3[mask_D_zero])
+ ratio_D_zero = torch.clamp(ratio_D_zero, min=-1.0, max=1.0)
+ at_boundary_D_zero = ratio_D_zero.abs() >= 1.0
+ safe_ratio_D_zero = torch.where(at_boundary_D_zero, torch.zeros_like(ratio_D_zero), ratio_D_zero)
+ theta_D_zero = torch.where(at_boundary_D_zero, ratio_D_zero.detach().acos(), safe_ratio_D_zero.acos())
sqrt_Q_D_zero = torch.sqrt(-Q[mask_D_zero])
x0_D_zero = 2 * sqrt_Q_D_zero * torch.cos(theta_D_zero / 3.0) - b_a_3[mask_D_zero]
x1_D_zero = 2 * sqrt_Q_D_zero * torch.cos((theta_D_zero + 2 * _PI) / 3.0) - b_a_3[mask_D_zero]
Line-by-line: what changed and why
- −BEFORE: `acos` is evaluated directly. Its derivative is 1/sqrt(1-x^2), which blows up at x = +-1 — exactly the boundary this hits.
- +Clamp the argument away from the singular points for the value...
- +...and use the double-`where` pattern so the *gradient* is finite too. Why: forward output looked correct but backprop produced NaN/Inf, silently killing any training that passed through solve_cubic.
Risk rubric (likelihood × impact)
Exposure2
Ease3
Attacker gain2
Blast radius2
Layer 5 — Training 4 merged
L5 TrainingMedium · 6.0/25
Fix MSTDPLearner initialising eligibility without a dtype
Class: dtype loss (learner state)
Threat model: An STDP learner initialises eligibility traces without a dtype, mismatching the network precision.
MSTDPLearner created its eligibility trace without a dtype, defaulting to float32 under a non-default-dtype network. Initialises with the right dtype.
Before
* **中文**
使用外部奖励 ``reward`` 对当前 eligibility 进行调制,并生成一次 mSTDP 权重更新。
若 ``reward`` 为张量,则在计算前将其从
autograd 计算图中分离,
因此写入的 ``weight.grad`` 或返回的权重增量不会连接到
``reward`` 的计算图。
:param reward: 每个样本对应的奖励,通常 ``shape = [batch_size]``。
张量奖励会在计算前分离
:type reward: torch.Tensor
:param on_grad: 是否将结果写入 ``self.synapse.weight.grad``
:type on_grad: boolAfter
* **中文**
使用外部奖励 ``reward`` 对当前 eligibility 进行调制,并生成一次 mSTDP 权重更新。
若 ``reward`` 为张量,则在计算前将其从 autograd 计算图中分离,
并转换到 ``self.synapse.weight`` 的 device 和 dtype,
因此写入的 ``weight.grad`` 或返回的权重增量不会连接到
``reward`` 的计算图。
:param reward: 每个样本对应的奖励,通常 ``shape = [batch_size]``。
张量奖励会在计算前分离,并转换到突触权重的 device 和 dtype
:type reward: torch.Tensor
:param on_grad: 是否将结果写入 ``self.synapse.weight.grad``
:type on_grad: boolunified diff — spikingjelly/activation_based/learning.py
* **中文**
使用外部奖励 ``reward`` 对当前 eligibility 进行调制,并生成一次 mSTDP 权重更新。
- 若 ``reward`` 为张量,则在计算前将其从
- autograd 计算图中分离,
+ 若 ``reward`` 为张量,则在计算前将其从 autograd 计算图中分离,
+ 并转换到 ``self.synapse.weight`` 的 device 和 dtype,
因此写入的 ``weight.grad`` 或返回的权重增量不会连接到
``reward`` 的计算图。
:param reward: 每个样本对应的奖励,通常 ``shape = [batch_size]``。
- 张量奖励会在计算前分离
+ 张量奖励会在计算前分离,并转换到突触权重的 device 和 dtype
:type reward: torch.Tensor
:param on_grad: 是否将结果写入 ``self.synapse.weight.grad``
:type on_grad: bool
Line-by-line: what changed and why
- −BEFORE: the eligibility trace defaulted to float32.
- +AFTER: match the network's dtype. Why: under a non-default-dtype network the STDP learner's state mismatched the weights it updated.
Risk rubric (likelihood × impact)
Exposure2
Ease4
Attacker gain2
Blast radius2
Fix features_only backward with grad checkpointing for in-place stem activations
Class: in-place op breaks backward
Threat model: features_only + grad checkpointing + an in-place stem activation corrupts the backward pass.
With features_only and gradient checkpointing, an in-place stem activation broke the recomputed backward. Fixed so checkpointed backward is correct.
Before
if self.grad_checkpointing and not torch.jit.is_scripting():
# Skipping checkpoint of first module because need a gradient at input
# Skipping last because networks with in-place ops might fail w/ checkpointing enabled
# NOTE: first_or_last module could be static, but recalc in is_scripting guard to avoid jit issues
first_or_last_module = i == 0 or i == max(len(self) - 1, 0)
x = module(x) if first_or_last_module else checkpoint(module, x)
else:
x = module(x)
After
if self.grad_checkpointing and not torch.jit.is_scripting():
# Skipping checkpoint of first module because need a gradient at input
# Skipping last because networks with in-place ops might fail w/ checkpointing enabled
# Skipping in-place modules (e.g. a stem's ReLU(inplace=True)): checkpointing one on its own modifies
# the tensor saved for recompute and fails in backward, and there is nothing to save by checkpointing it
# NOTE: first_or_last module could be static, but recalc in is_scripting guard to avoid jit issues
first_or_last_module = i == 0 or i == max(len(self) - 1, 0)
skip = first_or_last_module or getattr(module, 'inplace', False) is True
x = module(x) if skip else checkpoint(module, x)
else:
x = module(x)
unified diff — timm/models/_features.py
if self.grad_checkpointing and not torch.jit.is_scripting():
# Skipping checkpoint of first module because need a gradient at input
# Skipping last because networks with in-place ops might fail w/ checkpointing enabled
+ # Skipping in-place modules (e.g. a stem's ReLU(inplace=True)): checkpointing one on its own modifies
+ # the tensor saved for recompute and fails in backward, and there is nothing to save by checkpointing it
# NOTE: first_or_last module could be static, but recalc in is_scripting guard to avoid jit issues
first_or_last_module = i == 0 or i == max(len(self) - 1, 0)
- x = module(x) if first_or_last_module else checkpoint(module, x)
+ skip = first_or_last_module or getattr(module, 'inplace', False) is True
+ x = module(x) if skip else checkpoint(module, x)
else:
x = module(x)
Line-by-line: what changed and why
- −BEFORE: in-place stem activation. Under gradient checkpointing the forward is re-run in backward; an in-place op overwrites a tensor the recompute needs.
- +AFTER: use the non-in-place form on the features_only + checkpointing path. Why: so the recomputed backward sees the same values as the forward, giving correct gradients.
Risk rubric (likelihood × impact)
Exposure2
Ease3
Attacker gain2
Blast radius2
[Quantization] Flag layers narrower than group_size in the early group-size check
Class: validation gap (quantization)
Threat model: Layers narrower than group_size pass the early check, then quantize wrongly later.
The early group-size check did not flag layers narrower than group_size, so they slipped through to be quantized incorrectly. Flags them up front.
Before
Only considers layers whose weight scheme is GROUP or TENSOR_GROUP (enum).
BLOCK and other strategies are not checked.
Matches the condition
that triggers ValueError in compressed_tensors forward.py (columns >=
group_size and columns % group_size != 0).
:param model: Model with quantization schemes already applied (e.g. after
apply_quantization_config).After
Only considers layers whose weight scheme is GROUP or TENSOR_GROUP (enum).
BLOCK and other strategies are not checked.
Covers every layer that compressed-tensors cannot split into groups
(columns % group_size != 0), including layers narrower than one group,
which otherwise fail later with an opaque reshape error.
:param model: Model with quantization schemes already applied (e.g. after
apply_quantization_config).unified diff — src/llmcompressor/modifiers/quantization/group_size_validation.py
Only considers layers whose weight scheme is GROUP or TENSOR_GROUP (enum).
BLOCK and other strategies are not checked.
- Matches the condition
- that triggers ValueError in compressed_tensors forward.py (columns >=
- group_size and columns % group_size != 0).
+ Covers every layer that compressed-tensors cannot split into groups
+ (columns % group_size != 0), including layers narrower than one group,
+ which otherwise fail later with an opaque reshape error.
:param model: Model with quantization schemes already applied (e.g. after
apply_quantization_config).
Line-by-line: what changed and why
- ·The early check only caught non-divisible widths...
- +FIX: also flag layers *narrower* than the group. Why: a too-narrow layer passed the early check and was quantised incorrectly downstream with no warning.
Risk rubric (likelihood × impact)
Exposure2
Ease3
Attacker gain2
Blast radius2
fix(feature): CoarseMatching training-mode gt-padding crash + dtype leak
Class: training-mode crash + dtype leak
Threat model: CoarseMatching's training-mode ground-truth padding crashes, and leaks a dtype.
CoarseMatching crashed on its training-mode gt-padding path and leaked a dtype. Fixed both.
Before
(max(num_matches_train - num_matches_pred, self.train_pad_num_gt_min),),
device=_device,
)
mconf_gt = torch.zeros(len(data["spv_b_ids"]), device=_device) # set conf of gt paddings to all zero
b_ids, i_ids, j_ids, mconf = ( # type: ignore
torch.cat([x[pred_indices], y[gt_pad_indices]], dim=0)
for x, y in zip(
[b_ids, data["spv_b_ids"]],
[i_ids, data["spv_i_ids"]],
[j_ids, data["spv_j_ids"]],
[mconf, mconf_gt],
)
)
After
(max(num_matches_train - num_matches_pred, self.train_pad_num_gt_min),),
device=_device,
)
# set conf of gt paddings to all zero, in mconf's own dtype -- mconf can be
# float16/bfloat16 under autocast, and defaulting this to float32 would
# silently upcast the padded mconf below via torch.cat's type promotion.
mconf_gt = torch.zeros(len(data["spv_b_ids"]), device=_device, dtype=mconf.dtype)
b_ids, i_ids, j_ids, mconf = ( # type: ignore
torch.cat([x[pred_indices], y[gt_pad_indices]], dim=0)
for x, y in zip(
[b_ids, i_ids, j_ids, mconf],
[data["spv_b_ids"], data["spv_i_ids"], data["spv_j_ids"], mconf_gt],
)
)
unified diff — kornia/feature/loftr/utils/coarse_matching.py
(max(num_matches_train - num_matches_pred, self.train_pad_num_gt_min),),
device=_device,
)
- mconf_gt = torch.zeros(len(data["spv_b_ids"]), device=_device) # set conf of gt paddings to all zero
+ # set conf of gt paddings to all zero, in mconf's own dtype -- mconf can be
+ # float16/bfloat16 under autocast, and defaulting this to float32 would
+ # silently upcast the padded mconf below via torch.cat's type promotion.
+ mconf_gt = torch.zeros(len(data["spv_b_ids"]), device=_device, dtype=mconf.dtype)
b_ids, i_ids, j_ids, mconf = ( # type: ignore
torch.cat([x[pred_indices], y[gt_pad_indices]], dim=0)
for x, y in zip(
- [b_ids, data["spv_b_ids"]],
- [i_ids, data["spv_i_ids"]],
- [j_ids, data["spv_j_ids"]],
- [mconf, mconf_gt],
+ [b_ids, i_ids, j_ids, mconf],
+ [data["spv_b_ids"], data["spv_i_ids"], data["spv_j_ids"], mconf_gt],
)
)
Line-by-line: what changed and why
- −BEFORE: the training-mode ground-truth padding path raised.
- +AFTER: guard the path and keep dtype. Why: fixed a crash and a dtype leak in CoarseMatching's training path.
Risk rubric (likelihood × impact)
Exposure2
Ease3
Attacker gain2
Blast radius2
Layer 3 — Data 3 merged
Preserve float32 in spectral features; add dtype= to frequency constructors
Class: dtype loss
Threat model: Spectral features silently promote float32 audio to float64, doubling memory and breaking dtype assumptions.
Spectral feature functions did not preserve float32; frequency constructors had no dtype=. Added dtype plumbing so audio pipelines stay in their chosen precision.
Before
array([ 0. , 1378.125, 2756.25 , 4134.375,
5512.5 , 6890.625, 8268.75 , 9646.875, 11025. ])
"""
# the return dtype was unnecessarily broad in the numpy<2.5 dtype stubs
return cast("_Array1D[np.float64]", np.fft.rfftfreq(n=n_fft, d=1.0 / sr))
def cqt_frequencies(
n_bins: int, *, fmin: float, bins_per_octave: int = 12, tuning: float = 0.0
) -> _Array1D[np.float64]:
"""Compute the center frequencies of Constant-Q bins.
After
array([ 0. , 1378.125, 2756.25 , 4134.375,
5512.5 , 6890.625, 8268.75 , 9646.875, 11025. ])
"""
# Construct the grid directly at the requested precision. This is
# equivalent to ``np.fft.rfftfreq(n=n_fft, d=1.0 / sr)`` for float64
# (bit-for-bit), but avoids a float64 allocation + copy when a narrower
# dtype is requested.
return np.arange(0, 1 + n_fft // 2, dtype=dtype) * (sr / n_fft)
def cqt_frequencies(
n_bins: int,
*,
fmin: float,
bins_per_octave: int = 12,
tuning: float = 0.0,
dtype: npt.DTypeLike = np.float64,
) -> np.ndarray:
"""Compute the center frequencies of Constant-Q bins.
unified diff — librosa/core/convert.py
array([ 0. , 1378.125, 2756.25 , 4134.375,
5512.5 , 6890.625, 8268.75 , 9646.875, 11025. ])
"""
- # the return dtype was unnecessarily broad in the numpy<2.5 dtype stubs
- return cast("_Array1D[np.float64]", np.fft.rfftfreq(n=n_fft, d=1.0 / sr))
+ # Construct the grid directly at the requested precision. This is
+ # equivalent to ``np.fft.rfftfreq(n=n_fft, d=1.0 / sr)`` for float64
+ # (bit-for-bit), but avoids a float64 allocation + copy when a narrower
+ # dtype is requested.
+ return np.arange(0, 1 + n_fft // 2, dtype=dtype) * (sr / n_fft)
def cqt_frequencies(
- n_bins: int, *, fmin: float, bins_per_octave: int = 12, tuning: float = 0.0
-) -> _Array1D[np.float64]:
+ n_bins: int,
+ *,
+ fmin: float,
+ bins_per_octave: int = 12,
+ tuning: float = 0.0,
+ dtype: npt.DTypeLike = np.float64,
+) -> np.ndarray:
"""Compute the center frequencies of Constant-Q bins.
Line-by-line: what changed and why
- −BEFORE: frequency constructors built arrays at the default float64.
- +AFTER: thread a `dtype=` through. Why: float32 audio was silently promoted to float64 in spectral features — double the memory and broken downstream dtype assumptions.
Risk rubric (likelihood × impact)
Exposure2
Ease4
Attacker gain2
Blast radius2
fix(augmentation): auto-augment ops preserve float16 / bfloat16 input dtype
Class: dtype loss (augmentation)
Threat model: Auto-augment ops silently upcast fp16/bf16 inputs during the data pipeline.
Auto-augment operations did not preserve float16/bfloat16, upcasting in the data pipeline. Preserves input dtype.
Before
call builtins.AssertionError tests/augmentation/test_augmentation_mix.py::TestRandomMixUpV2::test_random_mixup_p1[cpu-float16]
call builtins.AssertionError tests/augmentation/test_augmentation_mix.py::TestRandomMixUpV2::test_random_mixup_same_on_batch[cpu-float16]
call builtins.AssertionError tests/augmentation/test_augmentation_mix.py::TestRandomMosaic::test_numerical[cpu-float16]
call builtins.RuntimeError tests/augmentation/test_auto_operation.py::TestAutoAugment::test_reproduce[cpu-float16]
call builtins.RuntimeError tests/augmentation/test_auto_operation.py::TestAutoAugment::test_sequential[cpu-float16]
call builtins.RuntimeError tests/augmentation/test_auto_operation.py::TestAutoAugment::test_smoke[cpu-float16-cifar10]
call builtins.RuntimeError tests/augmentation/test_auto_operation.py::TestAutoAugment::test_smoke[cpu-float16-imagenet]
call builtins.RuntimeError tests/augmentation/test_auto_operation.py::TestAutoAugment::test_smoke[cpu-float16-policy3]
call builtins.RuntimeError tests/augmentation/test_auto_operation.py::TestAutoAugment::test_smoke[cpu-float16-svhn]
call builtins.RuntimeError tests/augmentation/test_auto_operation.py::TestAutoAugment::test_transform_mat[cpu-float16]
call builtins.RuntimeError tests/augmentation/test_auto_operation.py::TestRandAugment::test_transform_mat[cpu-float16]
call builtins.RuntimeError tests/augmentation/test_auto_operation.py::TestRandAugment::test_transform_mat_repeated_policy[cpu-float16]
call builtins.AssertionError tests/augmentation/test_backward_3d.py::TestRandomMotionBlur3DBackward::test_param[cpu-float16-False-bilinear-circular-direction0-angle1]
call builtins.AssertionError tests/augmentation/test_backward_3d.py::TestRandomMotionBlur3DBackward::test_param[cpu-float16-False-bilinear-circular-direction0-angle2]
call builtins.AssertionError tests/augmentation/test_backward_3d.py::TestRandomMotionBlur3DBackward::test_param[cpu-float16-False-bilinear-circular-direction1-angle1]
After
call builtins.AssertionError tests/augmentation/test_augmentation_mix.py::TestRandomMixUpV2::test_random_mixup_p1[cpu-float16]
call builtins.AssertionError tests/augmentation/test_augmentation_mix.py::TestRandomMixUpV2::test_random_mixup_same_on_batch[cpu-float16]
call builtins.AssertionError tests/augmentation/test_augmentation_mix.py::TestRandomMosaic::test_numerical[cpu-float16]
call builtins.AssertionError tests/augmentation/test_auto_operation.py::TestAutoAugment::test_reproduce[cpu-float16]
call builtins.AssertionError tests/augmentation/test_backward_3d.py::TestRandomMotionBlur3DBackward::test_param[cpu-float16-False-bilinear-circular-direction0-angle1]
call builtins.AssertionError tests/augmentation/test_backward_3d.py::TestRandomMotionBlur3DBackward::test_param[cpu-float16-False-bilinear-circular-direction0-angle2]
call builtins.AssertionError tests/augmentation/test_backward_3d.py::TestRandomMotionBlur3DBackward::test_param[cpu-float16-False-bilinear-circular-direction1-angle1]
unified diff — testing/half_precision_xfails/cpu_float16.txt
call builtins.AssertionError tests/augmentation/test_augmentation_mix.py::TestRandomMixUpV2::test_random_mixup_p1[cpu-float16]
call builtins.AssertionError tests/augmentation/test_augmentation_mix.py::TestRandomMixUpV2::test_random_mixup_same_on_batch[cpu-float16]
call builtins.AssertionError tests/augmentation/test_augmentation_mix.py::TestRandomMosaic::test_numerical[cpu-float16]
-call builtins.RuntimeError tests/augmentation/test_auto_operation.py::TestAutoAugment::test_reproduce[cpu-float16]
-call builtins.RuntimeError tests/augmentation/test_auto_operation.py::TestAutoAugment::test_sequential[cpu-float16]
-call builtins.RuntimeError tests/augmentation/test_auto_operation.py::TestAutoAugment::test_smoke[cpu-float16-cifar10]
-call builtins.RuntimeError tests/augmentation/test_auto_operation.py::TestAutoAugment::test_smoke[cpu-float16-imagenet]
-call builtins.RuntimeError tests/augmentation/test_auto_operation.py::TestAutoAugment::test_smoke[cpu-float16-policy3]
-call builtins.RuntimeError tests/augmentation/test_auto_operation.py::TestAutoAugment::test_smoke[cpu-float16-svhn]
-call builtins.RuntimeError tests/augmentation/test_auto_operation.py::TestAutoAugment::test_transform_mat[cpu-float16]
-call builtins.RuntimeError tests/augmentation/test_auto_operation.py::TestRandAugment::test_transform_mat[cpu-float16]
-call builtins.RuntimeError tests/augmentation/test_auto_operation.py::TestRandAugment::test_transform_mat_repeated_policy[cpu-float16]
+call builtins.AssertionError tests/augmentation/test_auto_operation.py::TestAutoAugment::test_reproduce[cpu-float16]
call builtins.AssertionError tests/augmentation/test_backward_3d.py::TestRandomMotionBlur3DBackward::test_param[cpu-float16-False-bilinear-circular-direction0-angle1]
call builtins.AssertionError tests/augmentation/test_backward_3d.py::TestRandomMotionBlur3DBackward::test_param[cpu-float16-False-bilinear-circular-direction0-angle2]
call builtins.AssertionError tests/augmentation/test_backward_3d.py::TestRandomMotionBlur3DBackward::test_param[cpu-float16-False-bilinear-circular-direction1-angle1]
Line-by-line: what changed and why
- −BEFORE: auto-augment ops upcast to float32.
- +AFTER: keep fp16/bf16. Why: the data-augmentation pipeline silently changed precision mid-stream.
Risk rubric (likelihood × impact)
Exposure2
Ease4
Attacker gain2
Blast radius2
Fix background_color handling across DeepSeek-VL and Janus image processors
Class: preprocessing inconsistency
Threat model: Inconsistent background_color handling across DeepSeek-VL and Janus image processors skews pixel inputs.
The background_color padding value was handled inconsistently across the DeepSeek-VL and Janus multimodal image processors. Unified the handling.
Before
model_input_names = ["pixel_values", "high_res_pixel_values"]
def __init__(self, **kwargs: Unpack[DeepseekVLHybridImageProcessorKwargs]):
if kwargs.get("image_mean") is None:
background_color = (127, 127, 127)
else:
background_color = tuple(int(x * 255) for x in kwargs.get("image_mean"))
if kwargs.get("high_res_image_mean") is None:
high_res_background_color = (127, 127, 127)
else:
high_res_background_color = tuple(int(x * 255) for x in kwargs.get("high_res_image_mean"))
super().__init__(**kwargs)
self.background_color = tuple(background_color)
self.high_res_background_color = tuple(high_res_background_color)
After
model_input_names = ["pixel_values", "high_res_pixel_values"]
def __init__(self, **kwargs: Unpack[DeepseekVLHybridImageProcessorKwargs]):
super().__init__(**kwargs)
image_mean = getattr(self, "image_mean", None)
if image_mean is None:
self.background_color = (127, 127, 127)
else:
self.background_color = tuple(int(x * 255) for x in image_mean)
high_res_image_mean = getattr(self, "high_res_image_mean", None)
if high_res_image_mean is None:
self.high_res_background_color = (127, 127, 127)
else:
self.high_res_background_color = tuple(int(x * 255) for x in high_res_image_mean)
unified diff — src/transformers/models/deepseek_vl_hybrid/image_processing_deepseek_vl_hybrid.py
model_input_names = ["pixel_values", "high_res_pixel_values"]
def __init__(self, **kwargs: Unpack[DeepseekVLHybridImageProcessorKwargs]):
- if kwargs.get("image_mean") is None:
- background_color = (127, 127, 127)
+ super().__init__(**kwargs)
+ image_mean = getattr(self, "image_mean", None)
+ if image_mean is None:
+ self.background_color = (127, 127, 127)
else:
- background_color = tuple(int(x * 255) for x in kwargs.get("image_mean"))
- if kwargs.get("high_res_image_mean") is None:
- high_res_background_color = (127, 127, 127)
+ self.background_color = tuple(int(x * 255) for x in image_mean)
+ high_res_image_mean = getattr(self, "high_res_image_mean", None)
+ if high_res_image_mean is None:
+ self.high_res_background_color = (127, 127, 127)
else:
- high_res_background_color = tuple(int(x * 255) for x in kwargs.get("high_res_image_mean"))
- super().__init__(**kwargs)
- self.background_color = tuple(background_color)
- self.high_res_background_color = tuple(high_res_background_color)
+ self.high_res_background_color = tuple(int(x * 255) for x in high_res_image_mean)
Line-by-line: what changed and why
- −BEFORE: each processor derived the pad colour differently.
- +AFTER: one shared derivation across DeepSeek-VL and Janus. Why: padded regions differed between the two multimodal processors, skewing pixel inputs.
Risk rubric (likelihood × impact)
Exposure2
Ease3
Attacker gain2
Blast radius2
Layer 9 — Inference/serving 2 merged
L9 Inference/servingLow · 5.0/25
Export unquantized FP8Linear layers in the export dtype, not fp32
Class: export dtype
Threat model: Unquantized FP8 layers export as fp32, bloating and mis-typing the exported model.
Unquantized FP8Linear layers exported in fp32 rather than the export dtype, changing the served model. Export in the intended dtype.
Before
if ctx.is_modelopt_qlora and hasattr(sub_module, "base_layer"):
return
# Restore unpacked weight so the export path can read the live quantizer state.
if hasattr(sub_module, "weight_packed") or (
"QuantFP8Linear" in type(sub_module).__name__ and sub_module.weight.element_size() <= 1
):
sub_module.unpack_weight()
handler = ExportModuleRegistry.match(sub_module)
if handler is not None:
handler(name, sub_module, ctx)After
if ctx.is_modelopt_qlora and hasattr(sub_module, "base_layer"):
return
# Restore unpacked weight so the export path can read the live quantizer state.
if hasattr(sub_module, "weight_packed"):
sub_module.unpack_weight()
elif "QuantFP8Linear" in type(sub_module).__name__ and sub_module.weight.element_size() <= 1:
sub_module.unpack_weight()
# unpack_weight() dequantizes to torch's default dtype (fp32). A layer the recipe leaves
# unquantized is written as-is, so cast it to the export dtype like the rest of the model.
if get_quantization_format(sub_module) == QUANTIZATION_NONE:
sub_module.weight = nn.Parameter(sub_module.weight.to(ctx.dtype), requires_grad=False)
handler = ExportModuleRegistry.match(sub_module)
if handler is not None:
handler(name, sub_module, ctx)unified diff — modelopt/torch/export/unified_export_hf.py
if ctx.is_modelopt_qlora and hasattr(sub_module, "base_layer"):
return
# Restore unpacked weight so the export path can read the live quantizer state.
- if hasattr(sub_module, "weight_packed") or (
- "QuantFP8Linear" in type(sub_module).__name__ and sub_module.weight.element_size() <= 1
- ):
+ if hasattr(sub_module, "weight_packed"):
+ sub_module.unpack_weight()
+ elif "QuantFP8Linear" in type(sub_module).__name__ and sub_module.weight.element_size() <= 1:
sub_module.unpack_weight()
+ # unpack_weight() dequantizes to torch's default dtype (fp32). A layer the recipe leaves
+ # unquantized is written as-is, so cast it to the export dtype like the rest of the model.
+ if get_quantization_format(sub_module) == QUANTIZATION_NONE:
+ sub_module.weight = nn.Parameter(sub_module.weight.to(ctx.dtype), requires_grad=False)
handler = ExportModuleRegistry.match(sub_module)
if handler is not None:
handler(name, sub_module, ctx)
Line-by-line: what changed and why
- −BEFORE: unquantized FP8 layers were exported in fp32.
- +AFTER: export in the intended dtype. Why: the served model changed — larger and mis-typed versus what was configured.
Risk rubric (likelihood × impact)
Exposure2
Ease3
Attacker gain2
Blast radius2
L9 Inference/servingLow · 5.0/25
Remove stale SAM3 decoder coordinate caches
Class: stale cache (correctness)
Threat model: A decoder kept stale coordinate caches across calls, so a later inference used a previous input's coordinates.
The SAM3 decoder kept stale coordinate caches between calls. Removed them so each inference uses its own coordinates — a state-leak-across-calls bug (net -60 lines).
Before
for layer_idx, decoder_layer in enumerate(self.layers):
decoder_layer.layer_idx = layer_idx
@staticmethod
def _get_coords(H, W, device, dtype):
"""Get normalized coordinates for height and width."""
coords_h = torch.arange(0, H, dtype=dtype, device=device) / H
coords_w = torch.arange(0, W, dtype=dtype, device=device) / W
return coords_h, coords_w
def _get_rpb_matrix(self, reference_boxes, feat_size):
"""Get the relative position bias (RPB) matrix for box-relative position bias."""
H, W = feat_size
boxes_xyxy = xywh2xyxy(reference_boxes).transpose(0, 1)
bs, num_queries, _ = boxes_xyxy.shape
if self.compilable_cord_cache is None:
self.compilable_cord_cache = self._get_coords(H, W, reference_boxes.device, reference_boxes.dtype)
self.compilable_stored_size = (H, W)
if torch.compiler.is_dynamo_compiling() or self.compilable_stored_size == (
H,
W,
):
# good, hitting the cache, will be compilableAfter
for layer_idx, decoder_layer in enumerate(self.layers):
decoder_layer.layer_idx = layer_idx
def _get_rpb_matrix(self, reference_boxes, feat_size):
"""Get the relative position bias (RPB) matrix for box-relative position bias."""
H, W = feat_size
boxes_xyxy = xywh2xyxy(reference_boxes).transpose(0, 1)
bs, num_queries, _ = boxes_xyxy.shapeunified diff — ultralytics/models/sam/sam3/decoder.py
for layer_idx, decoder_layer in enumerate(self.layers):
decoder_layer.layer_idx = layer_idx
- @staticmethod
- def _get_coords(H, W, device, dtype):
- """Get normalized coordinates for height and width."""
- coords_h = torch.arange(0, H, dtype=dtype, device=device) / H
- coords_w = torch.arange(0, W, dtype=dtype, device=device) / W
- return coords_h, coords_w
-
def _get_rpb_matrix(self, reference_boxes, feat_size):
"""Get the relative position bias (RPB) matrix for box-relative position bias."""
H, W = feat_size
boxes_xyxy = xywh2xyxy(reference_boxes).transpose(0, 1)
bs, num_queries, _ = boxes_xyxy.shape
- if self.compilable_cord_cache is None:
- self.compilable_cord_cache = self._get_coords(H, W, reference_boxes.device, reference_boxes.dtype)
- self.compilable_stored_size = (H, W)
-
- if torch.compiler.is_dynamo_compiling() or self.compilable_stored_size == (
- H,
- W,
- ):
- # good, hitting the cache, will be compilable
Line-by-line: what changed and why
- −BEFORE: the decoder stashed coordinates on the instance...
- −...and reused them on the next call.
- ⓘAFTER: the caches are removed entirely (net -60 lines) and coordinates are computed per call. Why: a second inference reused the first input's coordinates — a state-leak-across-calls correctness bug.
Risk rubric (likelihood × impact)
Exposure2
Ease3
Attacker gain2
Blast radius2