Skip to content

fix(lib): a malformed saved record or send argument no longer stops a driver, and errors keep their stack - #205

Open
derek-miller wants to merge 2 commits into
mainfrom
fix/fleet-audit-hardening
Open

derek-miller wants to merge 2 commits into
mainfrom
fix/fleet-audit-hardening

Conversation

@derek-miller

Copy link
Copy Markdown
Contributor

The fleet audit after Home Connect's arm and disarm outage found a few more places in the template where a malformed argument or saved record could stop a driver, or where an error reached Cloud Monitoring without its stack. None is hit in the field today. Each would otherwise reach every driver on its next copier update.

What I changed:

  • Send wrappers. A nil before the last argument given to SendToDevice, SendToProxy, SendToNetwork or SendUIRequest is refused with an error that names the method and the argument, before it reaches C4. It raises rather than logging and dropping the send, so it reaches Cloud Monitoring the way this outage did. Library code checks it, so a test that spies on C4:SendToDevice still catches it. A flag given as anything but a boolean reaches C4 as one, and only true turns it on.
  • Saved events, conditionals and bindings. Each restore and id scan skips a namespace that isn't a table and a record without its id. The event and binding restores warn about what they skipped. When the driver adds that key again, the event or conditional gets a new id, and a binding keeps the id it had. A binding record missing its type or class is skipped at restore and added whole on its next add.
  • Events. getOrAddEvent declares an event before saving it, so a declaration C4 refuses leaves no record for every restart to replay.
  • Cloud Monitoring. captureError splits a message that holds its own traceback, so a debug.traceback() string passed as the message is stored as a message and a traceback. The module header says that stored messages and tracebacks hold literal \n, \t and \r.
  • persist:defer. It rethrows an error with the stack where it was raised. It adds the stack only once, however many scopes the error passes through, and rethrows a non-string error as it is.

Undoing any one of these fails a named check in its test. The one exception is the string check before defer adds a stack, since debug.traceback returns a non-string error unchanged anyway.

… driver, and errors keep their stack

The fleet audit after the Home Connect arm/disarm outage found these in the template:
- A nil before the last argument given to a C4 send is refused with an error naming the method and the
  argument, and a flag given as something other than a boolean reaches C4 as one.
- Saved events, conditionals and bindings skip a namespace that is not a table and a record without its id;
  a restore says what it skipped, and adding that key again gives it an id, or keeps the one it had.
- An event is declared before it is saved, so a declaration C4 refuses leaves no record.
- captureError splits a message that holds its own traceback.
- persist:defer rethrows an error with the stack where it was raised.
- The diagnostics header says how stored messages and tracebacks are escaped.
svc-finitelabs[bot]
svc-finitelabs Bot previously approved these changes Nov 11, 2026

@svc-finitelabs svc-finitelabs Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. I checked the riskiest change, the send wrappers' new raise, against the dev controller. C4:SendToDevice(id, cmd, nil, false), C4:SendToProxy(b, cmd, nil, "NOTIFY") and C4:SendToNetwork(b, nil, data) already raise there ("tParams should be a table", "3rd parameter must be a string (strValue) or table (tParams)", "nPort should be a number"), so no call that works today now fails. I also rendered the default and diagnostics answer sets and reverted each change one at a time. Every revert failed a named check except the one in the inline nit.

Comment thread template/src/lib/bindings.lua Outdated
local binding = Select(bindings, namespace, key)
if binding == nil then
local bindingId = self:_getNextBindingId(type)
if not holds(binding, "bindingId", "type", "class") then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the "type" half of this guard has no test. With it reduced to holds(binding, "bindingId"), the suite still passes, because the only incomplete fixture (noClass in test_saved_entries_without_ids.lua:120) has its type and differs in class, so the rename branch re-adds it anyway. A saved record with its id and class but no type, added again with the same name, provider and class, would go to neither branch. It would stay typeless, and restoreBindings would skip it on every restart. A noType fixture added again unchanged would cover it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f118046. A noType fixture (id and class, no type) is added again unchanged and comes back whole at its own id, and the restore warns about it. Reducing the guard to the id alone now fails that check. Its mirror, dropping the class half, used to survive for the same reason: the rename branch re-added the record. That path isn't equivalent, though. It removes the binding and drops its connections when the class differs. So the test now seeds a connection Director still holds on the classless binding and checks that it's kept. Dropping the class half fails that.

Comment thread template/src/lib/bindings.lua Outdated
--- @param entry any
--- @param ... string The fields it must hold.
--- @return boolean
local function holds(entry, ...)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: bindings checks a saved entry with holds(), while eachEvent (events.lua:35) and eachConditional (conditionals.lua:34), and both add paths, use type(keys) == "table" and Select(x, "<id>") ~= nil for the same check. One idiom across the three modules would read better.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f118046. bindings.lua now uses the same idiom as events.lua and conditionals.lua: type(keys) == "table" for a namespace and Select(binding, "bindingId") ~= nil for a record, plus field checks once it's known to be a table. holds() is gone. I only added it because a parameter named type shadowed the global in getOrAddDynamicBinding and _getNextBindingId, so I renamed that parameter to bindingType in both.

…and a test for a binding saved without its type

@svc-finitelabs svc-finitelabs Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving f118046. Both guard halves in getOrAddDynamicBinding now have tests that catch them. With either the type or class check removed, its own new case fails (the noType record comes back without a type; the classless binding loses the connection Director held), and so does the restore's type/class skip. Select returns nil for a non-table, so the unified idiom is still safe for a string or number record, and no remaining parameter shadows type now that it's bindingType. I mutated each bindings.lua guard against test_saved_entries_without_ids.lua (all nine failed a named check, the unmutated control passed) and ran render-checks for default and diagnostics.

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