From cfb3fbbc0364a01645be113826afcdc1b542cd61 Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Sun, 18 Oct 2026 11:55:38 +0200 Subject: [PATCH] A pin whose verdict is refused refuses the add with PIN_REFUSED (-516) verdict=refused was parsed, stored and then ignored. A plugin whose files failed qualification was added like any other. pins_check now refuses such a plugin with the new code PHD_ERR_PIN_REFUSED (-516), after the files have been checked against the pin: the verdict applies only to the pinned files, so files that differ still answer -509 first. Because the add and the replay share pins_check, an instance whose pin turns refused while it runs is refused at its next replay (instance_replay_refused with -516). The code is declared in protocol.h, and the README and protocol/plugin-hostd.json are regenerated from it. The test fails on main (the add answers resp 0). It covers pin_set, a pin_file under first-use (the plugin is not learned into the store), the replay, and changed files. A sabotage arm removes the check. Closes #71 --- README.md | 6 ++++- include/plugin-hostd/protocol.h | 6 ++++- protocol/plugin-hostd.json | 3 ++- src/pins.c | 6 +++++ src/pins.h | 3 ++- tests/daemon_test.py | 45 +++++++++++++++++++++++++++++++++ tests/sabotage.py | 3 +++ 7 files changed, 68 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cd2386d..ef56cdb 100644 --- a/README.md +++ b/README.md @@ -237,6 +237,7 @@ Error codes the daemon adds to mod-host's: | `-513` | `PHD_ERR_NO_NOTE_INPUT` | the instance's plugin takes no notes (an effect): its worker answers note_on, note_off and all_notes_off with this code | | `-514` | `PHD_ERR_NOTES_BY_JACK` | the instance's worker takes no notes on its socket (mod-host): the daemon answers the note verbs and note_input with this code, and the plugin is played through its jack MIDI input with connect | | `-515` | `PHD_ERR_NO_PRESET_LIST` | the instance's worker cannot list the plugin's presets (mod-host): the daemon answers preset_list and preset_get with this code; preset_load still takes a preset the controller knows | +| `-516` | `PHD_ERR_PIN_REFUSED` | the plugin's pin carries verdict=refused: its files are the ones that failed qualification, so add is refused and no worker sees it, and a replayed add is refused the same way; files that differ from the pin answer (-509) first | A worker's own refusal is returned as it said it. An `add` that killed its worker answers mod-host's `-102`. @@ -362,7 +363,10 @@ files, keeps the pin in the pin store (`$XDG_STATE_HOME/plugin-hostd/pins`) and asked of the worker after the add (`pin_layout`) and recorded too. A host that pins by itself sets `pin_mode strict`. The store, a `pin_file` and `pin_set` carry one line format, `pin_set [verdict=]` with the verb optional in a file. In `first-use` only a pin with `verdict=in-process`, which plugin-qualify writes, lets a plugin -share a worker. A plugin that changed since it was pinned is refused (-509, or -510 from the worker), the +share a worker. A pin with `verdict=refused` names files that failed qualification. Their `add` answers `-516` +(`PIN_REFUSED`), no worker sees it, and a replay is refused with the same code. A pin that changes to `refused` while +the instance runs takes effect at its next replay. Files that differ from the pin answer `-509` first, because the +verdict was given for the pinned files only. A plugin that changed since it was pinned is refused (-509, or -510 from the worker), the `pin_changed files|layout` event names what changed, and `pin_accept ` takes the new pin. What cannot be pinned: diff --git a/include/plugin-hostd/protocol.h b/include/plugin-hostd/protocol.h index 33f2f37..cc0cd29 100644 --- a/include/plugin-hostd/protocol.h +++ b/include/plugin-hostd/protocol.h @@ -260,6 +260,7 @@ static inline int phd_pool_name_valid(const char *name) #define PHD_ERR_NO_NOTE_INPUT (-513) #define PHD_ERR_NOTES_BY_JACK (-514) #define PHD_ERR_NO_PRESET_LIST (-515) +#define PHD_ERR_PIN_REFUSED (-516) /* X(id, meaning) */ #define PHD_ERRORS(X) \ @@ -294,7 +295,10 @@ static inline int phd_pool_name_valid(const char *name) "with connect") \ X(NO_PRESET_LIST, "the instance's worker cannot list the plugin's presets (mod-host): the daemon answers " \ PHD_VERB_PRESET_LIST " and " PHD_VERB_PRESET_GET " with this code; preset_load still takes a preset " \ - "the controller knows") + "the controller knows") \ + X(PIN_REFUSED, "the plugin's pin carries " PHD_PIN_VERDICT_KEY PHD_PIN_VERDICT_REFUSED ": its files are the ones that " \ + "failed qualification, so add is refused and no worker sees it, and a replayed add is refused the " \ + "same way; files that differ from the pin answer " PHD_STR(PHD_ERR_PIN_BINARY_MISMATCH) " first") /* ---------------------------------------------------------------- verbs */ diff --git a/protocol/plugin-hostd.json b/protocol/plugin-hostd.json index fabe69b..0c6c096 100644 --- a/protocol/plugin-hostd.json +++ b/protocol/plugin-hostd.json @@ -38,7 +38,8 @@ {"name": "LV2_ADAPTER_REFUSED", "macro": "PHD_ERR_LV2_ADAPTER_REFUSED", "code": -512, "meaning": "the lv2-clap worker's LV2 adapter cannot run this plugin (MIDI it does not feed, a required feature it lacks, a layout it does not take): the worker answered the add -102, the lv2_adapter_refused event carries the hosting.* code of that reply, and under lv2_worker auto the plugin is run by the lv2 row instead and this code is never answered; a plugin no bundle of the worker's world holds is no refusal: the worker's -101 is answered"}, {"name": "NO_NOTE_INPUT", "macro": "PHD_ERR_NO_NOTE_INPUT", "code": -513, "meaning": "the instance's plugin takes no notes (an effect): its worker answers note_on, note_off and all_notes_off with this code"}, {"name": "NOTES_BY_JACK", "macro": "PHD_ERR_NOTES_BY_JACK", "code": -514, "meaning": "the instance's worker takes no notes on its socket (mod-host): the daemon answers the note verbs and note_input with this code, and the plugin is played through its jack MIDI input with connect"}, - {"name": "NO_PRESET_LIST", "macro": "PHD_ERR_NO_PRESET_LIST", "code": -515, "meaning": "the instance's worker cannot list the plugin's presets (mod-host): the daemon answers preset_list and preset_get with this code; preset_load still takes a preset the controller knows"} + {"name": "NO_PRESET_LIST", "macro": "PHD_ERR_NO_PRESET_LIST", "code": -515, "meaning": "the instance's worker cannot list the plugin's presets (mod-host): the daemon answers preset_list and preset_get with this code; preset_load still takes a preset the controller knows"}, + {"name": "PIN_REFUSED", "macro": "PHD_ERR_PIN_REFUSED", "code": -516, "meaning": "the plugin's pin carries verdict=refused: its files are the ones that failed qualification, so add is refused and no worker sees it, and a replayed add is refused the same way; files that differ from the pin answer (-509) first"} ], "verbs": [ {"name": "add", "arguments": " [own | pool: | default] [client_name]", "reply": "resp ", "meaning": "mod-host's add with a placement; a token after the instance that is not a placement is the jack client name"}, diff --git a/src/pins.c b/src/pins.c index d3484db..f1b3583 100644 --- a/src/pins.c +++ b/src/pins.c @@ -663,6 +663,12 @@ int pins_check(const format_t *format, const char *uri, const char *search, cons names_free(&files); for (n = 0; n < p->nfiles && rc == SUCCESS; n++) rc = hash_file(root, p->path[n], p->digest[n], uri); + /* the verdict is about these very files: only once they are the pinned ones does a refused one refuse the add */ + if (rc == SUCCESS && !strcmp(p->verdict, PHD_PIN_VERDICT_REFUSED)) + { + fprintf(stderr, "plugin-hostd: %s: its pin carries " PHD_PIN_VERDICT_KEY PHD_PIN_VERDICT_REFUSED "\n", uri); + rc = PHD_ERR_PIN_REFUSED; + } if (rc == SUCCESS) *layout = strcmp(p->layout, PHD_PIN_LAYOUT_UNKNOWN) ? p->layout : NULL; return rc; diff --git a/src/pins.h b/src/pins.h index 65027df..53ff752 100644 --- a/src/pins.h +++ b/src/pins.h @@ -58,7 +58,8 @@ int pins_set_layout(const char *uri, const char *layout); * loads for it (format->pin_collect, searched in `search`) all held by the pin, then every file the pin names hashed * over one open descriptor. SUCCESS, with *layout the * layout pin ":" (the table's, valid until the next pin verb) or NULL while the pin's layout is - * unknown, or PHD_ERR_PIN_ABSENT or PHD_ERR_PIN_BINARY_MISMATCH. */ + * unknown, or PHD_ERR_PIN_ABSENT or PHD_ERR_PIN_BINARY_MISMATCH, or PHD_ERR_PIN_REFUSED for the pinned files under a + * verdict of refused. */ int pins_check(const format_t *format, const char *uri, const char *search, const char **layout); /* a refusal of a pin, with the reason on stderr: PHD_ERR_PIN_BINARY_MISMATCH */ diff --git a/tests/daemon_test.py b/tests/daemon_test.py index 3c425dc..5ef7682 100644 --- a/tests/daemon_test.py +++ b/tests/daemon_test.py @@ -1464,6 +1464,51 @@ def a_layout_pinned_again_while_its_worker_was_down_is_refused_by_the_replayed_a p.close() +@test +def a_pin_whose_verdict_is_refused_refuses_the_add_with_its_own_code(): + """issue 71: verdict=refused names bytes that failed qualification; an add of them is refused by code, no worker + sees it, and a replay of an instance whose pin turned refused is refused the same way""" + p = Plugins() + d = p.daemon() + refused = harness.CODE.get("PIN_REFUSED") + try: + check(refused is not None, "the protocol declares PIN_REFUSED: %s" % sorted(harness.CODE)) + d.expect(p.clap_pin() + " verdict=refused", "resp 0") + r = d.send("add %s 0" % p.clap_uri) + check(r == "resp %d" % refused, "the add of a refused plugin answers PIN_REFUSED: %r" % r) + check(d.workers() == {} and p.received() == [], "no worker started, none was told: %s %s" % (d.workers(), p.received())) + # positive control: the same bytes qualified again run + d.expect(p.clap_pin() + " verdict=isolated", "resp 0") + d.expect("add %s 0" % p.clap_uri, "resp 0") + # a pin turned refused while the instance runs: its replay is refused with the same code + _, w = d.holder(0) + d.expect(p.clap_pin() + " verdict=refused", "resp 0") + mark = d.mark() + os.kill(w["pid"], signal.SIGKILL) + d.wait_event("instance_replay_refused 0 %d" % refused, since=mark) + check(not [e for e in d.events[mark:] if e.startswith("pin_changed")], "a refusal is no change of the files") + finally: + d.close() + # bytes that changed are a mismatch first: a verdict is about the bytes it was given for + d = p.daemon() + try: + d.expect(p.clap_pin().replace(sha(p.clap)[:8], "00000000") + " verdict=refused", "resp 0") + d.expect("add %s 1" % p.clap_uri, resp("PIN_BINARY_MISMATCH")) + finally: + d.close() + # a pin_file carries the verdict as pin_set does + pins = os.path.join(p.dir, "pins.txt") + write(pins, ("%s verdict=refused\n" % p.clap_pin().split(" ", 1)[1]).encode()) + d = p.daemon(conf={"pin_mode": "first-use", "pin_file": pins}) + try: + r = d.send("add %s 2" % p.clap_uri) + check(r == "resp %d" % refused, "a pin_file's refused verdict refuses the add: %r" % r) + check(store_lines(p) == [], "and the refused plugin is not learned into the store") + finally: + d.close() + p.close() + + def own_worker_of(d, inst): return d.holder(inst)[1]["place"] == "own" diff --git a/tests/sabotage.py b/tests/sabotage.py index 7b159c5..c8aaf88 100644 --- a/tests/sabotage.py +++ b/tests/sabotage.py @@ -273,6 +273,9 @@ " shutdown(s->fd, SHUT_RDWR);\n", "", "relay_test", "relay"), ("the relay thread polls a set it had no memory for", "src/relay.c", " if (cap == 0)\n", " if (0)\n", "relay_test", "relay"), + ("a pin whose verdict is refused admits the add", "src/pins.c", + " if (rc == SUCCESS && !strcmp(p->verdict, PHD_PIN_VERDICT_REFUSED))\n", " if (0)\n", + "a_pin_whose_verdict_is_refused_refuses_the_add_with_its_own_code"), ("the build steps are back on the README", "README.md", "Building from source: see [BUILDING.md](BUILDING.md).", "Building from source:\n\n make\n\nsee [BUILDING.md](BUILDING.md).", "README.md runs no build tool", "readme"), ("BUILDING.md lost its build command", "BUILDING.md", " make [MOD_HOST_DIR=]\n", "",