Skip to content

fix: PATCH preserves uci options the resource does not model (PUT still replaces)#9

Merged
raspbeguy merged 1 commit into
mainfrom
fix/patch-preserve-unmodeled-uci-options
Jun 21, 2026
Merged

fix: PATCH preserves uci options the resource does not model (PUT still replaces)#9
raspbeguy merged 1 commit into
mainfrom
fix/patch-preserve-unmodeled-uci-options

Conversation

@raspbeguy

Copy link
Copy Markdown
Member

Summary

A full-resource audit of every curated resource against its stock OpenWrt config surfaced a systemic, pre-existing data-loss bug: both PUT and PATCH shared one diff_apply, which iterates the raw uci section and deletes every option not re-emitted by toUci. Because each resource models only a curated subset of its section's options, a partial PATCH silently wiped any hand-set or stock option the model omits.

The audit confirmed this on 13 resources, e.g.:

  • unbound.server: PATCH-ing verbosity wiped dns64_prefix, iface_trig, iface_wan, validator, edns_size, rate_limit, +11 more
  • firewall.rules: any PATCH wiped icmp_type (breaks Allow-Ping / Allow-ICMPv6-*) and limit
  • uhttpd.certs: wiped key_type/ec_curve (cert silently regenerates EC→RSA)
  • dhcp.dnsmasq: wiped localservice, rebind_localhost, localise_queries, ...
  • system: wiped ttylogin; firewall.defaults: wiped synflood_protect; openvpn: wiped the custom-config config path; etc.

The existing stock-config round-trip test could not catch this: its drift check compares fromUci output, which never surfaces unmodeled keys.

Change

Split the write semantics by verb (RFC-hybrid):

  • PUT (replace) keeps diff_apply (raw iterate → full normalize). PUT means "this body is the whole resource," so unmodeled options are intentionally dropped.
  • PATCH (partial) uses a new diff_apply_patch whose delete scope is the resource's modeled footprint (toUci(fromUci(existing))). It only deletes a modeled field the patch cleared; raw options outside the footprint (which toUci cannot emit) are never touched. Matches RFC 7396 merge-patch.

Applied to both the CRUD make().patch and singleton make_singleton().patch paths.

Tests

  • Unit (handler_test.uc): PATCH preserves an unmodeled option (icmp_type/limit); PATCH still deletes a modeled field it clears (footprint delete); PUT still normalizes away an unmodeled option.
  • Integration (44_stock_config_test.sh): set localservice on dnsmasq, PATCH a modeled field, assert it survives via uci get; set icmp_type on a firewall rule, PUT the body back, assert it's dropped. The drift check can't see this class, so these read uci directly.

Verification

  • make lint + make test (750 pass, +3) + make coverage (80.8% direct, 100% module) + make openapi-check all green.
  • Adversarial review across 7 angles (JSON-Patch path, dynamic-type resources, custom merge_for_patch, create_if_missing singletons, default-synthesizing fromUci, value-only PATCH, toUci throwing) found no regression and reproduced the pre-fix wipe.

Scope / follow-ups

  • The companion reject_422 audit (validation stricter than the platform) came back clean for the optional packages. Residual reject cases live only in wireless (stock empty country, encryption='owe' on 6 GHz) and network.devices (type-less macaddr-override sections) — both in test-excluded resources; tracked separately, not in this PR.
  • Pre-existing, out of scope: JSON-Patch on a section with a masked secret + conditional-required validation can 422 before the write path (the JSON-Patch post-image lacks the masked secret). Predates this change; worth a separate issue.

@raspbeguy raspbeguy force-pushed the fix/patch-preserve-unmodeled-uci-options branch from 5abcb38 to 78668df Compare June 20, 2026 22:41
@raspbeguy raspbeguy closed this Jun 21, 2026
@raspbeguy raspbeguy reopened this Jun 21, 2026
@raspbeguy raspbeguy force-pushed the fix/patch-preserve-unmodeled-uci-options branch from 78668df to c666b8f Compare June 21, 2026 05:34
@raspbeguy raspbeguy merged commit 73da671 into main Jun 21, 2026
5 checks passed
@raspbeguy raspbeguy deleted the fix/patch-preserve-unmodeled-uci-options branch June 21, 2026 05:43
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