diff --git a/.gitignore b/.gitignore index 59e2d6f..aa18795 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ tests/rt_interpose.so tests/gesture-alloc-test tests/lv2-alloc-test tests/lv2-alloc-lv2.o +tests/verbs-alloc-test tests/transforms-test /build/ fuzz/findings/ diff --git a/Makefile b/Makefile index 1532cd4..525dd3c 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ QUALCOST_TEST = tests/qualcost-test RELAY_TEST = tests/relay-test GESTURE_ALLOC_TEST = tests/gesture-alloc-test LV2_ALLOC_TEST = tests/lv2-alloc-test +VERBS_ALLOC_TEST = tests/verbs-alloc-test TRANSFORMS_TEST = tests/transforms-test PROBE = plugin-hostd-probe @@ -258,7 +259,7 @@ fuzz-corpus-check: # clean rule clean: - @rm -f src/*.o $(PROG) $(PROBE) tests/probe-child tests/lv2-probe-child tests/rt_interpose.so $(FAKE) $(PIN_TEST) $(HISTORY_TEST) $(PROC_TEST) $(PLANNER_TEST) $(QUALCOST_TEST) $(RELAY_TEST) tests/relay-test-relay.o $(FORMAT_TEST) $(GESTURE_ALLOC_TEST) $(LV2_ALLOC_TEST) tests/lv2-alloc-lv2.o $(TRANSFORMS_TEST) $(GEN) tests/stress.clap tests/jack_levels tests/host_scenarios $(LV2_FIXTURE) + @rm -f src/*.o $(PROG) $(PROBE) tests/probe-child tests/lv2-probe-child tests/rt_interpose.so $(FAKE) $(PIN_TEST) $(HISTORY_TEST) $(PROC_TEST) $(PLANNER_TEST) $(QUALCOST_TEST) $(RELAY_TEST) tests/relay-test-relay.o $(FORMAT_TEST) $(GESTURE_ALLOC_TEST) $(LV2_ALLOC_TEST) tests/lv2-alloc-lv2.o $(VERBS_ALLOC_TEST) $(TRANSFORMS_TEST) $(GEN) tests/stress.clap tests/jack_levels tests/host_scenarios $(LV2_FIXTURE) @rm -rf build # the daemon against workers that are not plugin hosts at all (tests/fake-host speaks the same protocol and can be @@ -306,8 +307,8 @@ test-socket: $(PROG) test-format: $(FORMAT_TEST) $(FAKE) PLUGIN_HOSTD_FORMAT_TEST_DIR=$${TMPDIR:-$${XDG_RUNTIME_DIR:-.}} ./$(FORMAT_TEST) ./$(FAKE) -# the daemon's parts without its main and its verbs: the test calls the supervisor as the verbs do -FORMAT_OBJ = src/conf.o src/proc.o src/supervisor.o src/pins.o src/fsutil.o src/machine.o src/history.o src/relay.o src/formats.o src/lv2.o src/planner.o src/qualcost.o src/json.o +# the daemon's parts without its main and its verbs: format_test calls the supervisor as the verbs do +FORMAT_OBJ = $(filter-out src/main.o src/verbs.o,$(OBJ)) $(FORMAT_TEST): tests/format_test.c $(FORMAT_OBJ) $(PROTOCOL_LIB) $(CC) $(INCS) -Isrc $(CFLAGS) -Werror -o $@ tests/format_test.c $(FORMAT_OBJ) $(PROTOCOL_LIBS) -pthread -lm @@ -361,11 +362,12 @@ test-relay: $(RELAY_TEST) $(RELAY_TEST): tests/relay_test.c src/relay.c src/relay.h include/plugin-hostd/protocol.h $(RENDERED) $(CC) -Iinclude $(CFLAGS) -Werror -Drealloc=relay_test_realloc -c -o tests/relay-test-relay.o src/relay.c $(CC) -Isrc $(CFLAGS) -Werror -o $@ tests/relay_test.c tests/relay-test-relay.o -pthread -# the relay's gesture table and lv2.c's refusal and pick tables when memory runs out: each is built with its realloc -# calls renamed, so the test picks the one that fails -test-alloc: $(GESTURE_ALLOC_TEST) $(LV2_ALLOC_TEST) +# the relay's gesture table, lv2.c's refusal and pick tables, and the verbs' copies and answer when memory runs out: +# each is built with its allocation calls renamed, so the test picks the one that fails +test-alloc: $(GESTURE_ALLOC_TEST) $(LV2_ALLOC_TEST) $(VERBS_ALLOC_TEST) ./$(GESTURE_ALLOC_TEST) ./$(LV2_ALLOC_TEST) + ./$(VERBS_ALLOC_TEST) $(GESTURE_ALLOC_TEST): tests/gesture_alloc_test.c src/relay.c src/relay.h include/plugin-hostd/protocol.h $(RENDERED) $(CC) -Iinclude -Isrc $(CFLAGS) -Werror -Drealloc=alloc_test_realloc -o $@ tests/gesture_alloc_test.c -pthread @@ -374,6 +376,10 @@ $(LV2_ALLOC_TEST): tests/lv2_alloc_test.c src/lv2.c src/formats.h $(CC) $(INCS) -Isrc $(CFLAGS) -Werror -Drealloc=alloc_test_realloc -c -o tests/lv2-alloc-lv2.o src/lv2.c $(CC) $(INCS) -Isrc $(CFLAGS) -Werror -o $@ tests/lv2_alloc_test.c tests/lv2-alloc-lv2.o +$(VERBS_ALLOC_TEST): tests/verbs_alloc_test.c src/verbs.c src/verbs.h $(FORMAT_OBJ) $(PROTOCOL_LIB) + $(CC) $(INCS) -Isrc $(CFLAGS) -Werror -Dstrndup=alloc_test_strndup -Dstrdup=alloc_test_strdup \ + -Dfree=alloc_test_free -o $@ tests/verbs_alloc_test.c $(FORMAT_OBJ) $(PROTOCOL_LIBS) -pthread -lm + tests/stress.clap: tests/stress_plugin.c $(CC) $(CLAP_CFLAGS) $(CFLAGS) -Werror -shared -o $@ $< diff --git a/data/verbs.json b/data/verbs.json index 332fd4a..7e536ad 100644 --- a/data/verbs.json +++ b/data/verbs.json @@ -151,6 +151,9 @@ "format": "worker_list", "reply": "resp :::::,,...", "codes": [ + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -168,6 +171,9 @@ { "ref": "HOST_REPLY_CODES.instanceNonExists" }, + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -185,6 +191,9 @@ { "ref": "PLUGIN_HOSTD_REPLY_CODES.noSuchWorker" }, + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -202,6 +211,9 @@ { "ref": "HOST_REPLY_CODES.instanceNonExists" }, + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -219,6 +231,9 @@ { "ref": "PLUGIN_HOSTD_REPLY_CODES.placementInvalid" }, + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -236,6 +251,9 @@ { "ref": "PLUGIN_HOSTD_REPLY_CODES.placementInvalid" }, + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -250,6 +268,9 @@ "format": "worker_env %s %s %s", "reply": "resp 0", "codes": [ + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -267,6 +288,9 @@ { "ref": "PLUGIN_HOSTD_REPLY_CODES.pinRefused" }, + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -281,6 +305,9 @@ "format": "pin_clear %s", "reply": "resp 0", "codes": [ + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -304,6 +331,9 @@ { "ref": "PLUGIN_HOSTD_REPLY_CODES.pinRefused" }, + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -328,6 +358,9 @@ "format": "lv2_worker_set %s %s", "reply": "resp 0", "codes": [ + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -357,6 +390,9 @@ { "ref": "PLUGIN_HOSTD_REPLY_CODES.placementInvalid" }, + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -376,6 +412,9 @@ { "ref": "PLUGIN_HOSTD_REPLY_CODES.noSuchChain" }, + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -393,6 +432,9 @@ { "ref": "PLUGIN_HOSTD_REPLY_CODES.noSuchChain" }, + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -419,6 +461,9 @@ { "ref": "PLUGIN_HOSTD_REPLY_CODES.descriptorScanFailed" }, + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -445,6 +490,9 @@ { "ref": "PLUGIN_HOSTD_REPLY_CODES.descriptorScanFailed" }, + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -465,6 +513,9 @@ { "ref": "HOST_REPLY_CODES.instanceNonExists" }, + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -487,6 +538,9 @@ { "ref": "HOST_REPLY_CODES.instanceNonExists" }, + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -503,6 +557,9 @@ "format": "history_get %s", "reply": "resp 0 ", "codes": [ + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -517,6 +574,9 @@ "format": "history_clear %s", "reply": "resp 0", "codes": [ + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -543,6 +603,9 @@ { "ref": "PLUGIN_HOSTD_REPLY_CODES.placementInvalid" }, + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -560,6 +623,9 @@ { "ref": "PLUGIN_HOSTD_REPLY_CODES.noSuchPlan" }, + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } @@ -577,6 +643,9 @@ { "ref": "PLUGIN_HOSTD_REPLY_CODES.noSuchPlan" }, + { + "ref": "HOST_REPLY_CODES.memoryAllocation" + }, { "ref": "HOST_REPLY_CODES.invalidOperation" } diff --git a/fuzz/README.md b/fuzz/README.md index 0f2ff27..52c3d1e 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -76,7 +76,7 @@ seed. | Harness | Lines | Covered | Lines no seed reaches | | --- | ---: | ---: | --- | -| command | 153 | 100.00% | | +| command | 194 | 92.78% | the paths taken when the message's copies or an answer get no memory (`tests/verbs_alloc_test.c` drives them) | | reply | 210 | 90.00% | the paths taken when `realloc` fails (`tests/relay_test.c` and `tests/gesture_alloc_test.c` drive them), the relay thread's join and the listening socket in `relay_finish`, and the `fd < 0` guards of `relay_add` and `relay_remove` | The records diff --git a/fuzz/coverage-floor.txt b/fuzz/coverage-floor.txt index e739f61..e99f128 100644 --- a/fuzz/coverage-floor.txt +++ b/fuzz/coverage-floor.txt @@ -1,4 +1,4 @@ # Covered lines each harness's seed corpus reaches (make fuzz-coverage, fuzz/coverage.sh). # A harness below its floor turns fuzz-coverage red. harness covered-lines -command 155 +command 180 reply 189 diff --git a/fuzz/fuzz_command.c b/fuzz/fuzz_command.c index ffde610..30791bc 100644 --- a/fuzz/fuzz_command.c +++ b/fuzz/fuzz_command.c @@ -203,11 +203,11 @@ __attribute__((noinline)) static void check_reply_answer(const struct words *w) if (g_rec.answer) { /* a call answered: the reply is its answer, as it answered it - * mutant: src/verbs.c:133 `text, strlen(text) + 1` -> `text + 1, strlen(text)` + * mutant: src/verbs.c:142 `text, strlen(text) + 1` -> `text + 1, strlen(text)` * input: add-default * mutant: src/verbs.c:90 `return rc;` -> `return SUCCESS;` * input: plugin-hostd-pin_set-refused - * mutant: src/verbs.c:251 `return reply_text;` -> `return sup_resp(SUCCESS);` + * mutant: src/verbs.c:260 `return reply_text;` -> `return sup_resp(SUCCESS);` * input: mod-host-set_bpm-refused */ if (strcmp(g_rec.reply, g_rec.answer)) abort(); @@ -215,7 +215,7 @@ __attribute__((noinline)) static void check_reply_answer(const struct words *w) } /* no call answered: the reply is one the daemon makes itself, a refusal or the reply of quit, which check_reply * holds to "resp " of a declared code and to "resp 0" - * mutant: src/verbs.c:148 `return sup_resp(ERR_INVALID_OPERATION);` -> `return sup_resp(SUCCESS);` + * mutant: src/verbs.c:157 `return sup_resp(ERR_INVALID_OPERATION);` -> `return sup_resp(SUCCESS);` * input: add-too-few */ if (sup_reply_code(g_rec.reply) >= 0 && !(w->n == 1 && verb_is(w->w[0], QUIT))) abort(); @@ -229,12 +229,12 @@ __attribute__((noinline)) static void check_reply(const struct words *w) int code; /* one reply, and nothing sent empty - * mutant: src/verbs.c:269 `reply(msg->sender_id, handle(line, tok, ntok));` -> `free(handle(line, tok, ntok));` + * mutant: src/verbs.c:290 `reply(msg->sender_id, handle(line, tok, ntok));` -> `free(handle(line, tok, ntok));` * input: add-default */ if (g_rec.nreplies != 1 || g_rec.reply_size == 0) abort(); /* the reply is a string that ends where the message the socket sends ends - * mutant: src/verbs.c:133 `strlen(text) + 1` -> `strlen(text)` + * mutant: src/verbs.c:142 `strlen(text) + 1` -> `strlen(text)` * input: add-default */ if (g_rec.reply[g_rec.reply_size - 1] != '\0' || memchr(g_rec.reply, '\0', g_rec.reply_size - 1)) abort(); @@ -252,7 +252,7 @@ __attribute__((noinline)) static void check_reply(const struct words *w) } shape = w->n ? rec_shape_of(w->w[0], strlen(w->w[0])) : REC_MOD_HOST_SHAPE; /* anything else has the shape its verb declares, or mod-host's - * mutant: src/verbs.c:240 `sup_resp(SUCCESS)` -> `sup_resp(1)` + * mutant: src/verbs.c:249 `sup_resp(SUCCESS)` -> `sup_resp(1)` * input: mod-host-quit */ if (!syntax_reply_matches(shape, g_rec.reply) && !syntax_reply_matches(REC_MOD_HOST_SHAPE, g_rec.reply)) abort(); @@ -286,9 +286,9 @@ __attribute__((noinline)) static void check_arity(const struct words *w) else syntax_arity(g_parsed[n].syntax, &min, &max); /* acted on, by its own call or by a broadcast, with an argument count the declaration allows - * mutant: src/verbs.c:188 `ntok == 3` -> `ntok >= 3` + * mutant: src/verbs.c:197 `ntok == 3` -> `ntok >= 3` * input: plugin-hostd-pool_config-extra - * mutant: src/verbs.c:165 `verb_is(verb, EFFECT_REMOVE)` -> `verb_is(verb, EFFECT_REMOVE) && ntok == 2` + * mutant: src/verbs.c:174 `verb_is(verb, EFFECT_REMOVE)` -> `verb_is(verb, EFFECT_REMOVE) && ntok == 2` * input: mod-host-remove-extra */ if (read - 1 < min || read - 1 > max) abort(); @@ -351,7 +351,7 @@ __attribute__((noinline)) static void check_words_add(const struct rec_call *cal const char *client = read > next ? w[next] : "-"; /* the placement and the client, each where the message has it - * mutant: src/verbs.c:155 `placement, client` -> `client, placement` + * mutant: src/verbs.c:164 `placement, client` -> `client, placement` * input: add-pool-p-fuzz_client */ if (call->nargs != 4 || strcmp(call->args[2], placement) || strcmp(call->args[3], client)) abort(); @@ -434,7 +434,7 @@ __attribute__((noinline)) static void check_words(const struct words *w) } item += strcspn(item, " "); /* the argument is the word the declaration puts there, as the controller wrote it - * mutant: src/verbs.c:155 `atoi(tok[2])` -> `atoi(tok[1])` + * mutant: src/verbs.c:164 `atoi(tok[2])` -> `atoi(tok[1])` * input: add-instance-past-the-table */ if (want && (call->args[a] == NULL || strcmp(call->args[a], want))) abort(); @@ -455,7 +455,7 @@ __attribute__((noinline)) static void check_refusal(uint64_t before) if (g_rec.reply == NULL || sup_reply_code(g_rec.reply) >= 0) return; /* nothing changed: no accepted call, no follow-up, no quit - * mutant: src/verbs.c:157 `reply_text && sup_reply_code(reply_text) >= 0` -> `reply_text` + * mutant: src/verbs.c:166 `reply_text && sup_reply_code(reply_text) >= 0` -> `reply_text` * input: add-instance-past-the-table */ if (g_rec.state != before || g_quitting) abort(); @@ -490,17 +490,17 @@ __attribute__((noinline)) static void check_round_trip(const struct words *w, ui free(line); /* the same calls with the same arguments - * mutant: src/verbs.c:263 `strtok_r(copy, " \t", &save)` -> `strtok_r(copy, " ", &save)` + * mutant: src/verbs.c:284 `strtok_r(copy, " \t", &save)` -> `strtok_r(copy, " ", &save)` * input: mod-host-add-tabs */ if (!rec_calls_equal(first, nfirst, g_rec.calls, g_rec.ncalls)) abort(); /* the same reply - * mutant: src/verbs.c:237 `ntok != 1` -> `ntok != 1 || line[0] != ' '` + * mutant: src/verbs.c:246 `ntok != 1` -> `ntok != 1 || line[0] != ' '` * input: quit-leading-space */ if (g_rec.reply == NULL || strcmp(g_rec.reply, reply_first)) abort(); /* the same state, the same host settings, and quitting the same - * mutant: src/verbs.c:239 `g_quitting = 1;` -> `g_quitting = line[0] == ' ';` + * mutant: src/verbs.c:248 `g_quitting = 1;` -> `g_quitting = line[0] == ' ';` * input: quit-leading-space */ if (g_quitting != quit_first || g_rec.state != state_first || g_rec.host_settings != host_first) abort(); diff --git a/fuzz/sabotage/command-reply.patch b/fuzz/sabotage/command-reply.patch index 92d6349..c99f445 100644 --- a/fuzz/sabotage/command-reply.patch +++ b/fuzz/sabotage/command-reply.patch @@ -2,12 +2,12 @@ Breaks the check of fuzz/fuzz_command.c:240 (the reply is a string that ends whe Caught by the seed input "add-default" of fuzz_command. diff --git a/src/verbs.c b/src/verbs.c -index 78daab7..6b8af56 100644 +index aebc6c6..4859ca8 100644 --- a/src/verbs.c +++ b/src/verbs.c -@@ -118,5 +118,5 @@ static char *chain_words(char **tok, int ntok) - static void reply(int fd, char *text) - { +@@ -140,5 +140,5 @@ static void reply(int fd, char *text) + return; + } - socket_send(fd, text, strlen(text) + 1); + socket_send(fd, text, strlen(text)); free(text); diff --git a/fuzz/sabotage/command-round-trip.patch b/fuzz/sabotage/command-round-trip.patch index 903e5e2..718ef9a 100644 --- a/fuzz/sabotage/command-round-trip.patch +++ b/fuzz/sabotage/command-round-trip.patch @@ -2,12 +2,12 @@ Breaks the check of fuzz/fuzz_command.c:495 (a command's words joined by one spa Caught by the seed input "mod-host-add-tabs" of fuzz_command. diff --git a/src/verbs.c b/src/verbs.c -index 78daab7..bf45b47 100644 +index aebc6c6..a715349 100644 --- a/src/verbs.c +++ b/src/verbs.c -@@ -222,5 +222,5 @@ void verbs_receive(msg_t *msg) - *--end = '\0'; - copy = strdup(line); +@@ -282,5 +282,5 @@ void verbs_receive(msg_t *msg) + return; + } - for (t = strtok_r(copy, " \t", &save); t && ntok < MAX_TOKENS; t = strtok_r(NULL, " \t", &save)) + for (t = strtok_r(copy, " ", &save); t && ntok < MAX_TOKENS; t = strtok_r(NULL, " \t", &save)) tok[ntok++] = t; diff --git a/include/plugin-hostd/plugin_hostd_host_verbs.h b/include/plugin-hostd/plugin_hostd_host_verbs.h index 4406be3..466aa4a 100644 --- a/include/plugin-hostd/plugin_hostd_host_verbs.h +++ b/include/plugin-hostd/plugin_hostd_host_verbs.h @@ -39,71 +39,71 @@ struct omx_host_verb { #define OMX_VERB_WORKER_LIST "worker_list" #define OMX_VERB_WORKER_LIST_FMT "worker_list" #define OMX_VERB_WORKER_LIST_REPLY "resp :::::,,..." -#define OMX_VERB_WORKER_LIST_CODES_COUNT 1u -#define OMX_VERB_WORKER_LIST_CODES_INIT { -902 } /* reply.host.invalid-operation */ +#define OMX_VERB_WORKER_LIST_CODES_COUNT 2u +#define OMX_VERB_WORKER_LIST_CODES_INIT { -901, -902 } /* reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.instanceInfo (data/verbs.json): Where an instance lives and what it has cost. */ #define OMX_VERB_INSTANCE_INFO "instance_info" #define OMX_VERB_INSTANCE_INFO_FMT "instance_info %i" #define OMX_VERB_INSTANCE_INFO_REPLY "resp 0 " -#define OMX_VERB_INSTANCE_INFO_CODES_COUNT 2u -#define OMX_VERB_INSTANCE_INFO_CODES_INIT { -3, -902 } /* reply.host.instance-non-exists, reply.host.invalid-operation */ +#define OMX_VERB_INSTANCE_INFO_CODES_COUNT 3u +#define OMX_VERB_INSTANCE_INFO_CODES_INIT { -3, -901, -902 } /* reply.host.instance-non-exists, reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.supervisorReset (data/verbs.json): Re-arm the storm bound and the backoff of a given-up placement: [ | all]. */ #define OMX_VERB_SUPERVISOR_RESET "supervisor_reset" #define OMX_VERB_SUPERVISOR_RESET_FMT "supervisor_reset ..." #define OMX_VERB_SUPERVISOR_RESET_REPLY "resp 0" -#define OMX_VERB_SUPERVISOR_RESET_CODES_COUNT 2u -#define OMX_VERB_SUPERVISOR_RESET_CODES_INIT { -506, -902 } /* reply.plugin-hostd.no-such-worker, reply.host.invalid-operation */ +#define OMX_VERB_SUPERVISOR_RESET_CODES_COUNT 3u +#define OMX_VERB_SUPERVISOR_RESET_CODES_INIT { -506, -901, -902 } /* reply.plugin-hostd.no-such-worker, reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.quarantineClear (data/verbs.json): Let a quarantined instance go back where its placement says: | all. */ #define OMX_VERB_QUARANTINE_CLEAR "quarantine_clear" #define OMX_VERB_QUARANTINE_CLEAR_FMT "quarantine_clear %s" #define OMX_VERB_QUARANTINE_CLEAR_REPLY "resp 0" -#define OMX_VERB_QUARANTINE_CLEAR_CODES_COUNT 2u -#define OMX_VERB_QUARANTINE_CLEAR_CODES_INIT { -3, -902 } /* reply.host.instance-non-exists, reply.host.invalid-operation */ +#define OMX_VERB_QUARANTINE_CLEAR_CODES_COUNT 3u +#define OMX_VERB_QUARANTINE_CLEAR_CODES_INIT { -3, -901, -902 } /* reply.host.instance-non-exists, reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.policySet (data/verbs.json): The placement of an add that says default: own | pool:. */ #define OMX_VERB_POLICY_SET "policy_set" #define OMX_VERB_POLICY_SET_FMT "policy_set %s %s" #define OMX_VERB_POLICY_SET_REPLY "resp 0" -#define OMX_VERB_POLICY_SET_CODES_COUNT 2u -#define OMX_VERB_POLICY_SET_CODES_INIT { -501, -902 } /* reply.plugin-hostd.placement-invalid, reply.host.invalid-operation */ +#define OMX_VERB_POLICY_SET_CODES_COUNT 3u +#define OMX_VERB_POLICY_SET_CODES_INIT { -501, -901, -902 } /* reply.plugin-hostd.placement-invalid, reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.poolConfig (data/verbs.json): The instances a pool holds before a sibling opens: . */ #define OMX_VERB_POOL_CONFIG "pool_config" #define OMX_VERB_POOL_CONFIG_FMT "pool_config %s %i" #define OMX_VERB_POOL_CONFIG_REPLY "resp 0" -#define OMX_VERB_POOL_CONFIG_CODES_COUNT 2u -#define OMX_VERB_POOL_CONFIG_CODES_INIT { -501, -902 } /* reply.plugin-hostd.placement-invalid, reply.host.invalid-operation */ +#define OMX_VERB_POOL_CONFIG_CODES_COUNT 3u +#define OMX_VERB_POOL_CONFIG_CODES_INIT { -501, -901, -902 } /* reply.plugin-hostd.placement-invalid, reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.workerEnv (data/verbs.json): The cpu list and the nice value of the workers of a format, at spawn and on the ones running: . */ #define OMX_VERB_WORKER_ENV "worker_env" #define OMX_VERB_WORKER_ENV_FMT "worker_env %s %s %s" #define OMX_VERB_WORKER_ENV_REPLY "resp 0" -#define OMX_VERB_WORKER_ENV_CODES_COUNT 1u -#define OMX_VERB_WORKER_ENV_CODES_INIT { -902 } /* reply.host.invalid-operation */ +#define OMX_VERB_WORKER_ENV_CODES_COUNT 2u +#define OMX_VERB_WORKER_ENV_CODES_INIT { -901, -902 } /* reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.pinSet (data/verbs.json): The pin of one plugin: =[,=...] : [verdict=]. */ #define OMX_VERB_PIN_SET "pin_set" #define OMX_VERB_PIN_SET_FMT "pin_set %s %s %s ..." #define OMX_VERB_PIN_SET_REPLY "resp 0" -#define OMX_VERB_PIN_SET_CODES_COUNT 2u -#define OMX_VERB_PIN_SET_CODES_INIT { -516, -902 } /* reply.plugin-hostd.pin-refused, reply.host.invalid-operation */ +#define OMX_VERB_PIN_SET_CODES_COUNT 3u +#define OMX_VERB_PIN_SET_CODES_INIT { -516, -901, -902 } /* reply.plugin-hostd.pin-refused, reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.pinClear (data/verbs.json): Forget the pin of one plugin, or of every one: | all. */ #define OMX_VERB_PIN_CLEAR "pin_clear" #define OMX_VERB_PIN_CLEAR_FMT "pin_clear %s" #define OMX_VERB_PIN_CLEAR_REPLY "resp 0" -#define OMX_VERB_PIN_CLEAR_CODES_COUNT 1u -#define OMX_VERB_PIN_CLEAR_CODES_INIT { -902 } /* reply.host.invalid-operation */ +#define OMX_VERB_PIN_CLEAR_CODES_COUNT 2u +#define OMX_VERB_PIN_CLEAR_CODES_INIT { -901, -902 } /* reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.pinAccept (data/verbs.json): Under pin_mode first-use, take the plugin's files as they are now as its pin: . */ #define OMX_VERB_PIN_ACCEPT "pin_accept" #define OMX_VERB_PIN_ACCEPT_FMT "pin_accept %s" #define OMX_VERB_PIN_ACCEPT_REPLY "resp 0" -#define OMX_VERB_PIN_ACCEPT_CODES_COUNT 4u -#define OMX_VERB_PIN_ACCEPT_CODES_INIT { -508, -509, -516, -902 } /* reply.host.pin-absent, reply.plugin-hostd.pin-binary-mismatch, reply.plugin-hostd.pin-refused, reply.host.invalid-operation */ +#define OMX_VERB_PIN_ACCEPT_CODES_COUNT 5u +#define OMX_VERB_PIN_ACCEPT_CODES_INIT { -508, -509, -516, -901, -902 } /* reply.host.pin-absent, reply.plugin-hostd.pin-binary-mismatch, reply.plugin-hostd.pin-refused, reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.pinLayout (data/verbs.json): Sent by the daemon to a worker that checks layouts, after it accepted an add whose layout pin is unknown: the layout the worker found after init. */ #define OMX_VERB_PIN_LAYOUT "pin_layout" @@ -114,92 +114,92 @@ struct omx_host_verb { #define OMX_VERB_LV2_WORKER_SET "lv2_worker_set" #define OMX_VERB_LV2_WORKER_SET_FMT "lv2_worker_set %s %s" #define OMX_VERB_LV2_WORKER_SET_REPLY "resp 0" -#define OMX_VERB_LV2_WORKER_SET_CODES_COUNT 1u -#define OMX_VERB_LV2_WORKER_SET_CODES_INIT { -902 } /* reply.host.invalid-operation */ +#define OMX_VERB_LV2_WORKER_SET_CODES_COUNT 2u +#define OMX_VERB_LV2_WORKER_SET_CODES_INIT { -901, -902 } /* reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.chain (data/verbs.json): The instances, in this order, are one chain run in one worker: [ ...]; a worker that runs chains in one client is sent the line with the members it holds. */ #define OMX_VERB_CHAIN "chain" #define OMX_VERB_CHAIN_FMT "chain %i %i ..." #define OMX_VERB_CHAIN_REPLY "resp 0" -#define OMX_VERB_CHAIN_CODES_COUNT 6u -#define OMX_VERB_CHAIN_CODES_INIT { -518, -519, -520, -521, -501, -902 } /* reply.plugin-hostd.chain-unknown-instance, reply.plugin-hostd.chain-duplicate, reply.plugin-hostd.chain-instance-busy, reply.plugin-hostd.chain-mixed-format, reply.plugin-hostd.placement-invalid, reply.host.invalid-operation */ +#define OMX_VERB_CHAIN_CODES_COUNT 7u +#define OMX_VERB_CHAIN_CODES_INIT { -518, -519, -520, -521, -501, -901, -902 } /* reply.plugin-hostd.chain-unknown-instance, reply.plugin-hostd.chain-duplicate, reply.plugin-hostd.chain-instance-busy, reply.plugin-hostd.chain-mixed-format, reply.plugin-hostd.placement-invalid, reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.chainClear (data/verbs.json): Forget the chain: each member goes to a worker of its own. */ #define OMX_VERB_CHAIN_CLEAR "chain_clear" #define OMX_VERB_CHAIN_CLEAR_FMT "chain_clear %i" #define OMX_VERB_CHAIN_CLEAR_REPLY "resp 0" -#define OMX_VERB_CHAIN_CLEAR_CODES_COUNT 2u -#define OMX_VERB_CHAIN_CLEAR_CODES_INIT { -522, -902 } /* reply.plugin-hostd.no-such-chain, reply.host.invalid-operation */ +#define OMX_VERB_CHAIN_CLEAR_CODES_COUNT 3u +#define OMX_VERB_CHAIN_CLEAR_CODES_INIT { -522, -901, -902 } /* reply.plugin-hostd.no-such-chain, reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.chainGet (data/verbs.json): Read a chain back: its worker, its members in the declared order, and the members its worker runs now. */ #define OMX_VERB_CHAIN_GET "chain_get" #define OMX_VERB_CHAIN_GET_FMT "chain_get %i" #define OMX_VERB_CHAIN_GET_REPLY "resp 0 ,... ,..." -#define OMX_VERB_CHAIN_GET_CODES_COUNT 2u -#define OMX_VERB_CHAIN_GET_CODES_INIT { -522, -902 } /* reply.plugin-hostd.no-such-chain, reply.host.invalid-operation */ +#define OMX_VERB_CHAIN_GET_CODES_COUNT 3u +#define OMX_VERB_CHAIN_GET_CODES_INIT { -522, -901, -902 } /* reply.plugin-hostd.no-such-chain, reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.descriptorGet (data/verbs.json): The scanned descriptor of the plugin, answered from descriptor_dir: . */ #define OMX_VERB_DESCRIPTOR_GET "descriptor_get" #define OMX_VERB_DESCRIPTOR_GET_FMT "descriptor_get %s" #define OMX_VERB_DESCRIPTOR_GET_REPLY "resp 0 \"\"" -#define OMX_VERB_DESCRIPTOR_GET_CODES_COUNT 5u -#define OMX_VERB_DESCRIPTOR_GET_CODES_INIT { -528, -529, -530, -531, -902 } /* reply.plugin-hostd.dir-not-writable, reply.plugin-hostd.descriptor-absent, reply.plugin-hostd.descriptor-pending, reply.plugin-hostd.descriptor-scan-failed, reply.host.invalid-operation */ +#define OMX_VERB_DESCRIPTOR_GET_CODES_COUNT 6u +#define OMX_VERB_DESCRIPTOR_GET_CODES_INIT { -528, -529, -530, -531, -901, -902 } /* reply.plugin-hostd.dir-not-writable, reply.plugin-hostd.descriptor-absent, reply.plugin-hostd.descriptor-pending, reply.plugin-hostd.descriptor-scan-failed, reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.panelGet (data/verbs.json): The panel the plugin publishes, answered from descriptor_dir: . */ #define OMX_VERB_PANEL_GET "panel_get" #define OMX_VERB_PANEL_GET_FMT "panel_get %s" #define OMX_VERB_PANEL_GET_REPLY "resp 0 \"\"" -#define OMX_VERB_PANEL_GET_CODES_COUNT 5u -#define OMX_VERB_PANEL_GET_CODES_INIT { -528, -529, -530, -531, -902 } /* reply.plugin-hostd.dir-not-writable, reply.plugin-hostd.descriptor-absent, reply.plugin-hostd.descriptor-pending, reply.plugin-hostd.descriptor-scan-failed, reply.host.invalid-operation */ +#define OMX_VERB_PANEL_GET_CODES_COUNT 6u +#define OMX_VERB_PANEL_GET_CODES_INIT { -528, -529, -530, -531, -901, -902 } /* reply.plugin-hostd.dir-not-writable, reply.plugin-hostd.descriptor-absent, reply.plugin-hostd.descriptor-pending, reply.plugin-hostd.descriptor-scan-failed, reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.presetList (data/verbs.json): The plugin's presets, the ones its preset discovery or its bundle lists. */ #define OMX_VERB_PRESET_LIST "preset_list" #define OMX_VERB_PRESET_LIST_FMT "preset_list %i" #define OMX_VERB_PRESET_LIST_REPLY "resp " -#define OMX_VERB_PRESET_LIST_CODES_COUNT 3u -#define OMX_VERB_PRESET_LIST_CODES_INIT { -515, -3, -902 } /* reply.plugin-hostd.no-preset-list, reply.host.instance-non-exists, reply.host.invalid-operation */ +#define OMX_VERB_PRESET_LIST_CODES_COUNT 4u +#define OMX_VERB_PRESET_LIST_CODES_INIT { -515, -3, -901, -902 } /* reply.plugin-hostd.no-preset-list, reply.host.instance-non-exists, reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.presetGet (data/verbs.json): One preset, 0 to count - 1: the key preset_load takes for it, and its name. */ #define OMX_VERB_PRESET_GET "preset_get" #define OMX_VERB_PRESET_GET_FMT "preset_get %i %i" #define OMX_VERB_PRESET_GET_REPLY "resp 0 " -#define OMX_VERB_PRESET_GET_CODES_COUNT 3u -#define OMX_VERB_PRESET_GET_CODES_INIT { -515, -3, -902 } /* reply.plugin-hostd.no-preset-list, reply.host.instance-non-exists, reply.host.invalid-operation */ +#define OMX_VERB_PRESET_GET_CODES_COUNT 4u +#define OMX_VERB_PRESET_GET_CODES_INIT { -515, -3, -901, -902 } /* reply.plugin-hostd.no-preset-list, reply.host.instance-non-exists, reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.historyGet (data/verbs.json): What the daemon kept of one plugin on this host class, for the binary its pin holds now: . The history is one word, digest=,class=,crashed=<0|1>,raises_xruns=<0|1> and then every key=value of PLUGIN_HOSTD_HISTORY_KEYS, joined by commas; - when the plugin has no record or no pin, unavailable while the history file is blocked. */ #define OMX_VERB_HISTORY_GET "history_get" #define OMX_VERB_HISTORY_GET_FMT "history_get %s" #define OMX_VERB_HISTORY_GET_REPLY "resp 0 " -#define OMX_VERB_HISTORY_GET_CODES_COUNT 1u -#define OMX_VERB_HISTORY_GET_CODES_INIT { -902 } /* reply.host.invalid-operation */ +#define OMX_VERB_HISTORY_GET_CODES_COUNT 2u +#define OMX_VERB_HISTORY_GET_CODES_INIT { -901, -902 } /* reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.historyClear (data/verbs.json): Delete the history of one plugin, every binary and host class, or all of it: | all. Refused while the history file is blocked. */ #define OMX_VERB_HISTORY_CLEAR "history_clear" #define OMX_VERB_HISTORY_CLEAR_FMT "history_clear %s" #define OMX_VERB_HISTORY_CLEAR_REPLY "resp 0" -#define OMX_VERB_HISTORY_CLEAR_CODES_COUNT 1u -#define OMX_VERB_HISTORY_CLEAR_CODES_INIT { -902 } /* reply.host.invalid-operation */ +#define OMX_VERB_HISTORY_CLEAR_CODES_COUNT 2u +#define OMX_VERB_HISTORY_CLEAR_CODES_INIT { -901, -902 } /* reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.plan (data/verbs.json): The instances, in signal order, are planned into the fewest workers their pins and costs allow: [ ...]. Each run of consecutive members that may share a worker becomes a chain of place chain:., the others stay alone in a worker of their own; the reply names the segments in order, the members of a segment joined by commas and the segments by semicolons. With the setting grouping off the plan is answered and kept, and nothing moves. */ #define OMX_VERB_PLAN "plan" #define OMX_VERB_PLAN_FMT "plan %i %i ..." #define OMX_VERB_PLAN_REPLY "resp 0 ,...;,...;..." -#define OMX_VERB_PLAN_CODES_COUNT 5u -#define OMX_VERB_PLAN_CODES_INIT { -518, -519, -520, -501, -902 } /* reply.plugin-hostd.chain-unknown-instance, reply.plugin-hostd.chain-duplicate, reply.plugin-hostd.chain-instance-busy, reply.plugin-hostd.placement-invalid, reply.host.invalid-operation */ +#define OMX_VERB_PLAN_CODES_COUNT 6u +#define OMX_VERB_PLAN_CODES_INIT { -518, -519, -520, -501, -901, -902 } /* reply.plugin-hostd.chain-unknown-instance, reply.plugin-hostd.chain-duplicate, reply.plugin-hostd.chain-instance-busy, reply.plugin-hostd.placement-invalid, reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.planGet (data/verbs.json): Read the last plan of a chain back: its segments as plan answered them, then the reason of each member that has one (a word of PLUGIN_HOSTD_GROUPING_REASONS, plan-refused followed by the refusal's number), instance:reason joined by commas, - for none. */ #define OMX_VERB_PLAN_GET "plan_get" #define OMX_VERB_PLAN_GET_FMT "plan_get %i" #define OMX_VERB_PLAN_GET_REPLY "resp 0 ,...;,...;... :,..." -#define OMX_VERB_PLAN_GET_CODES_COUNT 2u -#define OMX_VERB_PLAN_GET_CODES_INIT { -532, -902 } /* reply.plugin-hostd.no-such-plan, reply.host.invalid-operation */ +#define OMX_VERB_PLAN_GET_CODES_COUNT 3u +#define OMX_VERB_PLAN_GET_CODES_INIT { -532, -901, -902 } /* reply.plugin-hostd.no-such-plan, reply.host.memory-allocation, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.planClear (data/verbs.json): Forget the plan: each member goes to a worker of its own, as chain_clear does. */ #define OMX_VERB_PLAN_CLEAR "plan_clear" #define OMX_VERB_PLAN_CLEAR_FMT "plan_clear %i" #define OMX_VERB_PLAN_CLEAR_REPLY "resp 0" -#define OMX_VERB_PLAN_CLEAR_CODES_COUNT 2u -#define OMX_VERB_PLAN_CLEAR_CODES_INIT { -532, -902 } /* reply.plugin-hostd.no-such-plan, reply.host.invalid-operation */ +#define OMX_VERB_PLAN_CLEAR_CODES_COUNT 3u +#define OMX_VERB_PLAN_CLEAR_CODES_INIT { -532, -901, -902 } /* reply.plugin-hostd.no-such-plan, reply.host.memory-allocation, reply.host.invalid-operation */ #define PLUGIN_HOSTD_HOST_VERB_COUNT 24u diff --git a/src/verbs.c b/src/verbs.c index 1e425b3..5a06d95 100644 --- a/src/verbs.c +++ b/src/verbs.c @@ -130,6 +130,15 @@ static char *plan_words(char **tok, int ntok) static void reply(int fd, char *text) { + char refusal[32]; + + /* an answer there was no memory for: mod-host's refusal, written on the stack */ + if (!text) + { + snprintf(refusal, sizeof(refusal), "resp %d", ERR_MEMORY_ALLOCATION); + socket_send(fd, refusal, strlen(refusal) + 1); + return; + } socket_send(fd, text, strlen(text) + 1); free(text); } @@ -256,10 +265,22 @@ void verbs_receive(msg_t *msg) char *line = strndup(msg->data, msg->data_size), *tok[MAX_TOKENS], *save = NULL, *t, *copy, *end; int ntok = 0; + /* a message there is no memory to copy is refused, and not read */ + if (!line) + { + reply(msg->sender_id, NULL); + return; + } end = line + strlen(line); while (end > line && (end[-1] == '\n' || end[-1] == '\r' || end[-1] == ' ' || end[-1] == '\t')) *--end = '\0'; copy = strdup(line); + if (!copy) + { + reply(msg->sender_id, NULL); + free(line); + return; + } for (t = strtok_r(copy, " \t", &save); t && ntok < MAX_TOKENS; t = strtok_r(NULL, " \t", &save)) tok[ntok++] = t; if (ntok == 0) diff --git a/tests/sabotage.py b/tests/sabotage.py index 3069acc..22e132e 100644 --- a/tests/sabotage.py +++ b/tests/sabotage.py @@ -15,7 +15,7 @@ # an LV2 bundle (MOD_HOST, LV2_DIR, LV2_URI, LV2_BUNDLE, LV2_PARAM) and is skipped without them; "pin" breaks # include/plugin-hostd/pin.h and runs tests/pin_test.c; "readme" breaks README.md or BUILDING.md and runs # tests/readme_build.py, with no daemon to build; "probeschema" breaks protocol/probe-output.schema.json and runs -# tests/probe_schema.py; "relay" breaks src/relay.c and runs tests/relay_test.c; "history" breaks src/history.c or src/fsutil.c and runs tests/history_test.c; "proc" breaks src/proc.c and runs tests/proc_test.c; "alloc" breaks src/relay.c or src/lv2.c and runs make test-alloc; "contract" breaks the probe runner or the generator and runs tests/contract_derived.py; "consumer" breaks the header and compiles tests/consumer.c against it; "probe" breaks src/probe.c and runs tests/probe_test.py; "nopins" breaks the packaging or a workflow and runs tests/no_pins.py; "ciguards" breaks a workflow and runs tests/ci_guards.py; +# tests/probe_schema.py; "relay" breaks src/relay.c and runs tests/relay_test.c; "history" breaks src/history.c or src/fsutil.c and runs tests/history_test.c; "proc" breaks src/proc.c and runs tests/proc_test.c; "alloc" breaks src/relay.c, src/lv2.c or src/verbs.c and runs make test-alloc; "contract" breaks the probe runner or the generator and runs tests/contract_derived.py; "consumer" breaks the header and compiles tests/consumer.c against it; "probe" breaks src/probe.c and runs tests/probe_test.py; "nopins" breaks the packaging or a workflow and runs tests/no_pins.py; "ciguards" breaks a workflow and runs tests/ci_guards.py; # "vocabulary" breaks the probe schema, its template or a copy of omx-contract's omx-vocabulary.schema.json and runs # tests/vocabulary_derived.py; "verbtable" breaks the header and runs tests/verb_table.py; "presetschema" breaks # protocol/preset.schema.json and runs tests/preset_format.py; "pool" breaks tests/sabotage_pool.py or the port range of @@ -355,6 +355,13 @@ " if (grown == NULL)\n return;\n", "", "alloc_test", "alloc"), ("a pick the lv2 table has no memory for takes the table with it", "src/lv2.c", " if (grown == NULL)\n return -1;\n", "", "alloc_test", "alloc"), + ("a message there is no memory to copy is read anyway", "src/verbs.c", " if (!line)\n", " if (0)\n", + "alloc_test", "alloc"), + ("a message there is no memory to split is split anyway", "src/verbs.c", " if (!copy)\n", " if (0)\n", + "alloc_test", "alloc"), + ("a message there is no memory to split leaks its line", "src/verbs.c", + " reply(msg->sender_id, NULL);\n free(line);\n", " reply(msg->sender_id, NULL);\n", "alloc_test", "alloc"), + ("an answer there is no memory for is sent anyway", "src/verbs.c", " if (!text)\n", " if (0)\n", "alloc_test", "alloc"), ("the host's pin overrides a pins.d refusal", "src/pins.c", " pin_t *p = pkg_refusal(uri);\n\n if (!p)\n", " pin_t *p = NULL;\n\n if (!p)\n", "a_pins_d_refusal_wins_over_any_other_pins_d_pin_and_over_the_host"), ("pin_set overrides a pins.d refusal", "src/pins.c", " if (pkg_refusal(pin.uri))\n", " if (0)\n", "a_pins_d_refusal_wins_over_any_other_pins_d_pin_and_over_the_host"), @@ -1314,8 +1321,9 @@ def run_proc_test(tree): def run_alloc_test(tree): - """make test-alloc: the tables of src/relay.c and src/lv2.c when realloc fails; prints "ok alloc_test" when no check failed""" - for f in ("tests/gesture-alloc-test", "tests/lv2-alloc-test", "tests/lv2-alloc-lv2.o"): + """make test-alloc: the tables of src/relay.c and src/lv2.c when realloc fails, and the copies and the answer of + src/verbs.c when memory runs out; prints "ok alloc_test" when no check failed""" + for f in ("tests/gesture-alloc-test", "tests/lv2-alloc-test", "tests/lv2-alloc-lv2.o", "tests/verbs-alloc-test"): if os.path.exists(os.path.join(tree, f)): os.unlink(os.path.join(tree, f)) p = subprocess.run(["make", "-s", "MOD_HOST_DIR=" + MOD_HOST_DIR, "test-alloc"], cwd=tree, capture_output=True, text=True, @@ -1468,7 +1476,7 @@ def arm(entry, tree): shutil.copytree(os.path.join(ROOT, "include"), os.path.join(tree, "include")) shutil.copy(os.path.join(ROOT, "Makefile"), tree) os.makedirs(os.path.join(tree, "tests")) - for f in ("gesture_alloc_test.c", "lv2_alloc_test.c"): + for f in ("gesture_alloc_test.c", "lv2_alloc_test.c", "verbs_alloc_test.c"): shutil.copy(os.path.join(ROOT, "tests", f), os.path.join(tree, "tests")) elif jack == "probe": # the probe is built by the Makefile's own rule: its sources, headers and the spec its VERSION is read from diff --git a/tests/verbs_alloc_test.c b/tests/verbs_alloc_test.c new file mode 100644 index 0000000..5386083 --- /dev/null +++ b/tests/verbs_alloc_test.c @@ -0,0 +1,150 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* src/verbs.c when memory runs out. The test includes the verbs with their strndup, strdup and free calls renamed to + * alloc_test_strndup, alloc_test_strdup and alloc_test_free (the Makefile's -D), so it decides which copy fails and + * sees which copies are freed; the rest of the daemon is linked as it is. A message the daemon has no memory to copy, + * and an answer it has no memory for, are each answered once, with mod-host's ERR_MEMORY_ALLOCATION. */ + +#include +#include +#include +#include +#include + +#include "../src/verbs.c" + +/* the C library's strndup, strdup and free, by their symbols: the names are renamed in this file */ +char *libc_strndup(const char *s, size_t n) __asm__("strndup"); +char *libc_strdup(const char *s) __asm__("strdup"); +void libc_free(void *p) __asm__("free"); + +static int failures, checks; + +/* how many copies succeed before the next one fails; -1 is never */ +static int g_fail_after = -1; + +/* the controller's end of the command socket, and the daemon's */ +static int g_sv[2]; + +/* the copies the verbs made and have not freed */ +static void *g_held[4]; + +static char *hold(char *p) +{ + size_t n; + + for (n = 0; p && n < sizeof(g_held) / sizeof(g_held[0]); n++) + if (!g_held[n]) + { + g_held[n] = p; + break; + } + return p; +} + +static int held(void) +{ + size_t n; + int k = 0; + + for (n = 0; n < sizeof(g_held) / sizeof(g_held[0]); n++) + k += g_held[n] != NULL; + return k; +} + +static int copy_fails(void) +{ + if (g_fail_after == 0) + return 1; + if (g_fail_after > 0) + g_fail_after--; + return 0; +} + +char *alloc_test_strndup(const char *s, size_t n); +char *alloc_test_strdup(const char *s); +void alloc_test_free(void *p); + +char *alloc_test_strndup(const char *s, size_t n) +{ + return copy_fails() ? NULL : hold(libc_strndup(s, n)); +} + +char *alloc_test_strdup(const char *s) +{ + return copy_fails() ? NULL : hold(libc_strdup(s)); +} + +void alloc_test_free(void *p) +{ + size_t n; + + for (n = 0; n < sizeof(g_held) / sizeof(g_held[0]); n++) + if (g_held[n] == p) + g_held[n] = NULL; + libc_free(p); +} + +static void check(int ok, const char *what) +{ + checks++; + if (!ok) + { + failures++; + printf("FAIL verbs: %s\n", what); + } +} + +/* whether the controller has exactly `want` and its NUL to read, and nothing after it */ +static int answered(const char *want) +{ + char got[64]; + ssize_t n = recv(g_sv[0], got, sizeof(got), MSG_DONTWAIT); + + return n == (ssize_t)strlen(want) + 1 && !memcmp(got, want, (size_t)n) && recv(g_sv[0], got, 1, MSG_DONTWAIT) < 0; +} + +/* `text` sent by the controller, with `copies` copies of it made before the next one fails */ +static void receive(const char *text, int copies) +{ + msg_t msg; + + msg.sender_id = g_sv[1]; + msg.data = (char *)text; + msg.data_size = strlen(text); + g_fail_after = copies; + verbs_receive(&msg); + g_fail_after = -1; +} + +int main(void) +{ + if (socketpair(AF_UNIX, SOCK_STREAM, 0, g_sv) != 0) + { + perror("socketpair"); + return 1; + } + + receive(" \t\r\n", -1); + check(answered("resp -902"), "positive control: a message of no words is refused with memory"); + check(!held(), "positive control: a message read whole frees both its copies"); + + receive("quit", 0); + check(answered("resp -901"), "a message with no memory for its line is answered once, with -901"); + check(!g_quitting, "a quit with no memory for its line does not quit"); + + receive("quit", 1); + check(answered("resp -901"), "a message with no memory for its words is answered once, with -901"); + check(!g_quitting, "a quit with no memory for its words does not quit"); + check(!held(), "a message with no memory for its words frees its line"); + + reply(g_sv[1], NULL); + check(answered("resp -901"), "an answer there was no memory for is sent once, as -901"); + + receive(" \t\r\n", -1); + check(answered("resp -902"), "the next message is read as before"); + + close(g_sv[0]); + close(g_sv[1]); + printf("verbs: %d checks, %s\n", checks, failures ? "FAILED" : "ok"); + return failures ? 1 : 0; +}