Skip to content

fix: correctness and security pass over attacks and defenses (0.2.0) - #3

Merged
johanbeytell1 merged 3 commits into
masterfrom
arena/b52ef87b-deepdefend
Oct 31, 2026
Merged

johanbeytell1 merged 3 commits into
masterfrom
arena/b52ef87b-deepdefend

Conversation

@johanbeytell1

@johanbeytell1 johanbeytell1 commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

Summary

A full review of the package found that 7 of 11 attacks and 4 of 15 defenses were broken, silently inert, or were not the algorithm they claimed to be — plus the package could not be installed from a checkout and had no tests at all. This PR fixes the correctness and security problems, removes a false defense entirely, and adds the missing guardrails.

All findings are now documented in the README under What was wrong before 0.2.0 and Limitations. (The standalone review document was folded into the README at the maintainer's request.)

What was broken (measured, not inferred)

Finding Evidence
jsma, spsa, pixel_deflection raised on every call wrong Jacobian rank, tensor passed as tf.Variable, maxval not scalar
ead returned the input unchanged with default args max|δ| = 0.0000 — a user would conclude their model is robust
cw's c and kappa had no effect bit-identical output for c=1,κ=0 vs c=1000,κ=50
deepfool returned invalid images range [-0.90, 2.08] for [0,1] input
fgsm returned unclipped output range [-0.010, 1.007]
all 7 gradient attacks rejected sparse labels the default Keras label format
median_smoothing was avg_pool2d; gaussian_blur ignored sigma bit-identical outputs
JPEG / TV / squeezing defenses broke the gradient fgsm(defended_model, ...) → ValueError: NoneType
13/13 wrapped defense models failed save()/load_model() layers defined inside function bodies
gradient_masking was a no-op at inference fgsm(base) == fgsm(masked) — removed
pip install . failed version was {{VERSION_PLACEHOLDER}}
publish workflow @master actions with a long-lived PyPI token, deprecated ::set_output, any tag publishes

What changed

Removed — gradient_masking. Deleted rather than deprecated. It only masked training gradients, never the gradients an attacker computes, so it had no measurable defensive effect. Gradient masking is a documented false defense (Athalye et al., 2018): it makes a model look robust while leaving it vulnerable. Stated in the README changelog, the support matrix, a dedicated Limitations bullet, CHANGELOG.md, and enforced by a regression test.

Attacks — jsma, spsa, ead, cw, deepfool rewritten as the algorithms they claim to be; fgsm clipped; all attacks now accept one-hot/sparse/binary labels, auto-detect from_logits, call the model with training=False, and take clip_min/clip_max (so normalised inputs are not clipped to [0,1]). Targeted attacks no longer freeze at step 0. Text helpers preserve whitespace and punctuation.

Defenses — all layers hoisted to module level with get_config() and Keras registration, so defended models save and reload; straight-through gradients for JPEG/squeezing/TV; real median filter and real Gaussian kernel; pixel_deflection fixed and seedable; adversarial_training batches attack generation and keeps the caller's optimiser/loss; distillation no longer double-applies softmax; ALP actually generates adversarial examples; smooth_predict() added for randomized smoothing with a certified radius.

Packaging / CI / docs — pyproject.toml replaces setup.py, bounded deps, requires-python >= 3.9; 170 tests + ruff + CI; publish workflow rewritten with SHA-pinned actions, a tag/version check, a test gate and PyPI Trusted Publishing; SECURITY.md, CODE_OF_CONDUCT.md, CHANGELOG.md, Dependabot, pre-commit.

Docs handover — the README now ends with a self-contained text code cell brief for the docs agent, covering every changed signature, the removal, migration notes and the tone/format rules.

Verification

$ pytest -q
170 passed

$ ruff check .
All checks passed!

$ python -m build && twine check dist/*
Successfully built deepdefend-0.2.0.tar.gz and deepdefend-0.2.0-py3-none-any.whl
Checking dist/...: PASSED

Breaking changes

The numerical output of cw, deepfool, ead, spsa and jsma changes (they were wrong before), several defaults changed, gradient_masking was removed, and image-only defenses now raise on non-image inputs instead of silently returning the input. Released as 0.2.0 — see CHANGELOG.md.

Not addressed here: the library is still TensorFlow/Keras only, and preprocessing defenses remain defeatable by adaptive attacks (now documented).

Arena Agent and others added 3 commits October 31, 2026 12:57
Several public functions either raised on every call or returned silently
wrong results. This makes each function do what its name claims, and adds the
guardrails (tests, lint, packaging, CI) that were missing.

Attacks
- jsma: was indexing a rank-4 Jacobian with 5 indices and mutating an eager
  tensor in place; rewritten as a real saliency-map attack for any input rank.
- spsa: passed a tensor (not a tf.Variable) to the optimizer and hard-coded
  rank-4 inputs; now a correct finite-difference estimate with Adam.
- ead: returned the input unchanged with default args (alpha == beta); now a
  proper ISTA/proximal step on the elastic-net objective.
- cw: the L2 term and kappa were inert (x was already clipped); now a real
  tanh-space C&W with a fixed target class.
- deepfool: ignored any budget and returned out-of-range values; now computes
  per-class boundaries and respects epsilon/clip range.
- fgsm: output is clipped to the valid range.
- all attacks: accept sparse/one-hot/binary labels, honour from_logits,
  call the model with training=False, support clip_min/clip_max, and no
  longer freeze targeted attacks at step 0.
- deepfool/jsma/spsa no longer project against the whole batch.
- word_swap/char_swap preserve whitespace, newlines and punctuation.

Defenses
- every preprocessing defense can now be saved/reloaded (layers were defined
  inside function bodies, so no defended model was serialisable).
- jpeg_compression, feature_squeezing, total_variation_minimization: restore
  the gradient via straight-through so attacks against a defended model work.
- median_smoothing is a real median filter (was avg_pool2d); gaussian_blur
  honours sigma (was avg_pool2d).
- pixel_deflection no longer raises; deterministic when seeded.
- adversarial_training batches attack generation and keeps the caller's
  optimizer/loss; defensive_distillation stops double-applying softmax and
  scales by T^2; ALP now generates adversarial examples; gradient_masking
  warns that it is not a security defense.
- randomized_smoothing clips and is seedable, plus smooth_predict() with a
  certified-radius estimate.
- invalid parameters raise instead of producing NaN; subclassed/multi-input
  models get a clear TypeError.

Packaging / CI / docs
- replace setup.py ({{VERSION_PLACEHOLDER}}) with pyproject.toml; pip install .
  works from a checkout again.
- bound dependencies, require Python >= 3.9.
- add 171 tests, ruff config and a CI workflow; rewrite the publish workflow
  with SHA-pinned actions, a tag/version check, a test gate and PyPI Trusted
  Publishing.
- add SECURITY.md, CODE_OF_CONDUCT.md, CHANGELOG.md, CODE_REVIEW.md,
  Dependabot and pre-commit; correct the README support matrix.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…brief

- Remove gradient_masking()/GradientMaskingModel entirely. It only masked
  training gradients and had no effect on an attacker's gradients (attacks
  against a wrapped model were bit-identical to the base model), so it is a
  false defense. Removed from the package namespace, __all__, the module
  docstring, the README support matrix and the tests; a regression test now
  asserts it stays gone.
- Delete CODE_REVIEW.md and move its findings into the README as a
  'What was wrong before 0.2.0' section (attacks, defenses, packaging/CI
  tables) and a Limitations section.
- Add a 'Documentation update brief' code cell to the README describing every
  0.2.0 API change for the docs agent.
- CHANGELOG: gradient_masking moves from Fixed to Removed with the rationale;
  test count updated to 170.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
@johanbeytell1
johanbeytell1 merged commit 2e597c5 into master Oct 31, 2026
5 checks passed
@johanbeytell1
johanbeytell1 deleted the arena/b52ef87b-deepdefend branch October 31, 2026 13:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant