builtin/maintenance: fix leak in `get_schedule_cmd()`

The `get_schedule_cmd()` function allows us to override the schedule command with a specific test command such that we can verify the underlying logic in a platform-independent way. Its memory management is somewhat wild though, because it basically gives up and assigns an allocated string to the string constant output pointer. While this part is marked with `UNLEAK()` to mask this, we also leak the local string lists. Rework the function such that it has a separate out parameter. If set, we will assign it the final allocated command. Plug the other memory leaks and create a common exit path. Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Sep 26, 2024 at 13:47 UTC b6c3f8e12c0a521450923ddbcf7a19a81aa3c4e7
2 files changed +81 -47
builtin/gc.c
+80 -47
@@ -1780,32 +1780,33 @@ static const char *get_frequency(enum schedule_priority schedule)
1780 * * If $GIT_TEST_MAINT_SCHEDULER is set, return true.
1781 * In this case, the *cmd value is read as input.
1782 *
1783 - * * if the input value *cmd is the key of one of the comma-separated list
1784 - * item, then *is_available is set to true and *cmd is modified and becomes
1783 + * * if the input value cmd is the key of one of the comma-separated list
1784 + * item, then *is_available is set to true and *out is set to
1785 * the mock command.
1786 *
1787 * * if the input value *cmd isn’t the key of any of the comma-separated list
1788 - * item, then *is_available is set to false.
1788 + * item, then *is_available is set to false and *out is set to the original
1789 + * command.
1790 *
1791 * Ex.:
1792 * GIT_TEST_MAINT_SCHEDULER not set
1793 * +-------+-------------------------------------------------+
1794 * | Input | Output |
1794 - * | *cmd | return code | *cmd | *is_available |
1795 + * | *cmd | return code | *out | *is_available |
1796 * +-------+-------------+-------------------+---------------+
1796 - * | "foo" | false | "foo" (unchanged) | (unchanged) |
1797 + * | "foo" | false | NULL | (unchanged) |
1798 * +-------+-------------+-------------------+---------------+
1799 *
1800 * GIT_TEST_MAINT_SCHEDULER set to “foo:./mock_foo.sh,bar:./mock_bar.sh”
1801 * +-------+-------------------------------------------------+
1802 * | Input | Output |
1802 - * | *cmd | return code | *cmd | *is_available |
1803 + * | *cmd | return code | *out | *is_available |
1804 * +-------+-------------+-------------------+---------------+
1805 * | "foo" | true | "./mock.foo.sh" | true |
1805 - * | "qux" | true | "qux" (unchanged) | false |
1806 + * | "qux" | true | "qux" (allocated) | false |
1807 * +-------+-------------+-------------------+---------------+
1808 */
1808 -static int get_schedule_cmd(const char **cmd, int *is_available)
1809 +static int get_schedule_cmd(const char *cmd, int *is_available, char **out)
1810 {
1811 char *testing = xstrdup_or_null(getenv("GIT_TEST_MAINT_SCHEDULER"));
1812 struct string_list_item *item;
@@ -1824,16 +1825,22 @@ static int get_schedule_cmd(const char **cmd, int *is_available)
1825 if (string_list_split_in_place(&pair, item->string, ":", 2) != 2)
1826 continue;
1827
1827 - if (!strcmp(*cmd, pair.items[0].string)) {
1828 - *cmd = pair.items[1].string;
1828 + if (!strcmp(cmd, pair.items[0].string)) {
1829 + if (out)
1830 + *out = xstrdup(pair.items[1].string);
1831 if (is_available)
1832 *is_available = 1;
1831 - string_list_clear(&list, 0);
1832 - UNLEAK(testing);
1833 - return 1;
1833 + string_list_clear(&pair, 0);
1834 + goto out;
1835 }
1836 +
1837 + string_list_clear(&pair, 0);
1838 }
1839
1840 + if (out)
1841 + *out = xstrdup(cmd);
1842 +
1843 +out:
1844 string_list_clear(&list, 0);
1845 free(testing);
1846 return 1;
@@ -1850,9 +1857,8 @@ static int get_random_minute(void)
1857
1858 static int is_launchctl_available(void)
1859 {
1853 - const char *cmd = "launchctl";
1860 int is_available;
1855 - if (get_schedule_cmd(&cmd, &is_available))
1861 + if (get_schedule_cmd("launchctl", &is_available, NULL))
1862 return is_available;
1863
1864 #ifdef __APPLE__
@@ -1890,12 +1896,12 @@ static char *launchctl_get_uid(void)
1896
1897 static int launchctl_boot_plist(int enable, const char *filename)
1898 {
1893 - const char *cmd = "launchctl";
1899 + char *cmd;
1900 int result;
1901 struct child_process child = CHILD_PROCESS_INIT;
1902 char *uid = launchctl_get_uid();
1903
1898 - get_schedule_cmd(&cmd, NULL);
1904 + get_schedule_cmd("launchctl", NULL, &cmd);
1905 strvec_split(&child.args, cmd);
1906 strvec_pushl(&child.args, enable ? "bootstrap" : "bootout", uid,
1907 filename, NULL);
@@ -1908,6 +1914,7 @@ static int launchctl_boot_plist(int enable, const char *filename)
1914
1915 result = finish_command(&child);
1916
1917 + free(cmd);
1918 free(uid);
1919 return result;
1920 }
@@ -1959,10 +1966,10 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
1966 static unsigned long lock_file_timeout_ms = ULONG_MAX;
1967 struct strbuf plist = STRBUF_INIT, plist2 = STRBUF_INIT;
1968 struct stat st;
1962 - const char *cmd = "launchctl";
1969 + char *cmd;
1970 int minute = get_random_minute();
1971
1965 - get_schedule_cmd(&cmd, NULL);
1972 + get_schedule_cmd("launchctl", NULL, &cmd);
1973 preamble = "<?xml version=\"1.0\"?>\n"
1974 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
1975 "<plist version=\"1.0\">"
@@ -2052,6 +2059,7 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
2059
2060 free(filename);
2061 free(name);
2062 + free(cmd);
2063 strbuf_release(&plist);
2064 strbuf_release(&plist2);
2065 return 0;
@@ -2076,9 +2084,8 @@ static int launchctl_update_schedule(int run_maintenance, int fd UNUSED)
2084
2085 static int is_schtasks_available(void)
2086 {
2079 - const char *cmd = "schtasks";
2087 int is_available;
2081 - if (get_schedule_cmd(&cmd, &is_available))
2088 + if (get_schedule_cmd("schtasks", &is_available, NULL))
2089 return is_available;
2090
2091 #ifdef GIT_WINDOWS_NATIVE
@@ -2097,15 +2104,16 @@ static char *schtasks_task_name(const char *frequency)
2104
2105 static int schtasks_remove_task(enum schedule_priority schedule)
2106 {
2100 - const char *cmd = "schtasks";
2107 + char *cmd;
2108 struct child_process child = CHILD_PROCESS_INIT;
2109 const char *frequency = get_frequency(schedule);
2110 char *name = schtasks_task_name(frequency);
2111
2105 - get_schedule_cmd(&cmd, NULL);
2112 + get_schedule_cmd("schtasks", NULL, &cmd);
2113 strvec_split(&child.args, cmd);
2114 strvec_pushl(&child.args, "/delete", "/tn", name, "/f", NULL);
2115 free(name);
2116 + free(cmd);
2117
2118 return run_command(&child);
2119 }
@@ -2119,7 +2127,7 @@ static int schtasks_remove_tasks(void)
2127
2128 static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule)
2129 {
2122 - const char *cmd = "schtasks";
2130 + char *cmd;
2131 int result;
2132 struct child_process child = CHILD_PROCESS_INIT;
2133 const char *xml;
@@ -2129,7 +2137,7 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority
2137 struct strbuf tfilename = STRBUF_INIT;
2138 int minute = get_random_minute();
2139
2132 - get_schedule_cmd(&cmd, NULL);
2140 + get_schedule_cmd("schtasks", NULL, &cmd);
2141
2142 strbuf_addf(&tfilename, "%s/schedule_%s_XXXXXX",
2143 get_git_common_dir(), frequency);
@@ -2235,6 +2243,7 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority
2243
2244 delete_tempfile(&tfile);
2245 free(name);
2246 + free(cmd);
2247 return result;
2248 }
2249
@@ -2276,21 +2285,28 @@ static int check_crontab_process(const char *cmd)
2285
2286 static int is_crontab_available(void)
2287 {
2279 - const char *cmd = "crontab";
2288 + char *cmd;
2289 int is_available;
2290 + int ret;
2291
2282 - if (get_schedule_cmd(&cmd, &is_available))
2283 - return is_available;
2292 + if (get_schedule_cmd("crontab", &is_available, &cmd)) {
2293 + ret = is_available;
2294 + goto out;
2295 + }
2296
2297 #ifdef __APPLE__
2298 /*
2299 * macOS has cron, but it requires special permissions and will
2300 * create a UI alert when attempting to run this command.
2301 */
2290 - return 0;
2302 + ret = 0;
2303 #else
2292 - return check_crontab_process(cmd);
2304 + ret = check_crontab_process(cmd);
2305 #endif
2306 +
2307 +out:
2308 + free(cmd);
2309 + return ret;
2310 }
2311
2312 #define BEGIN_LINE "# BEGIN GIT MAINTENANCE SCHEDULE"
@@ -2298,7 +2314,7 @@ static int is_crontab_available(void)
2314
2315 static int crontab_update_schedule(int run_maintenance, int fd)
2316 {
2301 - const char *cmd = "crontab";
2317 + char *cmd;
2318 int result = 0;
2319 int in_old_region = 0;
2320 struct child_process crontab_list = CHILD_PROCESS_INIT;
@@ -2308,15 +2324,17 @@ static int crontab_update_schedule(int run_maintenance, int fd)
2324 struct tempfile *tmpedit = NULL;
2325 int minute = get_random_minute();
2326
2311 - get_schedule_cmd(&cmd, NULL);
2327 + get_schedule_cmd("crontab", NULL, &cmd);
2328 strvec_split(&crontab_list.args, cmd);
2329 strvec_push(&crontab_list.args, "-l");
2330 crontab_list.in = -1;
2331 crontab_list.out = dup(fd);
2332 crontab_list.git_cmd = 0;
2333
2318 - if (start_command(&crontab_list))
2319 - return error(_("failed to run 'crontab -l'; your system might not support 'cron'"));
2334 + if (start_command(&crontab_list)) {
2335 + result = error(_("failed to run 'crontab -l'; your system might not support 'cron'"));
2336 + goto out;
2337 + }
2338
2339 /* Ignore exit code, as an empty crontab will return error. */
2340 finish_command(&crontab_list);
@@ -2386,8 +2404,10 @@ static int crontab_update_schedule(int run_maintenance, int fd)
2404 result = error(_("'crontab' died"));
2405 else
2406 fclose(cron_list);
2407 +
2408 out:
2409 delete_tempfile(&tmpedit);
2410 + free(cmd);
2411 return result;
2412 }
2413
@@ -2410,10 +2430,9 @@ static int real_is_systemd_timer_available(void)
2430
2431 static int is_systemd_timer_available(void)
2432 {
2413 - const char *cmd = "systemctl";
2433 int is_available;
2434
2416 - if (get_schedule_cmd(&cmd, &is_available))
2435 + if (get_schedule_cmd("systemctl", &is_available, NULL))
2436 return is_available;
2437
2438 return real_is_systemd_timer_available();
@@ -2594,9 +2613,10 @@ static int systemd_timer_enable_unit(int enable,
2613 enum schedule_priority schedule,
2614 int minute)
2615 {
2597 - const char *cmd = "systemctl";
2616 + char *cmd = NULL;
2617 struct child_process child = CHILD_PROCESS_INIT;
2618 const char *frequency = get_frequency(schedule);
2619 + int ret;
2620
2621 /*
2622 * Disabling the systemd unit while it is already disabled makes
@@ -2607,20 +2627,25 @@ static int systemd_timer_enable_unit(int enable,
2627 * On the other hand, enabling a systemd unit which is already enabled
2628 * produces no error.
2629 */
2610 - if (!enable)
2630 + if (!enable) {
2631 child.no_stderr = 1;
2612 - else if (systemd_timer_write_timer_file(schedule, minute))
2613 - return -1;
2632 + } else if (systemd_timer_write_timer_file(schedule, minute)) {
2633 + ret = -1;
2634 + goto out;
2635 + }
2636
2615 - get_schedule_cmd(&cmd, NULL);
2637 + get_schedule_cmd("systemctl", NULL, &cmd);
2638 strvec_split(&child.args, cmd);
2639 strvec_pushl(&child.args, "--user", enable ? "enable" : "disable",
2640 "--now", NULL);
2641 strvec_pushf(&child.args, SYSTEMD_UNIT_FORMAT, frequency, "timer");
2642
2621 - if (start_command(&child))
2622 - return error(_("failed to start systemctl"));
2623 - if (finish_command(&child))
2643 + if (start_command(&child)) {
2644 + ret = error(_("failed to start systemctl"));
2645 + goto out;
2646 + }
2647 +
2648 + if (finish_command(&child)) {
2649 /*
2650 * Disabling an already disabled systemd unit makes
2651 * systemctl fail.
@@ -2628,9 +2653,17 @@ static int systemd_timer_enable_unit(int enable,
2653 *
2654 * Enabling an enabled systemd unit doesn't fail.
2655 */
2631 - if (enable)
2632 - return error(_("failed to run systemctl"));
2633 - return 0;
2656 + if (enable) {
2657 + ret = error(_("failed to run systemctl"));
2658 + goto out;
2659 + }
2660 + }
2661 +
2662 + ret = 0;
2663 +
2664 +out:
2665 + free(cmd);
2666 + return ret;
2667 }
2668
2669 /*
t/t7900-maintenance.sh
+1
@@ -2,6 +2,7 @@
2
3 test_description='git maintenance builtin'
4
5 +TEST_PASSES_SANITIZE_LEAK=true
6 . ./test-lib.sh
7
8 GIT_TEST_COMMIT_GRAPH=0