Volume XXII, number 279Tuesday, October 6, 2026Latest message 14 minutes ago

The Git List

News and archive of git@vger.kernel.org, since April 2005

patch, 11 partscoverity: fix unchecked returns

60 messages between Jul 14, 2026 and Aug 13, 2026, from Johannes Schindelin via GitGitGadget, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin, Jeff King.

Plain Markdown or JSON for tools and agents. Diffs are folded; open one to read it.

Johannes Schindelin via GitGitGadgetJul 14, 2026, 22:48 UTC on lore
This is the next batch of fixes in response to issues reported by Coverity.
Johannes Schindelin (11):
  http: die on curl_easy_duphandle failure in get_active_slot
  config: propagate launch_editor() failure in show_editor()
  reftable/block: check deflateInit() return value
  reftable tests: check reftable_table_init_ref_iterator() return
  last-modified: handle repo_parse_commit() failures
  compat/pread: check initial lseek for errors
  transport-helper: check dup() return in get_exporter
  transport-helper: warn when export-marks file cannot be finalized
  bisect: check strbuf_getline_lf return when reading terms
  bisect: check get_terms return at all call sites
  bisect: handle dup() failure when redirecting stdout
 bisect.c                        |  6 ++++--
 builtin/bisect.c                | 27 +++++++++++++++++++++++++--
 builtin/config.c                |  5 ++++-
 builtin/last-modified.c         |  9 ++++++---
 compat/pread.c                  |  2 ++
 http.c                          |  2 ++
 reftable/block.c                |  3 ++-
 t/unit-tests/u-reftable-table.c |  6 ++++--
 transport-helper.c              |  6 +++++-
 9 files changed, 54 insertions(+), 12 deletions(-)
base-commit: 55526a18268bbc1ddaf8a6b7850c33d984eac9e9
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2179%2Fdscho%2Fcoverity-fixes-unchecked-returns-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2179/dscho/coverity-fixes-unchecked-returns-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2179
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetJul 14, 2026, 22:48 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH 01/11] http: die on curl_easy_duphandle failure in get_active_slot

From: Johannes Schindelin <johannes.schindelin@gmx.de>

get_active_slot() duplicates the default curl handle via curl_easy_duphandle() to create a per-slot session handle. The return value is stored directly in slot->curl without checking for NULL. curl_easy_duphandle() can return NULL when memory allocation fails internally, and the libcurl documentation explicitly states this possibility.

When this happens, slot->curl is NULL and the very next operation (curl_easy_setopt on line 1632 for CURLOPT_COOKIEFILE) passes NULL as the curl handle, which is undefined behavior in libcurl and typically crashes.

Every HTTP operation in git goes through get_active_slot(), so this affects all remote-https, remote-http, and HTTP-based operations (clone, fetch, push over HTTP, bundle-uri downloads).

Add a NULL check and die() with a clear message. There is no reasonable recovery from a failed handle duplication: the process is out of memory and cannot perform any HTTP operation.

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 http.c | 2 ++
 1 file changed, 2 insertions(+)
Show changes to http.c +2 −0
diff --git a/http.c b/http.c
index b4e7b8d00b..8f1d6d1f56 100644
--- a/http.c
+++ b/http.c
@@ -1608,6 +1608,8 @@ struct active_request_slot *get_active_slot(void)
 
 	if (!slot->curl) {
 		slot->curl = curl_easy_duphandle(curl_default);
+		if (!slot->curl)
+			die("curl_easy_duphandle failed");
 		curl_session_count++;
 	}
 
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetJul 14, 2026, 22:48 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH 02/11] config: propagate launch_editor() failure in show_editor()

From: Johannes Schindelin <johannes.schindelin@gmx.de>

show_editor() calls launch_editor() to open the user's editor on the configuration file, but discards the return value and unconditionally returns 0 (success). When the editor fails to launch (e.g., $EDITOR is not found, or the editor exits with a nonzero status), the caller receives no indication that anything went wrong.

This affects "git config edit" and "git config --edit": the command silently succeeds even when the editor could not be started. In contrast, other editor-launching paths in git (such as "git commit" and "git rebase --edit-todo") properly propagate editor failures and exit with an error.

Check the return value and propagate the failure by returning -1. The two callers (cmd_config_edit at line 1315 and the legacy cmd_config at line 1478) both propagate this return to handle_builtin, which translates negative returns into an error exit.

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/config.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)
Show changes to builtin/config.c +4 −1
diff --git a/builtin/config.c b/builtin/config.c
index 8d8ec0beea..1307fdb0d6 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -1313,7 +1313,10 @@ static int show_editor(struct config_location_options *opts)
 		else if (errno != EEXIST)
 			die_errno(_("cannot create configuration file %s"), config_file);
 	}
-	launch_editor(config_file, NULL, NULL);
+	if (launch_editor(config_file, NULL, NULL)) {
+		free(config_file);
+		return -1;
+	}
 	free(config_file);
 
 	return 0;
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetJul 14, 2026, 22:48 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH 03/11] reftable/block: check deflateInit() return value

From: Johannes Schindelin <johannes.schindelin@gmx.de>

block_writer_init() allocates a z_stream and calls deflateInit() to prepare it for compressing log records. The return value of deflateInit() is silently discarded. If zlib initialization fails (e.g., Z_MEM_ERROR when the system is under memory pressure), the z_stream is left in an undefined state.

Subsequent deflate() calls in block_writer_finish() then operate on this uninitialized stream. Depending on the zlib implementation, this can produce silently corrupted compressed data (which would be written to the reftable file and discovered only when a later reader fails to inflate) or crash outright.

The function already uses REFTABLE_ZLIB_ERROR for deflate() failures later in the code path (lines 171, 199), so returning the same error code for deflateInit() failure is consistent.

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 reftable/block.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
Show changes to reftable/block.c +2 −1
diff --git a/reftable/block.c b/reftable/block.c
index 920b3f4486..ec81fd0493 100644
--- a/reftable/block.c
+++ b/reftable/block.c
@@ -87,7 +87,8 @@ int block_writer_init(struct block_writer *bw, uint8_t typ, uint8_t *block,
 		REFTABLE_CALLOC_ARRAY(bw->zstream, 1);
 		if (!bw->zstream)
 			return REFTABLE_OUT_OF_MEMORY_ERROR;
-		deflateInit(bw->zstream, 9);
+		if (deflateInit(bw->zstream, 9) != Z_OK)
+			return REFTABLE_ZLIB_ERROR;
 	}
 
 	return 0;
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetJul 14, 2026, 22:48 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH 04/11] reftable tests: check reftable_table_init_ref_iterator() return

From: Johannes Schindelin <johannes.schindelin@gmx.de>

test_reftable_table__seek_once() and test_reftable_table__reseek() both call reftable_table_init_ref_iterator() without checking its return value. This function returns an int error code (0 on success, negative on failure). Every other reftable function call in these same tests checks the return via cl_assert_equal_i() or cl_assert(), making this omission inconsistent.

If the iterator initialization ever fails (e.g., due to a memory allocation failure in the reftable internals), the test would proceed to seek and read with an uninitialized iterator, producing misleading test results or crashes rather than a clear assertion failure.

Check the return value via cl_assert_equal_i(ret, 0), consistent with the surrounding code.

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 t/unit-tests/u-reftable-table.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)
Show changes to t/unit-tests/u-reftable-table.c +4 −2
diff --git a/t/unit-tests/u-reftable-table.c b/t/unit-tests/u-reftable-table.c
index fae478ee04..6f444f8cf9 100644
--- a/t/unit-tests/u-reftable-table.c
+++ b/t/unit-tests/u-reftable-table.c
@@ -29,7 +29,8 @@ void test_reftable_table__seek_once(void)
 	ret = reftable_table_new(&table, &source, "name");
 	cl_assert(!ret);
 
-	reftable_table_init_ref_iterator(table, &it);
+	ret = reftable_table_init_ref_iterator(table, &it);
+	cl_assert_equal_i(ret, 0);
 	ret = reftable_iterator_seek_ref(&it, "");
 	cl_assert(!ret);
 	ret = reftable_iterator_next_ref(&it, &ref);
@@ -71,7 +72,8 @@ void test_reftable_table__reseek(void)
 	ret = reftable_table_new(&table, &source, "name");
 	cl_assert(!ret);
 
-	reftable_table_init_ref_iterator(table, &it);
+	ret = reftable_table_init_ref_iterator(table, &it);
+	cl_assert_equal_i(ret, 0);
 
 	for (size_t i = 0; i < 5; i++) {
 		ret = reftable_iterator_seek_ref(&it, "");
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetJul 14, 2026, 22:48 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH 05/11] last-modified: handle repo_parse_commit() failures

From: Johannes Schindelin <johannes.schindelin@gmx.de>

last_modified_run() and process_parent() call repo_parse_commit() without checking the return value at three sites. When a commit object is corrupt or unavailable (e.g., a shallow clone boundary or a missing object in a partial clone), the parse fails and the commit's internal fields (parents, tree, date) are not populated.

The consequences depend on which call site fails:

At line 417 (the main walk loop), c->parents stays NULL after a failed parse. The parent-walking loop at line 440 simply does not execute, silently treating the unparsable commit as a root commit. This produces incorrect "last modified" results: paths changed in ancestors beyond the corrupt commit are attributed to the wrong commit or not reported at all.

At line 423 (the --not exclusion walk), n->parents stays NULL, causing the exclusion walk to stop prematurely. Commits that should be excluded from the output may be incorrectly included.

At line 293 (process_parent), the parent's tree and parents are unavailable, so diff operations against it produce wrong results and the parent's own ancestors are never enqueued for walking.

Skip unparsable commits by checking the return value and continuing to the next iteration (or returning early in process_parent). This matches the defensive pattern used in other revision walkers such as limit_list() and get_revision_internal().

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/last-modified.c | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)
Show changes to builtin/last-modified.c +6 −3
diff --git a/builtin/last-modified.c b/builtin/last-modified.c
index 5478182f2e..fe012b0c2e 100644
--- a/builtin/last-modified.c
+++ b/builtin/last-modified.c
@@ -290,7 +290,8 @@ static void process_parent(struct last_modified *lm,
 {
 	struct bitmap *active_p;
 
-	repo_parse_commit(lm->rev.repo, parent);
+	if (repo_parse_commit(lm->rev.repo, parent))
+		return;
 	active_p = active_paths_for(lm, parent);
 
 	/*
@@ -414,12 +415,14 @@ static int last_modified_run(struct last_modified *lm)
 		 * Otherwise, make sure that 'c' isn't reachable from anything
 		 * in the '--not' queue.
 		 */
-		repo_parse_commit(lm->rev.repo, c);
+		if (repo_parse_commit(lm->rev.repo, c))
+			continue;
 
 		while ((n = prio_queue_get(&not_queue))) {
 			struct commit_list *np;
 
-			repo_parse_commit(lm->rev.repo, n);
+			if (repo_parse_commit(lm->rev.repo, n))
+				continue;
 
 			for (np = n->parents; np; np = np->next) {
 				if (!(np->item->object.flags & PARENT2)) {
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetJul 14, 2026, 22:48 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH 06/11] compat/pread: check initial lseek for errors

From: Johannes Schindelin <johannes.schindelin@gmx.de>

git_pread() saves the current file offset via lseek(fd, 0, SEEK_CUR) and later restores it. If the initial lseek fails (e.g., the fd is a pipe or otherwise non-seekable), current_offset is -1. This negative value is later passed to lseek(fd, -1, SEEK_SET) at line 16, which sets the file position to an unintended location (or fails with EINVAL on some platforms).

Check the initial lseek return value and return -1 immediately if it fails, consistent with the error handling for the other lseek calls in the same function.

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 compat/pread.c | 2 ++
 1 file changed, 2 insertions(+)
Show changes to compat/pread.c +2 −0
diff --git a/compat/pread.c b/compat/pread.c
index 484e6d4c71..ac7d058cb8 100644
--- a/compat/pread.c
+++ b/compat/pread.c
@@ -7,6 +7,8 @@ ssize_t git_pread(int fd, void *buf, size_t count, off_t offset)
         ssize_t rc;
 
         current_offset = lseek(fd, 0, SEEK_CUR);
+	if (current_offset < 0)
+		return -1;
 
         if (lseek(fd, offset, SEEK_SET) < 0)
                 return -1;
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetJul 14, 2026, 22:48 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH 07/11] transport-helper: check dup() return in get_exporter

From: Johannes Schindelin <johannes.schindelin@gmx.de>

get_exporter() duplicates helper->in via dup() and stores the result in fastexport->out. If dup() fails (fd exhaustion), it returns -1. The child_process machinery interprets out = -1 as "create a pipe for stdout", which would silently change the fast-export process's output wiring: instead of sending data back through the helper's input fd, it would write to a new pipe that nobody reads from.

Check the return value and report the error before proceeding.
Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 transport-helper.c | 2 ++
 1 file changed, 2 insertions(+)
Show changes to transport-helper.c +2 −0
diff --git a/transport-helper.c b/transport-helper.c
index 80f90eb7ba..31883b244e 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -487,6 +487,8 @@ static int get_exporter(struct transport *transport,
 	/* we need to duplicate helper->in because we want to use it after
 	 * fastexport is done with it. */
 	fastexport->out = dup(helper->in);
+	if (fastexport->out < 0)
+		return error_errno(_("could not dup helper output fd"));
 	strvec_push(&fastexport->args, "fast-export");
 	strvec_push(&fastexport->args, "--use-done-feature");
 	strvec_push(&fastexport->args, data->signed_tags ?
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetJul 14, 2026, 22:48 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH 08/11] transport-helper: warn when export-marks file cannot be finalized

From: Johannes Schindelin <johannes.schindelin@gmx.de>

When push_refs_with_export() finalizes a successful push, it writes the fast-export marks file to a .tmp sibling and rename()s it into place. The return value of rename() is currently ignored. If the rename fails (permission denied, full disk, or an antivirus product locking the destination on Windows), the .tmp file is left behind and the existing export_marks file remains stale; the next fast-export operation that resumes from it then silently operates on inconsistent bookkeeping.

The push itself succeeded by that point, so promoting this to a fatal error would be inappropriate. Emit warning_errno() naming both paths so the user can recover manually, and keep returning 0.

Flagged by Coverity as CID 1427723 ("Unchecked return value").
Assisted-by: Opus 4.7
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 transport-helper.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)
Show changes to transport-helper.c +3 −1
diff --git a/transport-helper.c b/transport-helper.c
index 31883b244e..ed0543f1ad 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -1184,7 +1184,9 @@ static int push_refs_with_export(struct transport *transport,
 
 	if (data->export_marks) {
 		strbuf_addf(&buf, "%s.tmp", data->export_marks);
-		rename(buf.buf, data->export_marks);
+		if (rename(buf.buf, data->export_marks))
+			warning_errno(_("could not rename '%s' to '%s'"),
+				      buf.buf, data->export_marks);
 		strbuf_release(&buf);
 	}
 
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetJul 14, 2026, 22:48 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH 09/11] bisect: check strbuf_getline_lf return when reading terms

From: Johannes Schindelin <johannes.schindelin@gmx.de>

get_terms() in builtin/bisect.c and read_bisect_terms() in bisect.c both read the BISECT_TERMS file but do not check the strbuf_getline_lf() return values. If the file is truncated (e.g., a partial write from a crash or disk-full condition), strbuf_getline_lf returns EOF and the strbuf remains empty. strbuf_detach then returns an empty string, and the term names silently become "" instead of the expected "bad"/"good" or custom terms.

In get_terms(), check for EOF and return -1 on truncation, matching the existing -1 return for a missing file.

In read_bisect_terms(), die with a descriptive message when a line cannot be read, consistent with the die_errno for a non-ENOENT open failure in the same function. Unlike get_terms(), read_bisect_terms() returns void and uses die() for all error paths, so the die is the appropriate error handling here.

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 bisect.c         |  6 ++++--
 builtin/bisect.c | 10 ++++++++--
 2 files changed, 12 insertions(+), 4 deletions(-)
Show changes to 2 files +12 −4

bisect.c, builtin/bisect.c

diff --git a/bisect.c b/bisect.c
index 94c7028d2a..c2ef5da462 100644
--- a/bisect.c
+++ b/bisect.c
@@ -1019,10 +1019,12 @@ void read_bisect_terms(char **read_bad, char **read_good)
 			die_errno(_("could not read file '%s'"), filename);
 		}
 	} else {
-		strbuf_getline_lf(&str, fp);
+		if (strbuf_getline_lf(&str, fp) == EOF)
+			die(_("could not read bad term from file '%s'"), filename);
 		free(*read_bad);
 		*read_bad = strbuf_detach(&str, NULL);
-		strbuf_getline_lf(&str, fp);
+		if (strbuf_getline_lf(&str, fp) == EOF)
+			die(_("could not read good term from file '%s'"), filename);
 		free(*read_good);
 		*read_good = strbuf_detach(&str, NULL);
 	}
diff --git a/builtin/bisect.c b/builtin/bisect.c
index 798e28f501..fe66d84382 100644
--- a/builtin/bisect.c
+++ b/builtin/bisect.c
@@ -498,9 +498,15 @@ static int get_terms(struct bisect_terms *terms)
 	}
 
 	free_terms(terms);
-	strbuf_getline_lf(&str, fp);
+	if (strbuf_getline_lf(&str, fp) == EOF) {
+		res = -1;
+		goto finish;
+	}
 	terms->term_bad = strbuf_detach(&str, NULL);
-	strbuf_getline_lf(&str, fp);
+	if (strbuf_getline_lf(&str, fp) == EOF) {
+		res = -1;
+		goto finish;
+	}
 	terms->term_good = strbuf_detach(&str, NULL);
 
 finish:
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetJul 14, 2026, 22:48 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH 10/11] bisect: check get_terms return at all call sites

From: Johannes Schindelin <johannes.schindelin@gmx.de>

Six callers of get_terms() silently discard its return value. When get_terms fails (missing or truncated BISECT_TERMS file), the term strings remain NULL or empty, causing confusing downstream behavior: commands like "bisect next" or "bisect run" proceed with empty term strings, producing nonsensical ref names (refs/bisect/ with no suffix) and misleading error messages.

Add checks at each call site so that a failed get_terms produces a clear "no terms defined" error, matching the pattern already used in bisect_terms() at line 512. The check tests the term pointers rather than the return value because some callers (bisect skip, legacy bad/good) call set_terms before get_terms, and the set_terms values should survive a get_terms failure.

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/bisect.c | 12 ++++++++++++
 1 file changed, 12 insertions(+)
Show changes to builtin/bisect.c +12 −0
diff --git a/builtin/bisect.c b/builtin/bisect.c
index fe66d84382..15a2a30f89 100644
--- a/builtin/bisect.c
+++ b/builtin/bisect.c
@@ -1057,6 +1057,8 @@ static int process_replay_line(struct bisect_terms *terms, struct strbuf *line)
 	*word_end = '\0'; /* NUL-terminate the word */
 
 	get_terms(terms);
+	if (!terms->term_bad || !terms->term_good)
+		return error(_("no terms defined"));
 	if (check_and_set_terms(terms, p))
 		return -1;
 
@@ -1383,6 +1385,8 @@ static int cmd_bisect__next(int argc, const char **argv UNUSED, const char *pref
 		return error(_("'%s' requires 0 arguments"),
 			     "git bisect next");
 	get_terms(&terms);
+	if (!terms.term_bad || !terms.term_good)
+		return error(_("no terms defined"));
 	res = bisect_next(&terms, prefix);
 	free_terms(&terms);
 	return res;
@@ -1417,6 +1421,8 @@ static int cmd_bisect__skip(int argc, const char **argv, const char *prefix UNUS
 
 	set_terms(&terms, "bad", "good");
 	get_terms(&terms);
+	if (!terms.term_bad || !terms.term_good)
+		return error(_("no terms defined"));
 	res = bisect_skip(&terms, argc, argv);
 	free_terms(&terms);
 	return res;
@@ -1429,6 +1435,8 @@ static int cmd_bisect__visualize(int argc, const char **argv, const char *prefix
 	struct bisect_terms terms = { 0 };
 
 	get_terms(&terms);
+	if (!terms.term_bad || !terms.term_good)
+		return error(_("no terms defined"));
 	res = bisect_visualize(&terms, argc, argv);
 	free_terms(&terms);
 	return res;
@@ -1443,6 +1451,8 @@ static int cmd_bisect__run(int argc, const char **argv, const char *prefix UNUSE
 	if (!argc)
 		return error(_("'%s' failed: no command provided."), "git bisect run");
 	get_terms(&terms);
+	if (!terms.term_bad || !terms.term_good)
+		return error(_("no terms defined"));
 	res = bisect_run(&terms, argc, argv);
 	free_terms(&terms);
 	return res;
@@ -1482,6 +1492,8 @@ int cmd_bisect(int argc,
 
 		set_terms(&terms, "bad", "good");
 		get_terms(&terms);
+		if (!terms.term_bad || !terms.term_good)
+			return error(_("no terms defined"));
 		if (check_and_set_terms(&terms, argv[0]) ||
 		    !one_of(argv[0], terms.term_good, terms.term_bad, NULL))
 			usage_msg_optf(_("unknown command: '%s'"), git_bisect_usage,
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetJul 14, 2026, 22:48 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH 11/11] bisect: handle dup() failure when redirecting stdout

From: Johannes Schindelin <johannes.schindelin@gmx.de>

To capture the output of each verdict command, bisect_run() temporarily redirects stdout to a temporary file via the classic dup(1) / dup2() pair, restoring it afterwards. The return value of dup(1) is not checked, however. When it fails, the saved descriptor is -1, which is then passed to close() (the issue Coverity flags), and the matching dup2() that is meant to restore stdout also fails, leaving the process with stdout still pointing at the temporary file for the remainder of the run.

Treat a failed dup(1) as a fatal error for this bisect step: close the temporary file descriptor, report the error via error_errno(), and break out of the loop so the existing cleanup path handles the rest, just as on other failure paths in this function.

Reported by Coverity as CID 1508242 ("Improper use of negative value").

Assisted-by: Opus 4.7
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/bisect.c | 5 +++++
 1 file changed, 5 insertions(+)
Show changes to builtin/bisect.c +5 −0
diff --git a/builtin/bisect.c b/builtin/bisect.c
index 15a2a30f89..801daf8c78 100644
--- a/builtin/bisect.c
+++ b/builtin/bisect.c
@@ -1308,6 +1308,11 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv)
 
 		fflush(stdout);
 		saved_stdout = dup(1);
+		if (saved_stdout < 0) {
+			res = error_errno(_("could not duplicate stdout"));
+			close(temporary_stdout_fd);
+			break;
+		}
 		dup2(temporary_stdout_fd, 1);
 
 		res = bisect_state(terms, 1, &new_state);
-- 
gitgitgadget
Junio C HamanoJul 15, 2026, 01:15 UTC in reply to Johannes Schindelin via GitGitGadget on lore

Re: [PATCH 05/11] last-modified: handle repo_parse_commit() failures

"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com> writes:

Show 12 quoted lines
> Skip unparsable commits by checking the return value and
> continuing to the next iteration (or returning early in
> process_parent). This matches the defensive pattern used in other
> revision walkers such as limit_list() and get_revision_internal().
> ...
> @@ -414,12 +415,14 @@ static int last_modified_run(struct last_modified *lm)
>  		 * Otherwise, make sure that 'c' isn't reachable from anything
>  		 * in the '--not' queue.
>  		 */
> -		repo_parse_commit(lm->rev.repo, c);
> +		if (repo_parse_commit(lm->rev.repo, c))
> +			continue;
Shouldn't this be
			goto cleanup;

instead? 'n' pulled out of not_queue may be unparseable and when we ignore it, don't we still want to clean up the active_paths slab for commit 'c'?

Show 9 quoted lines
>  		while ((n = prio_queue_get(&not_queue))) {
>  			struct commit_list *np;
>  
> -			repo_parse_commit(lm->rev.repo, n);
> +			if (repo_parse_commit(lm->rev.repo, n))
> +				continue;
>  
>  			for (np = n->parents; np; np = np->next) {
>  				if (!(np->item->object.flags & PARENT2)) {
Junio C HamanoJul 15, 2026, 01:17 UTC in reply to Johannes Schindelin via GitGitGadget on lore

Re: [PATCH 09/11] bisect: check strbuf_getline_lf return when reading terms

"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com> writes:

Show 19 quoted lines
> diff --git a/builtin/bisect.c b/builtin/bisect.c
> index 798e28f501..fe66d84382 100644
> --- a/builtin/bisect.c
> +++ b/builtin/bisect.c
> @@ -498,9 +498,15 @@ static int get_terms(struct bisect_terms *terms)
>  	}
>  
>  	free_terms(terms);
> -	strbuf_getline_lf(&str, fp);
> +	if (strbuf_getline_lf(&str, fp) == EOF) {
> +		res = -1;
> +		goto finish;
> +	}
>  	terms->term_bad = strbuf_detach(&str, NULL);
> -	strbuf_getline_lf(&str, fp);
> +	if (strbuf_getline_lf(&str, fp) == EOF) {
> +		res = -1;
> +		goto finish;
> +	}

We want to clean-up terms->term_bad when we fail to read the second line after reading the first line successfully, no?

>  	terms->term_good = strbuf_detach(&str, NULL);
>  
>  finish:
Patrick SteinhardtJul 15, 2026, 06:58 UTC in reply to Johannes Schindelin via GitGitGadget on lore

Re: [PATCH 02/11] config: propagate launch_editor() failure in show_editor()

On Tue, Jul 14, 2026 at 10:48:35PM +0000, Johannes Schindelin via GitGitGadget wrote:
Show 13 quoted lines
> diff --git a/builtin/config.c b/builtin/config.c
> index 8d8ec0beea..1307fdb0d6 100644
> --- a/builtin/config.c
> +++ b/builtin/config.c
> @@ -1313,7 +1313,10 @@ static int show_editor(struct config_location_options *opts)
>  		else if (errno != EEXIST)
>  			die_errno(_("cannot create configuration file %s"), config_file);
>  	}
> -	launch_editor(config_file, NULL, NULL);
> +	if (launch_editor(config_file, NULL, NULL)) {
> +		free(config_file);
> +		return -1;
> +	}

All error paths in `launch_editor()` already print an error message, so we indeed don't have to do anything but bubble up the error here.

Patrick
Patrick SteinhardtJul 15, 2026, 06:58 UTC in reply to Johannes Schindelin via GitGitGadget on lore

Re: [PATCH 06/11] compat/pread: check initial lseek for errors

On Tue, Jul 14, 2026 at 10:48:39PM +0000, Johannes Schindelin via GitGitGadget wrote:
Show 13 quoted lines
> diff --git a/compat/pread.c b/compat/pread.c
> index 484e6d4c71..ac7d058cb8 100644
> --- a/compat/pread.c
> +++ b/compat/pread.c
> @@ -7,6 +7,8 @@ ssize_t git_pread(int fd, void *buf, size_t count, off_t offset)
>          ssize_t rc;
>  
>          current_offset = lseek(fd, 0, SEEK_CUR);
> +	if (current_offset < 0)
> +		return -1;
>  
>          if (lseek(fd, offset, SEEK_SET) < 0)
>                  return -1;

Heh, funny. I wanted to complain about misindentation here, but your new code is actually indented correctly. It's everything else in this file that is indented with spaces.

Patrick
Patrick SteinhardtJul 15, 2026, 06:58 UTC in reply to Johannes Schindelin via GitGitGadget on lore

Re: [PATCH 07/11] transport-helper: check dup() return in get_exporter

On Tue, Jul 14, 2026 at 10:48:40PM +0000, Johannes Schindelin via GitGitGadget wrote:
Show 13 quoted lines
> diff --git a/transport-helper.c b/transport-helper.c
> index 80f90eb7ba..31883b244e 100644
> --- a/transport-helper.c
> +++ b/transport-helper.c
> @@ -487,6 +487,8 @@ static int get_exporter(struct transport *transport,
>  	/* we need to duplicate helper->in because we want to use it after
>  	 * fastexport is done with it. */
>  	fastexport->out = dup(helper->in);
> +	if (fastexport->out < 0)
> +		return error_errno(_("could not dup helper output fd"));
>  	strvec_push(&fastexport->args, "fast-export");
>  	strvec_push(&fastexport->args, "--use-done-feature");
>  	strvec_push(&fastexport->args, data->signed_tags ?

Makes sense. The only caller already knows to die in case it sees a non-zero return value.

Patrick
Patrick SteinhardtJul 15, 2026, 06:58 UTC in reply to Johannes Schindelin via GitGitGadget on lore

Re: [PATCH 10/11] bisect: check get_terms return at all call sites

On Tue, Jul 14, 2026 at 10:48:43PM +0000, Johannes Schindelin via GitGitGadget wrote:
Show 15 quoted lines
> From: Johannes Schindelin <johannes.schindelin@gmx.de>
> 
> Six callers of get_terms() silently discard its return value. When
> get_terms fails (missing or truncated BISECT_TERMS file), the term
> strings remain NULL or empty, causing confusing downstream
> behavior: commands like "bisect next" or "bisect run" proceed with
> empty term strings, producing nonsensical ref names (refs/bisect/
> with no suffix) and misleading error messages.
> 
> Add checks at each call site so that a failed get_terms produces a
> clear "no terms defined" error, matching the pattern already used
> in bisect_terms() at line 512. The check tests the term pointers
> rather than the return value because some callers (bisect skip,
> legacy bad/good) call set_terms before get_terms, and the
> set_terms values should survive a get_terms failure.

Hm. Are there any callers that accept the case where either `term->bad` or `term->good` are `NULL`? If not, should we maybe adapt the function itself to return an error if so and then have all callers only ever check for the return value of `get_term()` instead of also having to check the result? That might also allow us to deduplicate the error messages.

Patrick
Patrick SteinhardtJul 15, 2026, 06:58 UTC in reply to Johannes Schindelin via GitGitGadget on lore

Re: [PATCH 11/11] bisect: handle dup() failure when redirecting stdout

On Tue, Jul 14, 2026 at 10:48:44PM +0000, Johannes Schindelin via GitGitGadget wrote:
Show 14 quoted lines
> diff --git a/builtin/bisect.c b/builtin/bisect.c
> index 15a2a30f89..801daf8c78 100644
> --- a/builtin/bisect.c
> +++ b/builtin/bisect.c
> @@ -1308,6 +1308,11 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv)
>  
>  		fflush(stdout);
>  		saved_stdout = dup(1);
> +		if (saved_stdout < 0) {
> +			res = error_errno(_("could not duplicate stdout"));
> +			close(temporary_stdout_fd);
> +			break;
> +		}
>  		dup2(temporary_stdout_fd, 1);
Shouldn't we also verify the return value of `dup2()` while at it?
Patrick
Junio C HamanoJul 20, 2026, 05:09 UTC in reply to Junio C Hamano on lore

Re: [PATCH 05/11] last-modified: handle repo_parse_commit() failures

Junio C Hamano <gitster@pobox.com> writes:
Show 14 quoted lines
> "Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
> writes:
> ...
>> -		repo_parse_commit(lm->rev.repo, c);
>> +		if (repo_parse_commit(lm->rev.repo, c))
>> +			continue;
>
> Shouldn't this be
>
> 			goto cleanup;
>
> instead?  'n' pulled out of not_queue may be unparseable and when we
> ignore it, don't we still want to clean up the active_paths slab for
> commit 'c'?
--- >8 ---
Subject: [PATCH] fixup! last-modified: handle repo_parse_commit() failures
https://lore.kernel.org/git/xmqqldbdqciy.fsf@gitster.g/

'n' pulled out of not_queue may be unparseable and when we ignore it, we still want to clean up the active_paths slab for commit 'c'.

Show changes to builtin/last-modified.c +1 −1
diff --git a/builtin/last-modified.c b/builtin/last-modified.c
index fe012b0c2e..3846244dfc 100644
--- a/builtin/last-modified.c
+++ b/builtin/last-modified.c
@@ -416,7 +416,7 @@ static int last_modified_run(struct last_modified *lm)
 		 * in the '--not' queue.
 		 */
 		if (repo_parse_commit(lm->rev.repo, c))
-			continue;
+			goto cleanup;
 
 		while ((n = prio_queue_get(&not_queue))) {
 			struct commit_list *np;
Junio C HamanoJul 20, 2026, 05:09 UTC in reply to Junio C Hamano on lore

Re: [PATCH 09/11] bisect: check strbuf_getline_lf return when reading terms

Junio C Hamano <gitster@pobox.com> writes:
Show 29 quoted lines
> "Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
> writes:
> ...
>> diff --git a/builtin/bisect.c b/builtin/bisect.c
>> index 798e28f501..fe66d84382 100644
>> --- a/builtin/bisect.c
>> +++ b/builtin/bisect.c
>> @@ -498,9 +498,15 @@ static int get_terms(struct bisect_terms *terms)
>>  	}
>>  
>>  	free_terms(terms);
>> -	strbuf_getline_lf(&str, fp);
>> +	if (strbuf_getline_lf(&str, fp) == EOF) {
>> +		res = -1;
>> +		goto finish;
>> +	}
>>  	terms->term_bad = strbuf_detach(&str, NULL);
>> -	strbuf_getline_lf(&str, fp);
>> +	if (strbuf_getline_lf(&str, fp) == EOF) {
>> +		res = -1;
>> +		goto finish;
>> +	}
>
> We want to clean-up terms->term_bad when we fail to read the second
> line after reading the first line successfully, no?
>
>>  	terms->term_good = strbuf_detach(&str, NULL);
>>  
>>  finish:
--- >8 ---
Subject: [PATCH] fixup! bisect: check strbuf_getline_lf return when reading
 terms
https://lore.kernel.org/git/xmqqh5m1qcfh.fsf@gitster.g/
This fixes the immediate leak introduced by
https://lore.kernel.org/git/17c382fdf46eada79ce03a7604dd7e0454d8bea4.1784069325.git.gitgitgadget@gmail.com/

but many callers of get_terms() should all be fixed to check for return value. If it fails to grab the replacement word for "bad", both terms->term_bad and terms->term_good are left NULL, since the function calls free_terms() early.

Show changes to builtin/bisect.c +1 −0
diff --git a/builtin/bisect.c b/builtin/bisect.c
index fe66d84382..69ab7ea248 100644
--- a/builtin/bisect.c
+++ b/builtin/bisect.c
@@ -505,6 +505,7 @@ static int get_terms(struct bisect_terms *terms)
 	terms->term_bad = strbuf_detach(&str, NULL);
 	if (strbuf_getline_lf(&str, fp) == EOF) {
 		res = -1;
+		FREE_AND_NULL(terms->term_bad);
 		goto finish;
 	}
 	terms->term_good = strbuf_detach(&str, NULL);
Johannes SchindelinAug 5, 2026, 14:27 UTC in reply to Junio C Hamano on lore

Re: [PATCH 05/11] last-modified: handle repo_parse_commit() failures

Hi Junio,
On Sun, 19 Jul 2026, Junio C Hamano wrote:
Show 16 quoted lines
> Junio C Hamano <gitster@pobox.com> writes:
> 
> > "Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
> > writes:
> > ...
> >> -		repo_parse_commit(lm->rev.repo, c);
> >> +		if (repo_parse_commit(lm->rev.repo, c))
> >> +			continue;
> >
> > Shouldn't this be
> >
> > 			goto cleanup;
> >
> > instead?  'n' pulled out of not_queue may be unparseable and when we
> > ignore it, don't we still want to clean up the active_paths slab for
> > commit 'c'?
Correct.

Thanks, Johannes

Show 23 quoted lines
> 
> --- >8 ---
> Subject: [PATCH] fixup! last-modified: handle repo_parse_commit() failures
> 
> https://lore.kernel.org/git/xmqqldbdqciy.fsf@gitster.g/
> 
> 'n' pulled out of not_queue may be unparseable and when we ignore
> it, we still want to clean up the active_paths slab for commit 'c'.
> 
> diff --git a/builtin/last-modified.c b/builtin/last-modified.c
> index fe012b0c2e..3846244dfc 100644
> --- a/builtin/last-modified.c
> +++ b/builtin/last-modified.c
> @@ -416,7 +416,7 @@ static int last_modified_run(struct last_modified *lm)
>  		 * in the '--not' queue.
>  		 */
>  		if (repo_parse_commit(lm->rev.repo, c))
> -			continue;
> +			goto cleanup;
>  
>  		while ((n = prio_queue_get(&not_queue))) {
>  			struct commit_list *np;
> 
Johannes SchindelinAug 5, 2026, 14:29 UTC in reply to Patrick Steinhardt on lore

Re: [PATCH 06/11] compat/pread: check initial lseek for errors

Hi Patrick,
On Wed, 15 Jul 2026, Patrick Steinhardt wrote:
Show 18 quoted lines
> On Tue, Jul 14, 2026 at 10:48:39PM +0000, Johannes Schindelin via GitGitGadget wrote:
> > diff --git a/compat/pread.c b/compat/pread.c
> > index 484e6d4c71..ac7d058cb8 100644
> > --- a/compat/pread.c
> > +++ b/compat/pread.c
> > @@ -7,6 +7,8 @@ ssize_t git_pread(int fd, void *buf, size_t count, off_t offset)
> >          ssize_t rc;
> >  
> >          current_offset = lseek(fd, 0, SEEK_CUR);
> > +	if (current_offset < 0)
> > +		return -1;
> >  
> >          if (lseek(fd, offset, SEEK_SET) < 0)
> >                  return -1;
> 
> Heh, funny. I wanted to complain about misindentation here, but your new
> code is actually indented correctly. It's everything else in this file
> that is indented with spaces.

Heh. I did notice something odd going on, thinking that Opus ignored my clear instructions about tab-indentation once again when I replaced the spaces by tabs...

Ciao, Johannes

Johannes SchindelinAug 5, 2026, 14:33 UTC in reply to Junio C Hamano on lore

Re: [PATCH 09/11] bisect: check strbuf_getline_lf return when reading terms

Hi Junio,
On Sun, 19 Jul 2026, Junio C Hamano wrote:
Show 56 quoted lines
> Junio C Hamano <gitster@pobox.com> writes:
> 
> > "Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
> > writes:
> > ...
> >> diff --git a/builtin/bisect.c b/builtin/bisect.c
> >> index 798e28f501..fe66d84382 100644
> >> --- a/builtin/bisect.c
> >> +++ b/builtin/bisect.c
> >> @@ -498,9 +498,15 @@ static int get_terms(struct bisect_terms *terms)
> >>  	}
> >>  
> >>  	free_terms(terms);
> >> -	strbuf_getline_lf(&str, fp);
> >> +	if (strbuf_getline_lf(&str, fp) == EOF) {
> >> +		res = -1;
> >> +		goto finish;
> >> +	}
> >>  	terms->term_bad = strbuf_detach(&str, NULL);
> >> -	strbuf_getline_lf(&str, fp);
> >> +	if (strbuf_getline_lf(&str, fp) == EOF) {
> >> +		res = -1;
> >> +		goto finish;
> >> +	}
> >
> > We want to clean-up terms->term_bad when we fail to read the second
> > line after reading the first line successfully, no?
> >
> >>  	terms->term_good = strbuf_detach(&str, NULL);
> >>  
> >>  finish:
> 
> --- >8 ---
> Subject: [PATCH] fixup! bisect: check strbuf_getline_lf return when reading
>  terms
> 
> https://lore.kernel.org/git/xmqqh5m1qcfh.fsf@gitster.g/
> 
> This fixes the immediate leak introduced by
> 
> https://lore.kernel.org/git/17c382fdf46eada79ce03a7604dd7e0454d8bea4.1784069325.git.gitgitgadget@gmail.com/
> 
> but many callers of get_terms() should all be fixed to check for
> return value.  If it fails to grab the replacement word for "bad",
> both terms->term_bad and terms->term_good are left NULL, since the
> function calls free_terms() early.
> 
> diff --git a/builtin/bisect.c b/builtin/bisect.c
> index fe66d84382..69ab7ea248 100644
> --- a/builtin/bisect.c
> +++ b/builtin/bisect.c
> @@ -505,6 +505,7 @@ static int get_terms(struct bisect_terms *terms)
>  	terms->term_bad = strbuf_detach(&str, NULL);
>  	if (strbuf_getline_lf(&str, fp) == EOF) {
>  		res = -1;
> +		FREE_AND_NULL(terms->term_bad);
Good catch!

Thank you, Johannes

>  		goto finish;
>  	}
>  	terms->term_good = strbuf_detach(&str, NULL);
> 
Johannes SchindelinAug 5, 2026, 14:57 UTC in reply to Patrick Steinhardt on lore

Re: [PATCH 10/11] bisect: check get_terms return at all call sites

Hi Patrick,
On Wed, 15 Jul 2026, Patrick Steinhardt wrote:
Show 19 quoted lines
> On Tue, Jul 14, 2026 at 10:48:43PM +0000, Johannes Schindelin via GitGitGadget wrote:
> > From: Johannes Schindelin <johannes.schindelin@gmx.de>
> > 
> > Six callers of get_terms() silently discard its return value. When
> > get_terms fails (missing or truncated BISECT_TERMS file), the term
> > strings remain NULL or empty, causing confusing downstream
> > behavior: commands like "bisect next" or "bisect run" proceed with
> > empty term strings, producing nonsensical ref names (refs/bisect/
> > with no suffix) and misleading error messages.
> > 
> > Add checks at each call site so that a failed get_terms produces a
> > clear "no terms defined" error, matching the pattern already used
> > in bisect_terms() at line 512. The check tests the term pointers
> > rather than the return value because some callers (bisect skip,
> > legacy bad/good) call set_terms before get_terms, and the
> > set_terms values should survive a get_terms failure.
> 
> Hm. Are there any callers that accept the case where either `term->bad`
> or `term->good` are `NULL`?

As far as I can tell, no, the case where either `term->bad` or `term->good` are `NULL` is not permissible.

> If not, should we maybe adapt the function itself to return an error if
> so and then have all callers only ever check for the return value of
> `get_term()` instead of also having to check the result? That might also
> allow us to deduplicate the error messages.

It's a good point that we should not look at `term->bad` and `term->good`, but at the return value of `get_term()` instead. That's incidentally what `bisect_terms()` does, and we should do the same here (including the same, already-translated error message).

Thanks, Johannes

Johannes SchindelinAug 5, 2026, 16:44 UTC in reply to Patrick Steinhardt on lore

Re: [PATCH 11/11] bisect: handle dup() failure when redirecting stdout

Hi Patrick,
On Wed, 15 Jul 2026, Patrick Steinhardt wrote:
Show 17 quoted lines
> On Tue, Jul 14, 2026 at 10:48:44PM +0000, Johannes Schindelin via GitGitGadget wrote:
> > diff --git a/builtin/bisect.c b/builtin/bisect.c
> > index 15a2a30f89..801daf8c78 100644
> > --- a/builtin/bisect.c
> > +++ b/builtin/bisect.c
> > @@ -1308,6 +1308,11 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv)
> >  
> >  		fflush(stdout);
> >  		saved_stdout = dup(1);
> > +		if (saved_stdout < 0) {
> > +			res = error_errno(_("could not duplicate stdout"));
> > +			close(temporary_stdout_fd);
> > +			break;
> > +		}
> >  		dup2(temporary_stdout_fd, 1);
> 
> Shouldn't we also verify the return value of `dup2()` while at it?

True. I wonder why Coverity didn't complain... funny. I changed it to also check the return value of `dup2()`.

Thank you for your review! Johannes

Johannes Schindelin via GitGitGadgetAug 5, 2026, 18:30 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v2 00/11] coverity: fix unchecked returns

This is the next batch of fixes in response to issues reported by Coverity.
Changes since v1:
 * The last-modified patch is now more careful to clean up a commit slab
   when parsing the commit failed.
 * When the "good" bisect term was read successfully, but not the "bad" one,
   the "good" one is now cleaned up.
 * Instead of detecting failed get_terms() calls indirectly, the return
   value is now checked.
 * Failures when bisect_run() calls dup2() are now handled properly, too.
Johannes Schindelin (11):
  http: die on curl_easy_duphandle failure in get_active_slot
  config: propagate launch_editor() failure in show_editor()
  reftable/block: check deflateInit() return value
  reftable tests: check reftable_table_init_ref_iterator() return
  last-modified: handle repo_parse_commit() failures
  compat/pread: check initial lseek for errors
  transport-helper: check dup() return in get_exporter
  transport-helper: warn when export-marks file cannot be finalized
  bisect: check strbuf_getline_lf return when reading terms
  bisect: check get_terms return at all call sites
  bisect: handle dup() failure when redirecting stdout
 bisect.c                        |  6 +++--
 builtin/bisect.c                | 42 +++++++++++++++++++++++----------
 builtin/config.c                |  5 +++-
 builtin/last-modified.c         |  9 ++++---
 compat/pread.c                  |  2 ++
 http.c                          |  2 ++
 reftable/block.c                |  3 ++-
 t/unit-tests/u-reftable-table.c |  6 +++--
 transport-helper.c              |  6 ++++-
 9 files changed, 59 insertions(+), 22 deletions(-)
base-commit: 55526a18268bbc1ddaf8a6b7850c33d984eac9e9
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2179%2Fdscho%2Fcoverity-fixes-unchecked-returns-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2179/dscho/coverity-fixes-unchecked-returns-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/2179
Range-diff vs v1:
  1:  e653255de1 =  1:  e653255de1 http: die on curl_easy_duphandle failure in get_active_slot
  2:  0692704d45 =  2:  0692704d45 config: propagate launch_editor() failure in show_editor()
  3:  9bf7e737c7 =  3:  9bf7e737c7 reftable/block: check deflateInit() return value
  4:  711671c3ab =  4:  711671c3ab reftable tests: check reftable_table_init_ref_iterator() return
  5:  f728be4dac !  5:  72a74c76be last-modified: handle repo_parse_commit() failures
     @@ Commit message
          Pointed out by Coverity.
      
          Assisted-by: Claude Opus 4.6
     +    Helped-by: Junio C Hamano <gitster@pobox.com>
          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
      
       ## builtin/last-modified.c ##
     @@ builtin/last-modified.c: static int last_modified_run(struct last_modified *lm)
       		 */
      -		repo_parse_commit(lm->rev.repo, c);
      +		if (repo_parse_commit(lm->rev.repo, c))
     -+			continue;
     ++			goto cleanup;
       
       		while ((n = prio_queue_get(&not_queue))) {
       			struct commit_list *np;
  6:  b31e0326e7 =  6:  f0b1e13979 compat/pread: check initial lseek for errors
  7:  1792042098 =  7:  0facb9e8ca transport-helper: check dup() return in get_exporter
  8:  13ddcce053 =  8:  2b0e4f32fd transport-helper: warn when export-marks file cannot be finalized
  9:  17c382fdf4 !  9:  7f2b963103 bisect: check strbuf_getline_lf return when reading terms
     @@ Commit message
          Pointed out by Coverity.
      
          Assisted-by: Claude Opus 4.6
     +    Helped-by: Junio C Hamano <gitster@pobox.com>
          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
      
       ## bisect.c ##
     @@ builtin/bisect.c: static int get_terms(struct bisect_terms *terms)
      -	strbuf_getline_lf(&str, fp);
      +	if (strbuf_getline_lf(&str, fp) == EOF) {
      +		res = -1;
     ++		FREE_AND_NULL(terms->term_bad);
      +		goto finish;
      +	}
       	terms->term_good = strbuf_detach(&str, NULL);
 10:  c0827a7947 ! 10:  9a9103096a bisect: check get_terms return at all call sites
     @@ Commit message
          empty term strings, producing nonsensical ref names (refs/bisect/
          with no suffix) and misleading error messages.
      
     -    Add checks at each call site so that a failed get_terms produces a
     -    clear "no terms defined" error, matching the pattern already used
     -    in bisect_terms() at line 512. The check tests the term pointers
     -    rather than the return value because some callers (bisect skip,
     -    legacy bad/good) call set_terms before get_terms, and the
     -    set_terms values should survive a get_terms failure.
     +    Let's not discard the return value, but handle an error with the same
     +    message `bisect_terms()` already uses when reading the terms failed.
      
          Pointed out by Coverity.
      
     +    There is one slight complication here: One caller _needs_ the return
     +    value to indicate an error when the `BISECT_TERMS` file is absent, all
     +    the other call sites are totally okay with a "missing" `BISECT_TERMS`
     +    file. To address that, extend the function signature of `get_terms()` to
     +    indicate which behavior the caller wants.
     +
          Assisted-by: Claude Opus 4.6
     +    Helped-by: Patrick Steinhardt <ps@pks.im>
          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
      
       ## builtin/bisect.c ##
     +@@ builtin/bisect.c: static int bisect_next_check(const struct bisect_terms *terms,
     + 	return decide_next(terms, current_term, !state.nr_good, !state.nr_bad);
     + }
     + 
     +-static int get_terms(struct bisect_terms *terms)
     ++static int get_terms(struct bisect_terms *terms, int file_missing_is_ok)
     + {
     + 	struct strbuf str = STRBUF_INIT;
     + 	FILE *fp = NULL;
     +@@ builtin/bisect.c: static int get_terms(struct bisect_terms *terms)
     + 
     + 	fp = fopen(git_path_bisect_terms(), "r");
     + 	if (!fp) {
     +-		res = -1;
     ++		res = file_missing_is_ok ? 0 : -1;
     + 		goto finish;
     + 	}
     + 
     +@@ builtin/bisect.c: finish:
     + 
     + static int bisect_terms(struct bisect_terms *terms, const char *option)
     + {
     +-	if (get_terms(terms))
     ++	if (get_terms(terms, 0))
     + 		return error(_("no terms defined"));
     + 
     + 	if (!option) {
      @@ builtin/bisect.c: static int process_replay_line(struct bisect_terms *terms, struct strbuf *line)
     + 	rev = word_end + strspn(word_end, " \t");
       	*word_end = '\0'; /* NUL-terminate the word */
       
     - 	get_terms(terms);
     -+	if (!terms->term_bad || !terms->term_good)
     +-	get_terms(terms);
     ++	if (get_terms(terms, 1))
      +		return error(_("no terms defined"));
       	if (check_and_set_terms(terms, p))
       		return -1;
       
      @@ builtin/bisect.c: static int cmd_bisect__next(int argc, const char **argv UNUSED, const char *pref
     + 	if (argc)
       		return error(_("'%s' requires 0 arguments"),
       			     "git bisect next");
     - 	get_terms(&terms);
     -+	if (!terms.term_bad || !terms.term_good)
     +-	get_terms(&terms);
     ++	if (get_terms(&terms, 1))
      +		return error(_("no terms defined"));
       	res = bisect_next(&terms, prefix);
       	free_terms(&terms);
       	return res;
      @@ builtin/bisect.c: static int cmd_bisect__skip(int argc, const char **argv, const char *prefix UNUS
     + 	struct bisect_terms terms = { 0 };
       
       	set_terms(&terms, "bad", "good");
     - 	get_terms(&terms);
     -+	if (!terms.term_bad || !terms.term_good)
     +-	get_terms(&terms);
     ++	if (get_terms(&terms, 1))
      +		return error(_("no terms defined"));
       	res = bisect_skip(&terms, argc, argv);
       	free_terms(&terms);
       	return res;
      @@ builtin/bisect.c: static int cmd_bisect__visualize(int argc, const char **argv, const char *prefix
     + 	int res;
       	struct bisect_terms terms = { 0 };
       
     - 	get_terms(&terms);
     -+	if (!terms.term_bad || !terms.term_good)
     +-	get_terms(&terms);
     ++	if (get_terms(&terms, 1))
      +		return error(_("no terms defined"));
       	res = bisect_visualize(&terms, argc, argv);
       	free_terms(&terms);
       	return res;
      @@ builtin/bisect.c: static int cmd_bisect__run(int argc, const char **argv, const char *prefix UNUSE
     + 
       	if (!argc)
       		return error(_("'%s' failed: no command provided."), "git bisect run");
     - 	get_terms(&terms);
     -+	if (!terms.term_bad || !terms.term_good)
     +-	get_terms(&terms);
     ++	if (get_terms(&terms, 1))
      +		return error(_("no terms defined"));
       	res = bisect_run(&terms, argc, argv);
       	free_terms(&terms);
       	return res;
      @@ builtin/bisect.c: int cmd_bisect(int argc,
     + 			usage_with_options(git_bisect_usage, options);
       
       		set_terms(&terms, "bad", "good");
     - 		get_terms(&terms);
     -+		if (!terms.term_bad || !terms.term_good)
     +-		get_terms(&terms);
     ++		if (get_terms(&terms, 1))
      +			return error(_("no terms defined"));
       		if (check_and_set_terms(&terms, argv[0]) ||
       		    !one_of(argv[0], terms.term_good, terms.term_bad, NULL))
 11:  2da452e39c ! 11:  829cd82177 bisect: handle dup() failure when redirecting stdout
     @@ Commit message
          leaving the process with stdout still pointing at the temporary file
          for the remainder of the run.
      
     -    Treat a failed dup(1) as a fatal error for this bisect step: close
     -    the temporary file descriptor, report the error via error_errno(),
     -    and break out of the loop so the existing cleanup path handles the
     -    rest, just as on other failure paths in this function.
     +    Treat a failed dup(1) or dup2(..., 1) as a fatal error for this bisect
     +    step: close the temporary file descriptor, report the error via
     +    error_errno(), and break out of the loop so the existing cleanup path
     +    handles the rest, just as on other failure paths in this function.
      
          Reported by Coverity as CID 1508242 ("Improper use of negative
          value").
      
          Assisted-by: Opus 4.7
     +    Helped-by: Patrick Steinhardt <ps@pks.im>
          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
      
       ## builtin/bisect.c ##
     @@ builtin/bisect.c: static int bisect_run(struct bisect_terms *terms, int argc, co
       
       		fflush(stdout);
       		saved_stdout = dup(1);
     -+		if (saved_stdout < 0) {
     +-		dup2(temporary_stdout_fd, 1);
     ++		if (saved_stdout < 0 ||
     ++		    dup2(temporary_stdout_fd, 1) < 0) {
      +			res = error_errno(_("could not duplicate stdout"));
      +			close(temporary_stdout_fd);
      +			break;
      +		}
     - 		dup2(temporary_stdout_fd, 1);
       
       		res = bisect_state(terms, 1, &new_state);
     + 
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 5, 2026, 18:30 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v2 01/11] http: die on curl_easy_duphandle failure in get_active_slot

From: Johannes Schindelin <johannes.schindelin@gmx.de>

get_active_slot() duplicates the default curl handle via curl_easy_duphandle() to create a per-slot session handle. The return value is stored directly in slot->curl without checking for NULL. curl_easy_duphandle() can return NULL when memory allocation fails internally, and the libcurl documentation explicitly states this possibility.

When this happens, slot->curl is NULL and the very next operation (curl_easy_setopt on line 1632 for CURLOPT_COOKIEFILE) passes NULL as the curl handle, which is undefined behavior in libcurl and typically crashes.

Every HTTP operation in git goes through get_active_slot(), so this affects all remote-https, remote-http, and HTTP-based operations (clone, fetch, push over HTTP, bundle-uri downloads).

Add a NULL check and die() with a clear message. There is no reasonable recovery from a failed handle duplication: the process is out of memory and cannot perform any HTTP operation.

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 http.c | 2 ++
 1 file changed, 2 insertions(+)
Show changes to http.c +2 −0
diff --git a/http.c b/http.c
index b4e7b8d00b..8f1d6d1f56 100644
--- a/http.c
+++ b/http.c
@@ -1608,6 +1608,8 @@ struct active_request_slot *get_active_slot(void)
 
 	if (!slot->curl) {
 		slot->curl = curl_easy_duphandle(curl_default);
+		if (!slot->curl)
+			die("curl_easy_duphandle failed");
 		curl_session_count++;
 	}
 
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 5, 2026, 18:30 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v2 02/11] config: propagate launch_editor() failure in show_editor()

From: Johannes Schindelin <johannes.schindelin@gmx.de>

show_editor() calls launch_editor() to open the user's editor on the configuration file, but discards the return value and unconditionally returns 0 (success). When the editor fails to launch (e.g., $EDITOR is not found, or the editor exits with a nonzero status), the caller receives no indication that anything went wrong.

This affects "git config edit" and "git config --edit": the command silently succeeds even when the editor could not be started. In contrast, other editor-launching paths in git (such as "git commit" and "git rebase --edit-todo") properly propagate editor failures and exit with an error.

Check the return value and propagate the failure by returning -1. The two callers (cmd_config_edit at line 1315 and the legacy cmd_config at line 1478) both propagate this return to handle_builtin, which translates negative returns into an error exit.

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/config.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)
Show changes to builtin/config.c +4 −1
diff --git a/builtin/config.c b/builtin/config.c
index 8d8ec0beea..1307fdb0d6 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -1313,7 +1313,10 @@ static int show_editor(struct config_location_options *opts)
 		else if (errno != EEXIST)
 			die_errno(_("cannot create configuration file %s"), config_file);
 	}
-	launch_editor(config_file, NULL, NULL);
+	if (launch_editor(config_file, NULL, NULL)) {
+		free(config_file);
+		return -1;
+	}
 	free(config_file);
 
 	return 0;
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 5, 2026, 18:30 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v2 03/11] reftable/block: check deflateInit() return value

From: Johannes Schindelin <johannes.schindelin@gmx.de>

block_writer_init() allocates a z_stream and calls deflateInit() to prepare it for compressing log records. The return value of deflateInit() is silently discarded. If zlib initialization fails (e.g., Z_MEM_ERROR when the system is under memory pressure), the z_stream is left in an undefined state.

Subsequent deflate() calls in block_writer_finish() then operate on this uninitialized stream. Depending on the zlib implementation, this can produce silently corrupted compressed data (which would be written to the reftable file and discovered only when a later reader fails to inflate) or crash outright.

The function already uses REFTABLE_ZLIB_ERROR for deflate() failures later in the code path (lines 171, 199), so returning the same error code for deflateInit() failure is consistent.

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 reftable/block.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
Show changes to reftable/block.c +2 −1
diff --git a/reftable/block.c b/reftable/block.c
index 920b3f4486..ec81fd0493 100644
--- a/reftable/block.c
+++ b/reftable/block.c
@@ -87,7 +87,8 @@ int block_writer_init(struct block_writer *bw, uint8_t typ, uint8_t *block,
 		REFTABLE_CALLOC_ARRAY(bw->zstream, 1);
 		if (!bw->zstream)
 			return REFTABLE_OUT_OF_MEMORY_ERROR;
-		deflateInit(bw->zstream, 9);
+		if (deflateInit(bw->zstream, 9) != Z_OK)
+			return REFTABLE_ZLIB_ERROR;
 	}
 
 	return 0;
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 5, 2026, 18:30 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v2 04/11] reftable tests: check reftable_table_init_ref_iterator() return

From: Johannes Schindelin <johannes.schindelin@gmx.de>

test_reftable_table__seek_once() and test_reftable_table__reseek() both call reftable_table_init_ref_iterator() without checking its return value. This function returns an int error code (0 on success, negative on failure). Every other reftable function call in these same tests checks the return via cl_assert_equal_i() or cl_assert(), making this omission inconsistent.

If the iterator initialization ever fails (e.g., due to a memory allocation failure in the reftable internals), the test would proceed to seek and read with an uninitialized iterator, producing misleading test results or crashes rather than a clear assertion failure.

Check the return value via cl_assert_equal_i(ret, 0), consistent with the surrounding code.

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 t/unit-tests/u-reftable-table.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)
Show changes to t/unit-tests/u-reftable-table.c +4 −2
diff --git a/t/unit-tests/u-reftable-table.c b/t/unit-tests/u-reftable-table.c
index fae478ee04..6f444f8cf9 100644
--- a/t/unit-tests/u-reftable-table.c
+++ b/t/unit-tests/u-reftable-table.c
@@ -29,7 +29,8 @@ void test_reftable_table__seek_once(void)
 	ret = reftable_table_new(&table, &source, "name");
 	cl_assert(!ret);
 
-	reftable_table_init_ref_iterator(table, &it);
+	ret = reftable_table_init_ref_iterator(table, &it);
+	cl_assert_equal_i(ret, 0);
 	ret = reftable_iterator_seek_ref(&it, "");
 	cl_assert(!ret);
 	ret = reftable_iterator_next_ref(&it, &ref);
@@ -71,7 +72,8 @@ void test_reftable_table__reseek(void)
 	ret = reftable_table_new(&table, &source, "name");
 	cl_assert(!ret);
 
-	reftable_table_init_ref_iterator(table, &it);
+	ret = reftable_table_init_ref_iterator(table, &it);
+	cl_assert_equal_i(ret, 0);
 
 	for (size_t i = 0; i < 5; i++) {
 		ret = reftable_iterator_seek_ref(&it, "");
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 5, 2026, 18:30 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v2 05/11] last-modified: handle repo_parse_commit() failures

From: Johannes Schindelin <johannes.schindelin@gmx.de>

last_modified_run() and process_parent() call repo_parse_commit() without checking the return value at three sites. When a commit object is corrupt or unavailable (e.g., a shallow clone boundary or a missing object in a partial clone), the parse fails and the commit's internal fields (parents, tree, date) are not populated.

The consequences depend on which call site fails:

At line 417 (the main walk loop), c->parents stays NULL after a failed parse. The parent-walking loop at line 440 simply does not execute, silently treating the unparsable commit as a root commit. This produces incorrect "last modified" results: paths changed in ancestors beyond the corrupt commit are attributed to the wrong commit or not reported at all.

At line 423 (the --not exclusion walk), n->parents stays NULL, causing the exclusion walk to stop prematurely. Commits that should be excluded from the output may be incorrectly included.

At line 293 (process_parent), the parent's tree and parents are unavailable, so diff operations against it produce wrong results and the parent's own ancestors are never enqueued for walking.

Skip unparsable commits by checking the return value and continuing to the next iteration (or returning early in process_parent). This matches the defensive pattern used in other revision walkers such as limit_list() and get_revision_internal().

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Helped-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/last-modified.c | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)
Show changes to builtin/last-modified.c +6 −3
diff --git a/builtin/last-modified.c b/builtin/last-modified.c
index 5478182f2e..3846244dfc 100644
--- a/builtin/last-modified.c
+++ b/builtin/last-modified.c
@@ -290,7 +290,8 @@ static void process_parent(struct last_modified *lm,
 {
 	struct bitmap *active_p;
 
-	repo_parse_commit(lm->rev.repo, parent);
+	if (repo_parse_commit(lm->rev.repo, parent))
+		return;
 	active_p = active_paths_for(lm, parent);
 
 	/*
@@ -414,12 +415,14 @@ static int last_modified_run(struct last_modified *lm)
 		 * Otherwise, make sure that 'c' isn't reachable from anything
 		 * in the '--not' queue.
 		 */
-		repo_parse_commit(lm->rev.repo, c);
+		if (repo_parse_commit(lm->rev.repo, c))
+			goto cleanup;
 
 		while ((n = prio_queue_get(&not_queue))) {
 			struct commit_list *np;
 
-			repo_parse_commit(lm->rev.repo, n);
+			if (repo_parse_commit(lm->rev.repo, n))
+				continue;
 
 			for (np = n->parents; np; np = np->next) {
 				if (!(np->item->object.flags & PARENT2)) {
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 5, 2026, 18:30 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v2 06/11] compat/pread: check initial lseek for errors

From: Johannes Schindelin <johannes.schindelin@gmx.de>

git_pread() saves the current file offset via lseek(fd, 0, SEEK_CUR) and later restores it. If the initial lseek fails (e.g., the fd is a pipe or otherwise non-seekable), current_offset is -1. This negative value is later passed to lseek(fd, -1, SEEK_SET) at line 16, which sets the file position to an unintended location (or fails with EINVAL on some platforms).

Check the initial lseek return value and return -1 immediately if it fails, consistent with the error handling for the other lseek calls in the same function.

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 compat/pread.c | 2 ++
 1 file changed, 2 insertions(+)
Show changes to compat/pread.c +2 −0
diff --git a/compat/pread.c b/compat/pread.c
index 484e6d4c71..ac7d058cb8 100644
--- a/compat/pread.c
+++ b/compat/pread.c
@@ -7,6 +7,8 @@ ssize_t git_pread(int fd, void *buf, size_t count, off_t offset)
         ssize_t rc;
 
         current_offset = lseek(fd, 0, SEEK_CUR);
+	if (current_offset < 0)
+		return -1;
 
         if (lseek(fd, offset, SEEK_SET) < 0)
                 return -1;
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 5, 2026, 18:30 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v2 07/11] transport-helper: check dup() return in get_exporter

From: Johannes Schindelin <johannes.schindelin@gmx.de>

get_exporter() duplicates helper->in via dup() and stores the result in fastexport->out. If dup() fails (fd exhaustion), it returns -1. The child_process machinery interprets out = -1 as "create a pipe for stdout", which would silently change the fast-export process's output wiring: instead of sending data back through the helper's input fd, it would write to a new pipe that nobody reads from.

Check the return value and report the error before proceeding.
Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 transport-helper.c | 2 ++
 1 file changed, 2 insertions(+)
Show changes to transport-helper.c +2 −0
diff --git a/transport-helper.c b/transport-helper.c
index 80f90eb7ba..31883b244e 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -487,6 +487,8 @@ static int get_exporter(struct transport *transport,
 	/* we need to duplicate helper->in because we want to use it after
 	 * fastexport is done with it. */
 	fastexport->out = dup(helper->in);
+	if (fastexport->out < 0)
+		return error_errno(_("could not dup helper output fd"));
 	strvec_push(&fastexport->args, "fast-export");
 	strvec_push(&fastexport->args, "--use-done-feature");
 	strvec_push(&fastexport->args, data->signed_tags ?
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 5, 2026, 18:30 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v2 08/11] transport-helper: warn when export-marks file cannot be finalized

From: Johannes Schindelin <johannes.schindelin@gmx.de>

When push_refs_with_export() finalizes a successful push, it writes the fast-export marks file to a .tmp sibling and rename()s it into place. The return value of rename() is currently ignored. If the rename fails (permission denied, full disk, or an antivirus product locking the destination on Windows), the .tmp file is left behind and the existing export_marks file remains stale; the next fast-export operation that resumes from it then silently operates on inconsistent bookkeeping.

The push itself succeeded by that point, so promoting this to a fatal error would be inappropriate. Emit warning_errno() naming both paths so the user can recover manually, and keep returning 0.

Flagged by Coverity as CID 1427723 ("Unchecked return value").
Assisted-by: Opus 4.7
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 transport-helper.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)
Show changes to transport-helper.c +3 −1
diff --git a/transport-helper.c b/transport-helper.c
index 31883b244e..ed0543f1ad 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -1184,7 +1184,9 @@ static int push_refs_with_export(struct transport *transport,
 
 	if (data->export_marks) {
 		strbuf_addf(&buf, "%s.tmp", data->export_marks);
-		rename(buf.buf, data->export_marks);
+		if (rename(buf.buf, data->export_marks))
+			warning_errno(_("could not rename '%s' to '%s'"),
+				      buf.buf, data->export_marks);
 		strbuf_release(&buf);
 	}
 
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 5, 2026, 18:30 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v2 09/11] bisect: check strbuf_getline_lf return when reading terms

From: Johannes Schindelin <johannes.schindelin@gmx.de>

get_terms() in builtin/bisect.c and read_bisect_terms() in bisect.c both read the BISECT_TERMS file but do not check the strbuf_getline_lf() return values. If the file is truncated (e.g., a partial write from a crash or disk-full condition), strbuf_getline_lf returns EOF and the strbuf remains empty. strbuf_detach then returns an empty string, and the term names silently become "" instead of the expected "bad"/"good" or custom terms.

In get_terms(), check for EOF and return -1 on truncation, matching the existing -1 return for a missing file.

In read_bisect_terms(), die with a descriptive message when a line cannot be read, consistent with the die_errno for a non-ENOENT open failure in the same function. Unlike get_terms(), read_bisect_terms() returns void and uses die() for all error paths, so the die is the appropriate error handling here.

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Helped-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 bisect.c         |  6 ++++--
 builtin/bisect.c | 11 +++++++++--
 2 files changed, 13 insertions(+), 4 deletions(-)
Show changes to 2 files +13 −4

bisect.c, builtin/bisect.c

diff --git a/bisect.c b/bisect.c
index 94c7028d2a..c2ef5da462 100644
--- a/bisect.c
+++ b/bisect.c
@@ -1019,10 +1019,12 @@ void read_bisect_terms(char **read_bad, char **read_good)
 			die_errno(_("could not read file '%s'"), filename);
 		}
 	} else {
-		strbuf_getline_lf(&str, fp);
+		if (strbuf_getline_lf(&str, fp) == EOF)
+			die(_("could not read bad term from file '%s'"), filename);
 		free(*read_bad);
 		*read_bad = strbuf_detach(&str, NULL);
-		strbuf_getline_lf(&str, fp);
+		if (strbuf_getline_lf(&str, fp) == EOF)
+			die(_("could not read good term from file '%s'"), filename);
 		free(*read_good);
 		*read_good = strbuf_detach(&str, NULL);
 	}
diff --git a/builtin/bisect.c b/builtin/bisect.c
index 798e28f501..69ab7ea248 100644
--- a/builtin/bisect.c
+++ b/builtin/bisect.c
@@ -498,9 +498,16 @@ static int get_terms(struct bisect_terms *terms)
 	}
 
 	free_terms(terms);
-	strbuf_getline_lf(&str, fp);
+	if (strbuf_getline_lf(&str, fp) == EOF) {
+		res = -1;
+		goto finish;
+	}
 	terms->term_bad = strbuf_detach(&str, NULL);
-	strbuf_getline_lf(&str, fp);
+	if (strbuf_getline_lf(&str, fp) == EOF) {
+		res = -1;
+		FREE_AND_NULL(terms->term_bad);
+		goto finish;
+	}
 	terms->term_good = strbuf_detach(&str, NULL);
 
 finish:
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 5, 2026, 18:30 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v2 10/11] bisect: check get_terms return at all call sites

From: Johannes Schindelin <johannes.schindelin@gmx.de>

Six callers of get_terms() silently discard its return value. When get_terms fails (missing or truncated BISECT_TERMS file), the term strings remain NULL or empty, causing confusing downstream behavior: commands like "bisect next" or "bisect run" proceed with empty term strings, producing nonsensical ref names (refs/bisect/ with no suffix) and misleading error messages.

Let's not discard the return value, but handle an error with the same message `bisect_terms()` already uses when reading the terms failed.

Pointed out by Coverity.

There is one slight complication here: One caller _needs_ the return value to indicate an error when the `BISECT_TERMS` file is absent, all the other call sites are totally okay with a "missing" `BISECT_TERMS` file. To address that, extend the function signature of `get_terms()` to indicate which behavior the caller wants.

Assisted-by: Claude Opus 4.6
Helped-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/bisect.c | 24 +++++++++++++++---------
 1 file changed, 15 insertions(+), 9 deletions(-)
Show changes to builtin/bisect.c +15 −9
diff --git a/builtin/bisect.c b/builtin/bisect.c
index 69ab7ea248..ceb60b0626 100644
--- a/builtin/bisect.c
+++ b/builtin/bisect.c
@@ -485,7 +485,7 @@ static int bisect_next_check(const struct bisect_terms *terms,
 	return decide_next(terms, current_term, !state.nr_good, !state.nr_bad);
 }
 
-static int get_terms(struct bisect_terms *terms)
+static int get_terms(struct bisect_terms *terms, int file_missing_is_ok)
 {
 	struct strbuf str = STRBUF_INIT;
 	FILE *fp = NULL;
@@ -493,7 +493,7 @@ static int get_terms(struct bisect_terms *terms)
 
 	fp = fopen(git_path_bisect_terms(), "r");
 	if (!fp) {
-		res = -1;
+		res = file_missing_is_ok ? 0 : -1;
 		goto finish;
 	}
 
@@ -519,7 +519,7 @@ finish:
 
 static int bisect_terms(struct bisect_terms *terms, const char *option)
 {
-	if (get_terms(terms))
+	if (get_terms(terms, 0))
 		return error(_("no terms defined"));
 
 	if (!option) {
@@ -1057,7 +1057,8 @@ static int process_replay_line(struct bisect_terms *terms, struct strbuf *line)
 	rev = word_end + strspn(word_end, " \t");
 	*word_end = '\0'; /* NUL-terminate the word */
 
-	get_terms(terms);
+	if (get_terms(terms, 1))
+		return error(_("no terms defined"));
 	if (check_and_set_terms(terms, p))
 		return -1;
 
@@ -1383,7 +1384,8 @@ static int cmd_bisect__next(int argc, const char **argv UNUSED, const char *pref
 	if (argc)
 		return error(_("'%s' requires 0 arguments"),
 			     "git bisect next");
-	get_terms(&terms);
+	if (get_terms(&terms, 1))
+		return error(_("no terms defined"));
 	res = bisect_next(&terms, prefix);
 	free_terms(&terms);
 	return res;
@@ -1417,7 +1419,8 @@ static int cmd_bisect__skip(int argc, const char **argv, const char *prefix UNUS
 	struct bisect_terms terms = { 0 };
 
 	set_terms(&terms, "bad", "good");
-	get_terms(&terms);
+	if (get_terms(&terms, 1))
+		return error(_("no terms defined"));
 	res = bisect_skip(&terms, argc, argv);
 	free_terms(&terms);
 	return res;
@@ -1429,7 +1432,8 @@ static int cmd_bisect__visualize(int argc, const char **argv, const char *prefix
 	int res;
 	struct bisect_terms terms = { 0 };
 
-	get_terms(&terms);
+	if (get_terms(&terms, 1))
+		return error(_("no terms defined"));
 	res = bisect_visualize(&terms, argc, argv);
 	free_terms(&terms);
 	return res;
@@ -1443,7 +1447,8 @@ static int cmd_bisect__run(int argc, const char **argv, const char *prefix UNUSE
 
 	if (!argc)
 		return error(_("'%s' failed: no command provided."), "git bisect run");
-	get_terms(&terms);
+	if (get_terms(&terms, 1))
+		return error(_("no terms defined"));
 	res = bisect_run(&terms, argc, argv);
 	free_terms(&terms);
 	return res;
@@ -1482,7 +1487,8 @@ int cmd_bisect(int argc,
 			usage_with_options(git_bisect_usage, options);
 
 		set_terms(&terms, "bad", "good");
-		get_terms(&terms);
+		if (get_terms(&terms, 1))
+			return error(_("no terms defined"));
 		if (check_and_set_terms(&terms, argv[0]) ||
 		    !one_of(argv[0], terms.term_good, terms.term_bad, NULL))
 			usage_msg_optf(_("unknown command: '%s'"), git_bisect_usage,
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 5, 2026, 18:31 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v2 11/11] bisect: handle dup() failure when redirecting stdout

From: Johannes Schindelin <johannes.schindelin@gmx.de>

To capture the output of each verdict command, bisect_run() temporarily redirects stdout to a temporary file via the classic dup(1) / dup2() pair, restoring it afterwards. The return value of dup(1) is not checked, however. When it fails, the saved descriptor is -1, which is then passed to close() (the issue Coverity flags), and the matching dup2() that is meant to restore stdout also fails, leaving the process with stdout still pointing at the temporary file for the remainder of the run.

Treat a failed dup(1) or dup2(..., 1) as a fatal error for this bisect step: close the temporary file descriptor, report the error via error_errno(), and break out of the loop so the existing cleanup path handles the rest, just as on other failure paths in this function.

Reported by Coverity as CID 1508242 ("Improper use of negative value").

Assisted-by: Opus 4.7
Helped-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/bisect.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)
Show changes to builtin/bisect.c +6 −1
diff --git a/builtin/bisect.c b/builtin/bisect.c
index ceb60b0626..733d28d377 100644
--- a/builtin/bisect.c
+++ b/builtin/bisect.c
@@ -1308,7 +1308,12 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv)
 
 		fflush(stdout);
 		saved_stdout = dup(1);
-		dup2(temporary_stdout_fd, 1);
+		if (saved_stdout < 0 ||
+		    dup2(temporary_stdout_fd, 1) < 0) {
+			res = error_errno(_("could not duplicate stdout"));
+			close(temporary_stdout_fd);
+			break;
+		}
 
 		res = bisect_state(terms, 1, &new_state);
 
-- 
gitgitgadget
Junio C HamanoAug 5, 2026, 20:26 UTC in reply to Johannes Schindelin via GitGitGadget on lore

Re: [PATCH v2 10/11] bisect: check get_terms return at all call sites

"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com> writes:

Show 5 quoted lines
> There is one slight complication here: One caller _needs_ the return
> value to indicate an error when the `BISECT_TERMS` file is absent, all
> the other call sites are totally okay with a "missing" `BISECT_TERMS`
> file. To address that, extend the function signature of `get_terms()` to
> indicate which behavior the caller wants.
Show 13 quoted lines
> -static int get_terms(struct bisect_terms *terms)
> +static int get_terms(struct bisect_terms *terms, int file_missing_is_ok)
>  {
>  	struct strbuf str = STRBUF_INIT;
>  	FILE *fp = NULL;
> @@ -493,7 +493,7 @@ static int get_terms(struct bisect_terms *terms)
>  
>  	fp = fopen(git_path_bisect_terms(), "r");
>  	if (!fp) {
> -		res = -1;
> +		res = file_missing_is_ok ? 0 : -1;
>  		goto finish;
>  	}

Hmph. So, depending on the caller, a missing file error may have to be treated as OK or as an error, while all other kinds of anomalies are treated by all callers as errors.

As all the existing callsites of this function need to be adjusted for this change anyway, I would have thought a more typical way to handle a situation like this would be to define different error codes for this function and have the callers deal with them. But it seems that almost all callers, except for one, pass "missing is OK."

So, instead of adjusting the majority of callers with something like:
        -       if (get_terms(...))
        +       if (get_terms(...) == BISECT_TERMS_ERROR)
                        oops we got an error

and keeping only the single oddball caller to barf on any non-zero return,

        -       if (get_terms(...))
        +       switch (get_terms(...)) {
	+	case BISECT_TERMS_ERROR:
                        oops we got an error
	+		break;
	+	case BISECT_TERMS_MISSING_FILE:
	+		deal with the missing file error
	+		break;
	+	default:
	+		break; /* ok */
	+	}
it may be simpler to change:
        -       if (get_terms(...))
        +       if (get_terms(..., 1))
                        oops we got an error
for the majority of them.  The one oddball caller then becomes:
        -       if (get_terms(...))
        +       if (get_terms(..., 0))
                        oops we got an error
to treat a missing file as an error as well.
I guess I can buy that.

If get_terms() were a public function that had many more callers, my preference would probably be very different. But this is local to a single file, so the meaning of the mysterious 0/1 parameter will quickly become evident to those who have to work with this part of the system anyway.

Thanks.
Junio C HamanoAug 6, 2026, 01:11 UTC in reply to Johannes Schindelin via GitGitGadget on lore

Re: [PATCH v2 03/11] reftable/block: check deflateInit() return value

"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com> writes:

Show 24 quoted lines
> The function already uses REFTABLE_ZLIB_ERROR for deflate()
> failures later in the code path (lines 171, 199), so returning
> the same error code for deflateInit() failure is consistent.
>
> Pointed out by Coverity.
>
> Assisted-by: Claude Opus 4.6
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
>  reftable/block.c | 3 ++-
>  1 file changed, 2 insertions(+), 1 deletion(-)
>
> diff --git a/reftable/block.c b/reftable/block.c
> index 920b3f4486..ec81fd0493 100644
> --- a/reftable/block.c
> +++ b/reftable/block.c
> @@ -87,7 +87,8 @@ int block_writer_init(struct block_writer *bw, uint8_t typ, uint8_t *block,
>  		REFTABLE_CALLOC_ARRAY(bw->zstream, 1);
>  		if (!bw->zstream)
>  			return REFTABLE_OUT_OF_MEMORY_ERROR;
> -		deflateInit(bw->zstream, 9);
> +		if (deflateInit(bw->zstream, 9) != Z_OK)
> +			return REFTABLE_ZLIB_ERROR;
>  	}

Presumably bw->zstream occupies some memory allocated on the heap. Does a failing deflateInit() release it? If not, do we leak memory here? Or do we need

		if (deflateInit(bw->zstream, 9) !+ Z_OK) {
			REFTABLE_FREE_AND_NULL(bw->zstream);
			return REFTABLE_ZLIB_ERROR;
		}
here?

Noticing and returning an error is a good first step. The only caller of it is reftable/writer.c:writer_reinit_block_writer(), and it checks and relays the error code from here to its callers, but not all callers of it check the error condition. The most blatant offender being reftable_writer_new() that happily keeps going. I do not know if we end up calling zlib on bw->zstream for such a broken block_writer(), as I didn't trace the call graph fully myself.

Stepping back a bit, if REFTABLE_CALLOC_ARRAY() fails, bw->zstream would be NULL, and a caller that does not check the return value of writer_reinit_block_writer() would be holding a block writer whose zstream is NULL. If the block writer is eventually passed to the block_writer_release() function, we would call deflateEnd() on it.

Jeff KingAug 6, 2026, 15:41 UTC in reply to Johannes Schindelin via GitGitGadget on lore

Re: [PATCH v2 11/11] bisect: handle dup() failure when redirecting stdout

On Wed, Aug 05, 2026 at 06:31:00PM +0000, Johannes Schindelin via GitGitGadget wrote:
Show 15 quoted lines
> diff --git a/builtin/bisect.c b/builtin/bisect.c
> index ceb60b0626..733d28d377 100644
> --- a/builtin/bisect.c
> +++ b/builtin/bisect.c
> @@ -1308,7 +1308,12 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv)
>  
>  		fflush(stdout);
>  		saved_stdout = dup(1);
> -		dup2(temporary_stdout_fd, 1);
> +		if (saved_stdout < 0 ||
> +		    dup2(temporary_stdout_fd, 1) < 0) {
> +			res = error_errno(_("could not duplicate stdout"));
> +			close(temporary_stdout_fd);
> +			break;
> +		}
Ironically this produces a new Coverity complaint. ;)
If dup2() fails, then we break out of the loop, leaking saved_stdout.
-Peff
Junio C HamanoAug 6, 2026, 17:31 UTC in reply to Jeff King on lore

Re: [PATCH v2 11/11] bisect: handle dup() failure when redirecting stdout

Jeff King <peff@peff.net> writes:
Show 21 quoted lines
> On Wed, Aug 05, 2026 at 06:31:00PM +0000, Johannes Schindelin via GitGitGadget wrote:
>
>> diff --git a/builtin/bisect.c b/builtin/bisect.c
>> index ceb60b0626..733d28d377 100644
>> --- a/builtin/bisect.c
>> +++ b/builtin/bisect.c
>> @@ -1308,7 +1308,12 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv)
>>  
>>  		fflush(stdout);
>>  		saved_stdout = dup(1);
>> -		dup2(temporary_stdout_fd, 1);
>> +		if (saved_stdout < 0 ||
>> +		    dup2(temporary_stdout_fd, 1) < 0) {
>> +			res = error_errno(_("could not duplicate stdout"));
>> +			close(temporary_stdout_fd);
>> +			break;
>> +		}
>
> Ironically this produces a new Coverity complaint. ;)
>
> If dup2() fails, then we break out of the loop, leaking saved_stdout.

I didn't notice it while I was looking at this part, and wondering if we can (and should) do anything if close() failed there.

Johannes Schindelin via GitGitGadgetAug 12, 2026, 08:03 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v3 00/12] coverity: fix unchecked returns

This is the next batch of fixes in response to issues reported by Coverity.
Changes since v2:
 * Added a new commit to handle block-writer initialization errors (instead
   of ignoring them).
 * The bw->zstream attribute is now also deinitialized in the error case, as
   suggested by Junio.
 * The commit message of "reftable/block: check deflateInit() return value"
   was rephrased to stop suggesting that silent corruption by zlib would be
   possible before that patch: This turned out to be provably incorrect.
 * When aborting the bisect because dup2() failed, a left-over saved_stdout
   is now also cleaned up.
Changes since v1:
 * The last-modified patch is now more careful to clean up a commit slab
   when parsing the commit failed.
 * When the "good" bisect term was read successfully, but not the "bad" one,
   the "good" one is now cleaned up.
 * Instead of detecting failed get_terms() calls indirectly, the return
   value is now checked.
 * Failures when bisect_run() calls dup2() are now handled properly, too.
Johannes Schindelin (12):
  http: die on curl_easy_duphandle failure in get_active_slot
  config: propagate launch_editor() failure in show_editor()
  reftable: handle block-writer initialization errors
  reftable/block: check deflateInit() return value
  reftable tests: check reftable_table_init_ref_iterator() return
  last-modified: handle repo_parse_commit() failures
  compat/pread: check initial lseek for errors
  transport-helper: check dup() return in get_exporter
  transport-helper: warn when export-marks file cannot be finalized
  bisect: check strbuf_getline_lf return when reading terms
  bisect: check get_terms return at all call sites
  bisect: handle dup() failure when redirecting stdout
 bisect.c                        |  6 +++--
 builtin/bisect.c                | 44 ++++++++++++++++++++++++---------
 builtin/config.c                |  5 +++-
 builtin/last-modified.c         |  9 ++++---
 compat/pread.c                  |  2 ++
 http.c                          |  2 ++
 reftable/block.c                |  5 +++-
 reftable/writer.c               |  8 +++++-
 t/unit-tests/u-reftable-table.c |  6 +++--
 transport-helper.c              |  6 ++++-
 10 files changed, 70 insertions(+), 23 deletions(-)
base-commit: 55526a18268bbc1ddaf8a6b7850c33d984eac9e9
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2179%2Fdscho%2Fcoverity-fixes-unchecked-returns-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2179/dscho/coverity-fixes-unchecked-returns-v3
Pull-Request: https://github.com/gitgitgadget/git/pull/2179
Range-diff vs v2:
  1:  e653255de1 =  1:  e653255de1 http: die on curl_easy_duphandle failure in get_active_slot
  2:  0692704d45 =  2:  0692704d45 config: propagate launch_editor() failure in show_editor()
  -:  ---------- >  3:  c689148aef reftable: handle block-writer initialization errors
  3:  9bf7e737c7 !  4:  66953a65d0 reftable/block: check deflateInit() return value
     @@ Commit message
          z_stream is left in an undefined state.
      
          Subsequent deflate() calls in block_writer_finish() then operate
     -    on this uninitialized stream. Depending on the zlib
     -    implementation, this can produce silently corrupted compressed
     -    data (which would be written to the reftable file and discovered
     -    only when a later reader fails to inflate) or crash outright.
     +    on this uninitialized stream. Current zlib/zlib-ng versions handle
     +    such a stream gracefully, by returning `Z_STREAM_ERROR`, so in
     +    practice it would likely not result in catastrophic error.
      
     -    The function already uses REFTABLE_ZLIB_ERROR for deflate()
     -    failures later in the code path (lines 171, 199), so returning
     -    the same error code for deflateInit() failure is consistent.
     +    The function already uses REFTABLE_ZLIB_ERROR for deflate() failures
     +    later in the code path, so returning the same error code for
     +    deflateInit() failure is consistent.
      
          Pointed out by Coverity.
      
          Assisted-by: Claude Opus 4.6
     +    Helped-by: Junio C Hamano <gitster@pobox.com>
          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
      
       ## reftable/block.c ##
     @@ reftable/block.c: int block_writer_init(struct block_writer *bw, uint8_t typ, ui
       		if (!bw->zstream)
       			return REFTABLE_OUT_OF_MEMORY_ERROR;
      -		deflateInit(bw->zstream, 9);
     -+		if (deflateInit(bw->zstream, 9) != Z_OK)
     ++		if (deflateInit(bw->zstream, 9) != Z_OK) {
     ++			REFTABLE_FREE_AND_NULL(bw->zstream);
      +			return REFTABLE_ZLIB_ERROR;
     ++		}
       	}
       
       	return 0;
  4:  711671c3ab =  5:  a49af20d30 reftable tests: check reftable_table_init_ref_iterator() return
  5:  72a74c76be =  6:  bf06239732 last-modified: handle repo_parse_commit() failures
  6:  f0b1e13979 =  7:  6e2295b8f0 compat/pread: check initial lseek for errors
  7:  0facb9e8ca =  8:  689bb48fe5 transport-helper: check dup() return in get_exporter
  8:  2b0e4f32fd =  9:  ad6ea19737 transport-helper: warn when export-marks file cannot be finalized
  9:  7f2b963103 = 10:  7db6ac2ab0 bisect: check strbuf_getline_lf return when reading terms
 10:  9a9103096a = 11:  aefdbe2bdf bisect: check get_terms return at all call sites
 11:  829cd82177 ! 12:  258dbb0fbd bisect: handle dup() failure when redirecting stdout
     @@ builtin/bisect.c: static int bisect_run(struct bisect_terms *terms, int argc, co
      +		if (saved_stdout < 0 ||
      +		    dup2(temporary_stdout_fd, 1) < 0) {
      +			res = error_errno(_("could not duplicate stdout"));
     ++			if (saved_stdout >= 0)
     ++				close(saved_stdout);
      +			close(temporary_stdout_fd);
      +			break;
      +		}
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 12, 2026, 08:03 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v3 01/12] http: die on curl_easy_duphandle failure in get_active_slot

From: Johannes Schindelin <johannes.schindelin@gmx.de>

get_active_slot() duplicates the default curl handle via curl_easy_duphandle() to create a per-slot session handle. The return value is stored directly in slot->curl without checking for NULL. curl_easy_duphandle() can return NULL when memory allocation fails internally, and the libcurl documentation explicitly states this possibility.

When this happens, slot->curl is NULL and the very next operation (curl_easy_setopt on line 1632 for CURLOPT_COOKIEFILE) passes NULL as the curl handle, which is undefined behavior in libcurl and typically crashes.

Every HTTP operation in git goes through get_active_slot(), so this affects all remote-https, remote-http, and HTTP-based operations (clone, fetch, push over HTTP, bundle-uri downloads).

Add a NULL check and die() with a clear message. There is no reasonable recovery from a failed handle duplication: the process is out of memory and cannot perform any HTTP operation.

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 http.c | 2 ++
 1 file changed, 2 insertions(+)
Show changes to http.c +2 −0
diff --git a/http.c b/http.c
index b4e7b8d00b..8f1d6d1f56 100644
--- a/http.c
+++ b/http.c
@@ -1608,6 +1608,8 @@ struct active_request_slot *get_active_slot(void)
 
 	if (!slot->curl) {
 		slot->curl = curl_easy_duphandle(curl_default);
+		if (!slot->curl)
+			die("curl_easy_duphandle failed");
 		curl_session_count++;
 	}
 
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 12, 2026, 08:03 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v3 02/12] config: propagate launch_editor() failure in show_editor()

From: Johannes Schindelin <johannes.schindelin@gmx.de>

show_editor() calls launch_editor() to open the user's editor on the configuration file, but discards the return value and unconditionally returns 0 (success). When the editor fails to launch (e.g., $EDITOR is not found, or the editor exits with a nonzero status), the caller receives no indication that anything went wrong.

This affects "git config edit" and "git config --edit": the command silently succeeds even when the editor could not be started. In contrast, other editor-launching paths in git (such as "git commit" and "git rebase --edit-todo") properly propagate editor failures and exit with an error.

Check the return value and propagate the failure by returning -1. The two callers (cmd_config_edit at line 1315 and the legacy cmd_config at line 1478) both propagate this return to handle_builtin, which translates negative returns into an error exit.

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/config.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)
Show changes to builtin/config.c +4 −1
diff --git a/builtin/config.c b/builtin/config.c
index 8d8ec0beea..1307fdb0d6 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -1313,7 +1313,10 @@ static int show_editor(struct config_location_options *opts)
 		else if (errno != EEXIST)
 			die_errno(_("cannot create configuration file %s"), config_file);
 	}
-	launch_editor(config_file, NULL, NULL);
+	if (launch_editor(config_file, NULL, NULL)) {
+		free(config_file);
+		return -1;
+	}
 	free(config_file);
 
 	return 0;
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 12, 2026, 08:03 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v3 03/12] reftable: handle block-writer initialization errors

From: Johannes Schindelin <johannes.schindelin@gmx.de>

2d5dbb37b284 (reftable/block: handle allocation failures, 2024-10-02) taught `writer_reinit_block_writer()` to report initialization failures and updated its callers, but `reftable_writer_new()` continued to ignore the return value.

Consequently, the constructor could report success after block-writer initialization had failed. Propagate the error and release the constructor's allocations instead of returning an unusable writer.

Pointed out by GPT-5.6 Sol and Claude Opus 4.8.
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 reftable/writer.c | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)
Show changes to reftable/writer.c +7 −1
diff --git a/reftable/writer.c b/reftable/writer.c
index d969a6a021..073b9bbd89 100644
--- a/reftable/writer.c
+++ b/reftable/writer.c
@@ -150,6 +150,7 @@ int reftable_writer_new(struct reftable_writer **out,
 {
 	struct reftable_write_options opts = {0};
 	struct reftable_writer *wp;
+	int err;
 
 	if (_opts)
 		opts = *_opts;
@@ -177,7 +178,12 @@ int reftable_writer_new(struct reftable_writer **out,
 	wp->opts = opts;
 	wp->hash_id = hash_id;
 	wp->flush = flush_func;
-	writer_reinit_block_writer(wp, REFTABLE_BLOCK_TYPE_REF);
+	err = writer_reinit_block_writer(wp, REFTABLE_BLOCK_TYPE_REF);
+	if (err < 0) {
+		reftable_free(wp->block);
+		reftable_free(wp);
+		return err;
+	}
 
 	*out = wp;
 
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 12, 2026, 08:03 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v3 04/12] reftable/block: check deflateInit() return value

From: Johannes Schindelin <johannes.schindelin@gmx.de>

block_writer_init() allocates a z_stream and calls deflateInit() to prepare it for compressing log records. The return value of deflateInit() is silently discarded. If zlib initialization fails (e.g., Z_MEM_ERROR when the system is under memory pressure), the z_stream is left in an undefined state.

Subsequent deflate() calls in block_writer_finish() then operate on this uninitialized stream. Current zlib/zlib-ng versions handle such a stream gracefully, by returning `Z_STREAM_ERROR`, so in practice it would likely not result in catastrophic error.

The function already uses REFTABLE_ZLIB_ERROR for deflate() failures later in the code path, so returning the same error code for deflateInit() failure is consistent.

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Helped-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 reftable/block.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)
Show changes to reftable/block.c +4 −1
diff --git a/reftable/block.c b/reftable/block.c
index 920b3f4486..c12fedc5a2 100644
--- a/reftable/block.c
+++ b/reftable/block.c
@@ -87,7 +87,10 @@ int block_writer_init(struct block_writer *bw, uint8_t typ, uint8_t *block,
 		REFTABLE_CALLOC_ARRAY(bw->zstream, 1);
 		if (!bw->zstream)
 			return REFTABLE_OUT_OF_MEMORY_ERROR;
-		deflateInit(bw->zstream, 9);
+		if (deflateInit(bw->zstream, 9) != Z_OK) {
+			REFTABLE_FREE_AND_NULL(bw->zstream);
+			return REFTABLE_ZLIB_ERROR;
+		}
 	}
 
 	return 0;
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 12, 2026, 08:03 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v3 05/12] reftable tests: check reftable_table_init_ref_iterator() return

From: Johannes Schindelin <johannes.schindelin@gmx.de>

test_reftable_table__seek_once() and test_reftable_table__reseek() both call reftable_table_init_ref_iterator() without checking its return value. This function returns an int error code (0 on success, negative on failure). Every other reftable function call in these same tests checks the return via cl_assert_equal_i() or cl_assert(), making this omission inconsistent.

If the iterator initialization ever fails (e.g., due to a memory allocation failure in the reftable internals), the test would proceed to seek and read with an uninitialized iterator, producing misleading test results or crashes rather than a clear assertion failure.

Check the return value via cl_assert_equal_i(ret, 0), consistent with the surrounding code.

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 t/unit-tests/u-reftable-table.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)
Show changes to t/unit-tests/u-reftable-table.c +4 −2
diff --git a/t/unit-tests/u-reftable-table.c b/t/unit-tests/u-reftable-table.c
index fae478ee04..6f444f8cf9 100644
--- a/t/unit-tests/u-reftable-table.c
+++ b/t/unit-tests/u-reftable-table.c
@@ -29,7 +29,8 @@ void test_reftable_table__seek_once(void)
 	ret = reftable_table_new(&table, &source, "name");
 	cl_assert(!ret);
 
-	reftable_table_init_ref_iterator(table, &it);
+	ret = reftable_table_init_ref_iterator(table, &it);
+	cl_assert_equal_i(ret, 0);
 	ret = reftable_iterator_seek_ref(&it, "");
 	cl_assert(!ret);
 	ret = reftable_iterator_next_ref(&it, &ref);
@@ -71,7 +72,8 @@ void test_reftable_table__reseek(void)
 	ret = reftable_table_new(&table, &source, "name");
 	cl_assert(!ret);
 
-	reftable_table_init_ref_iterator(table, &it);
+	ret = reftable_table_init_ref_iterator(table, &it);
+	cl_assert_equal_i(ret, 0);
 
 	for (size_t i = 0; i < 5; i++) {
 		ret = reftable_iterator_seek_ref(&it, "");
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 12, 2026, 08:03 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v3 06/12] last-modified: handle repo_parse_commit() failures

From: Johannes Schindelin <johannes.schindelin@gmx.de>

last_modified_run() and process_parent() call repo_parse_commit() without checking the return value at three sites. When a commit object is corrupt or unavailable (e.g., a shallow clone boundary or a missing object in a partial clone), the parse fails and the commit's internal fields (parents, tree, date) are not populated.

The consequences depend on which call site fails:

At line 417 (the main walk loop), c->parents stays NULL after a failed parse. The parent-walking loop at line 440 simply does not execute, silently treating the unparsable commit as a root commit. This produces incorrect "last modified" results: paths changed in ancestors beyond the corrupt commit are attributed to the wrong commit or not reported at all.

At line 423 (the --not exclusion walk), n->parents stays NULL, causing the exclusion walk to stop prematurely. Commits that should be excluded from the output may be incorrectly included.

At line 293 (process_parent), the parent's tree and parents are unavailable, so diff operations against it produce wrong results and the parent's own ancestors are never enqueued for walking.

Skip unparsable commits by checking the return value and continuing to the next iteration (or returning early in process_parent). This matches the defensive pattern used in other revision walkers such as limit_list() and get_revision_internal().

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Helped-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/last-modified.c | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)
Show changes to builtin/last-modified.c +6 −3
diff --git a/builtin/last-modified.c b/builtin/last-modified.c
index 5478182f2e..3846244dfc 100644
--- a/builtin/last-modified.c
+++ b/builtin/last-modified.c
@@ -290,7 +290,8 @@ static void process_parent(struct last_modified *lm,
 {
 	struct bitmap *active_p;
 
-	repo_parse_commit(lm->rev.repo, parent);
+	if (repo_parse_commit(lm->rev.repo, parent))
+		return;
 	active_p = active_paths_for(lm, parent);
 
 	/*
@@ -414,12 +415,14 @@ static int last_modified_run(struct last_modified *lm)
 		 * Otherwise, make sure that 'c' isn't reachable from anything
 		 * in the '--not' queue.
 		 */
-		repo_parse_commit(lm->rev.repo, c);
+		if (repo_parse_commit(lm->rev.repo, c))
+			goto cleanup;
 
 		while ((n = prio_queue_get(&not_queue))) {
 			struct commit_list *np;
 
-			repo_parse_commit(lm->rev.repo, n);
+			if (repo_parse_commit(lm->rev.repo, n))
+				continue;
 
 			for (np = n->parents; np; np = np->next) {
 				if (!(np->item->object.flags & PARENT2)) {
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 12, 2026, 08:03 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v3 07/12] compat/pread: check initial lseek for errors

From: Johannes Schindelin <johannes.schindelin@gmx.de>

git_pread() saves the current file offset via lseek(fd, 0, SEEK_CUR) and later restores it. If the initial lseek fails (e.g., the fd is a pipe or otherwise non-seekable), current_offset is -1. This negative value is later passed to lseek(fd, -1, SEEK_SET) at line 16, which sets the file position to an unintended location (or fails with EINVAL on some platforms).

Check the initial lseek return value and return -1 immediately if it fails, consistent with the error handling for the other lseek calls in the same function.

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 compat/pread.c | 2 ++
 1 file changed, 2 insertions(+)
Show changes to compat/pread.c +2 −0
diff --git a/compat/pread.c b/compat/pread.c
index 484e6d4c71..ac7d058cb8 100644
--- a/compat/pread.c
+++ b/compat/pread.c
@@ -7,6 +7,8 @@ ssize_t git_pread(int fd, void *buf, size_t count, off_t offset)
         ssize_t rc;
 
         current_offset = lseek(fd, 0, SEEK_CUR);
+	if (current_offset < 0)
+		return -1;
 
         if (lseek(fd, offset, SEEK_SET) < 0)
                 return -1;
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 12, 2026, 08:03 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v3 08/12] transport-helper: check dup() return in get_exporter

From: Johannes Schindelin <johannes.schindelin@gmx.de>

get_exporter() duplicates helper->in via dup() and stores the result in fastexport->out. If dup() fails (fd exhaustion), it returns -1. The child_process machinery interprets out = -1 as "create a pipe for stdout", which would silently change the fast-export process's output wiring: instead of sending data back through the helper's input fd, it would write to a new pipe that nobody reads from.

Check the return value and report the error before proceeding.
Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 transport-helper.c | 2 ++
 1 file changed, 2 insertions(+)
Show changes to transport-helper.c +2 −0
diff --git a/transport-helper.c b/transport-helper.c
index 80f90eb7ba..31883b244e 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -487,6 +487,8 @@ static int get_exporter(struct transport *transport,
 	/* we need to duplicate helper->in because we want to use it after
 	 * fastexport is done with it. */
 	fastexport->out = dup(helper->in);
+	if (fastexport->out < 0)
+		return error_errno(_("could not dup helper output fd"));
 	strvec_push(&fastexport->args, "fast-export");
 	strvec_push(&fastexport->args, "--use-done-feature");
 	strvec_push(&fastexport->args, data->signed_tags ?
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 12, 2026, 08:03 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v3 09/12] transport-helper: warn when export-marks file cannot be finalized

From: Johannes Schindelin <johannes.schindelin@gmx.de>

When push_refs_with_export() finalizes a successful push, it writes the fast-export marks file to a .tmp sibling and rename()s it into place. The return value of rename() is currently ignored. If the rename fails (permission denied, full disk, or an antivirus product locking the destination on Windows), the .tmp file is left behind and the existing export_marks file remains stale; the next fast-export operation that resumes from it then silently operates on inconsistent bookkeeping.

The push itself succeeded by that point, so promoting this to a fatal error would be inappropriate. Emit warning_errno() naming both paths so the user can recover manually, and keep returning 0.

Flagged by Coverity as CID 1427723 ("Unchecked return value").
Assisted-by: Opus 4.7
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 transport-helper.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)
Show changes to transport-helper.c +3 −1
diff --git a/transport-helper.c b/transport-helper.c
index 31883b244e..ed0543f1ad 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -1184,7 +1184,9 @@ static int push_refs_with_export(struct transport *transport,
 
 	if (data->export_marks) {
 		strbuf_addf(&buf, "%s.tmp", data->export_marks);
-		rename(buf.buf, data->export_marks);
+		if (rename(buf.buf, data->export_marks))
+			warning_errno(_("could not rename '%s' to '%s'"),
+				      buf.buf, data->export_marks);
 		strbuf_release(&buf);
 	}
 
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 12, 2026, 08:03 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v3 10/12] bisect: check strbuf_getline_lf return when reading terms

From: Johannes Schindelin <johannes.schindelin@gmx.de>

get_terms() in builtin/bisect.c and read_bisect_terms() in bisect.c both read the BISECT_TERMS file but do not check the strbuf_getline_lf() return values. If the file is truncated (e.g., a partial write from a crash or disk-full condition), strbuf_getline_lf returns EOF and the strbuf remains empty. strbuf_detach then returns an empty string, and the term names silently become "" instead of the expected "bad"/"good" or custom terms.

In get_terms(), check for EOF and return -1 on truncation, matching the existing -1 return for a missing file.

In read_bisect_terms(), die with a descriptive message when a line cannot be read, consistent with the die_errno for a non-ENOENT open failure in the same function. Unlike get_terms(), read_bisect_terms() returns void and uses die() for all error paths, so the die is the appropriate error handling here.

Pointed out by Coverity.
Assisted-by: Claude Opus 4.6
Helped-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 bisect.c         |  6 ++++--
 builtin/bisect.c | 11 +++++++++--
 2 files changed, 13 insertions(+), 4 deletions(-)
Show changes to 2 files +13 −4

bisect.c, builtin/bisect.c

diff --git a/bisect.c b/bisect.c
index 94c7028d2a..c2ef5da462 100644
--- a/bisect.c
+++ b/bisect.c
@@ -1019,10 +1019,12 @@ void read_bisect_terms(char **read_bad, char **read_good)
 			die_errno(_("could not read file '%s'"), filename);
 		}
 	} else {
-		strbuf_getline_lf(&str, fp);
+		if (strbuf_getline_lf(&str, fp) == EOF)
+			die(_("could not read bad term from file '%s'"), filename);
 		free(*read_bad);
 		*read_bad = strbuf_detach(&str, NULL);
-		strbuf_getline_lf(&str, fp);
+		if (strbuf_getline_lf(&str, fp) == EOF)
+			die(_("could not read good term from file '%s'"), filename);
 		free(*read_good);
 		*read_good = strbuf_detach(&str, NULL);
 	}
diff --git a/builtin/bisect.c b/builtin/bisect.c
index 798e28f501..69ab7ea248 100644
--- a/builtin/bisect.c
+++ b/builtin/bisect.c
@@ -498,9 +498,16 @@ static int get_terms(struct bisect_terms *terms)
 	}
 
 	free_terms(terms);
-	strbuf_getline_lf(&str, fp);
+	if (strbuf_getline_lf(&str, fp) == EOF) {
+		res = -1;
+		goto finish;
+	}
 	terms->term_bad = strbuf_detach(&str, NULL);
-	strbuf_getline_lf(&str, fp);
+	if (strbuf_getline_lf(&str, fp) == EOF) {
+		res = -1;
+		FREE_AND_NULL(terms->term_bad);
+		goto finish;
+	}
 	terms->term_good = strbuf_detach(&str, NULL);
 
 finish:
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 12, 2026, 08:03 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v3 11/12] bisect: check get_terms return at all call sites

From: Johannes Schindelin <johannes.schindelin@gmx.de>

Six callers of get_terms() silently discard its return value. When get_terms fails (missing or truncated BISECT_TERMS file), the term strings remain NULL or empty, causing confusing downstream behavior: commands like "bisect next" or "bisect run" proceed with empty term strings, producing nonsensical ref names (refs/bisect/ with no suffix) and misleading error messages.

Let's not discard the return value, but handle an error with the same message `bisect_terms()` already uses when reading the terms failed.

Pointed out by Coverity.

There is one slight complication here: One caller _needs_ the return value to indicate an error when the `BISECT_TERMS` file is absent, all the other call sites are totally okay with a "missing" `BISECT_TERMS` file. To address that, extend the function signature of `get_terms()` to indicate which behavior the caller wants.

Assisted-by: Claude Opus 4.6
Helped-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/bisect.c | 24 +++++++++++++++---------
 1 file changed, 15 insertions(+), 9 deletions(-)
Show changes to builtin/bisect.c +15 −9
diff --git a/builtin/bisect.c b/builtin/bisect.c
index 69ab7ea248..ceb60b0626 100644
--- a/builtin/bisect.c
+++ b/builtin/bisect.c
@@ -485,7 +485,7 @@ static int bisect_next_check(const struct bisect_terms *terms,
 	return decide_next(terms, current_term, !state.nr_good, !state.nr_bad);
 }
 
-static int get_terms(struct bisect_terms *terms)
+static int get_terms(struct bisect_terms *terms, int file_missing_is_ok)
 {
 	struct strbuf str = STRBUF_INIT;
 	FILE *fp = NULL;
@@ -493,7 +493,7 @@ static int get_terms(struct bisect_terms *terms)
 
 	fp = fopen(git_path_bisect_terms(), "r");
 	if (!fp) {
-		res = -1;
+		res = file_missing_is_ok ? 0 : -1;
 		goto finish;
 	}
 
@@ -519,7 +519,7 @@ finish:
 
 static int bisect_terms(struct bisect_terms *terms, const char *option)
 {
-	if (get_terms(terms))
+	if (get_terms(terms, 0))
 		return error(_("no terms defined"));
 
 	if (!option) {
@@ -1057,7 +1057,8 @@ static int process_replay_line(struct bisect_terms *terms, struct strbuf *line)
 	rev = word_end + strspn(word_end, " \t");
 	*word_end = '\0'; /* NUL-terminate the word */
 
-	get_terms(terms);
+	if (get_terms(terms, 1))
+		return error(_("no terms defined"));
 	if (check_and_set_terms(terms, p))
 		return -1;
 
@@ -1383,7 +1384,8 @@ static int cmd_bisect__next(int argc, const char **argv UNUSED, const char *pref
 	if (argc)
 		return error(_("'%s' requires 0 arguments"),
 			     "git bisect next");
-	get_terms(&terms);
+	if (get_terms(&terms, 1))
+		return error(_("no terms defined"));
 	res = bisect_next(&terms, prefix);
 	free_terms(&terms);
 	return res;
@@ -1417,7 +1419,8 @@ static int cmd_bisect__skip(int argc, const char **argv, const char *prefix UNUS
 	struct bisect_terms terms = { 0 };
 
 	set_terms(&terms, "bad", "good");
-	get_terms(&terms);
+	if (get_terms(&terms, 1))
+		return error(_("no terms defined"));
 	res = bisect_skip(&terms, argc, argv);
 	free_terms(&terms);
 	return res;
@@ -1429,7 +1432,8 @@ static int cmd_bisect__visualize(int argc, const char **argv, const char *prefix
 	int res;
 	struct bisect_terms terms = { 0 };
 
-	get_terms(&terms);
+	if (get_terms(&terms, 1))
+		return error(_("no terms defined"));
 	res = bisect_visualize(&terms, argc, argv);
 	free_terms(&terms);
 	return res;
@@ -1443,7 +1447,8 @@ static int cmd_bisect__run(int argc, const char **argv, const char *prefix UNUSE
 
 	if (!argc)
 		return error(_("'%s' failed: no command provided."), "git bisect run");
-	get_terms(&terms);
+	if (get_terms(&terms, 1))
+		return error(_("no terms defined"));
 	res = bisect_run(&terms, argc, argv);
 	free_terms(&terms);
 	return res;
@@ -1482,7 +1487,8 @@ int cmd_bisect(int argc,
 			usage_with_options(git_bisect_usage, options);
 
 		set_terms(&terms, "bad", "good");
-		get_terms(&terms);
+		if (get_terms(&terms, 1))
+			return error(_("no terms defined"));
 		if (check_and_set_terms(&terms, argv[0]) ||
 		    !one_of(argv[0], terms.term_good, terms.term_bad, NULL))
 			usage_msg_optf(_("unknown command: '%s'"), git_bisect_usage,
-- 
gitgitgadget
Johannes Schindelin via GitGitGadgetAug 12, 2026, 08:03 UTC in reply to Johannes Schindelin via GitGitGadget on lore

[PATCH v3 12/12] bisect: handle dup() failure when redirecting stdout

From: Johannes Schindelin <johannes.schindelin@gmx.de>

To capture the output of each verdict command, bisect_run() temporarily redirects stdout to a temporary file via the classic dup(1) / dup2() pair, restoring it afterwards. The return value of dup(1) is not checked, however. When it fails, the saved descriptor is -1, which is then passed to close() (the issue Coverity flags), and the matching dup2() that is meant to restore stdout also fails, leaving the process with stdout still pointing at the temporary file for the remainder of the run.

Treat a failed dup(1) or dup2(..., 1) as a fatal error for this bisect step: close the temporary file descriptor, report the error via error_errno(), and break out of the loop so the existing cleanup path handles the rest, just as on other failure paths in this function.

Reported by Coverity as CID 1508242 ("Improper use of negative value").

Assisted-by: Opus 4.7
Helped-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/bisect.c | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)
Show changes to builtin/bisect.c +8 −1
diff --git a/builtin/bisect.c b/builtin/bisect.c
index ceb60b0626..be42468af6 100644
--- a/builtin/bisect.c
+++ b/builtin/bisect.c
@@ -1308,7 +1308,14 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv)
 
 		fflush(stdout);
 		saved_stdout = dup(1);
-		dup2(temporary_stdout_fd, 1);
+		if (saved_stdout < 0 ||
+		    dup2(temporary_stdout_fd, 1) < 0) {
+			res = error_errno(_("could not duplicate stdout"));
+			if (saved_stdout >= 0)
+				close(saved_stdout);
+			close(temporary_stdout_fd);
+			break;
+		}
 
 		res = bisect_state(terms, 1, &new_state);
 
-- 
gitgitgadget
Junio C HamanoAug 12, 2026, 17:29 UTC in reply to Johannes Schindelin via GitGitGadget on lore

Re: [PATCH v3 00/12] coverity: fix unchecked returns

"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com> writes:

Show 13 quoted lines
> This is the next batch of fixes in response to issues reported by Coverity.
>
> Changes since v2:
>
>  * Added a new commit to handle block-writer initialization errors (instead
>    of ignoring them).
>  * The bw->zstream attribute is now also deinitialized in the error case, as
>    suggested by Junio.
>  * The commit message of "reftable/block: check deflateInit() return value"
>    was rephrased to stop suggesting that silent corruption by zlib would be
>    possible before that patch: This turned out to be provably incorrect.
>  * When aborting the bisect because dup2() failed, a left-over saved_stdout
>    is now also cleaned up.

Everything looks sensible. I am fine with declaring victory, but does anyone want to second it?

Jeff KingAug 12, 2026, 21:33 UTC in reply to Johannes Schindelin via GitGitGadget on lore

Re: [PATCH v3 12/12] bisect: handle dup() failure when redirecting stdout

On Wed, Aug 12, 2026 at 08:03:20AM +0000, Johannes Schindelin via GitGitGadget wrote:
Show 13 quoted lines
> @@ -1308,7 +1308,14 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv)
>  
>  		fflush(stdout);
>  		saved_stdout = dup(1);
> -		dup2(temporary_stdout_fd, 1);
> +		if (saved_stdout < 0 ||
> +		    dup2(temporary_stdout_fd, 1) < 0) {
> +			res = error_errno(_("could not duplicate stdout"));
> +			if (saved_stdout >= 0)
> +				close(saved_stdout);
> +			close(temporary_stdout_fd);
> +			break;
> +		}

OK. The extra "if (saved_stdout >= 0)" is not strictly necessary if we are OK considering close(-1) as a noop, but it doesn't hurt too much.

It could also be avoided with two separate checks:
  saved_stdout = dup(1);
  if (saved_stdout < 0)
	...
  if (dup2_temporary_stdout_fd, 1) < 0)
	...

but that would involve a little bit of repetition of the other cleanup lines (though it would also allow more specific error messages).

Probably not worth polishing this further, though. What you have here is correct and I would be surprised if any user ever sees this error case. It is mostly about covering all of the paths for leaks.

-Peff
Jeff KingAug 12, 2026, 21:34 UTC in reply to Junio C Hamano on lore

Re: [PATCH v3 00/12] coverity: fix unchecked returns

On Wed, Aug 12, 2026 at 10:29:33AM -0700, Junio C Hamano wrote:
Show 19 quoted lines
> "Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
> writes:
> 
> > This is the next batch of fixes in response to issues reported by Coverity.
> >
> > Changes since v2:
> >
> >  * Added a new commit to handle block-writer initialization errors (instead
> >    of ignoring them).
> >  * The bw->zstream attribute is now also deinitialized in the error case, as
> >    suggested by Junio.
> >  * The commit message of "reftable/block: check deflateInit() return value"
> >    was rephrased to stop suggesting that silent corruption by zlib would be
> >    possible before that patch: This turned out to be provably incorrect.
> >  * When aborting the bisect because dup2() failed, a left-over saved_stdout
> >    is now also cleaned up.
> 
> Everything looks sensible.  I am fine with declaring victory, but
> does anyone want to second it?

I cannot claim to have read all of the patches carefully, but this version addressed the sole concern I raised, and in the few other patches I glanced over I didn't see anything to complain about. So maybe consider that a weak second. :)

-Peff
Patrick SteinhardtAug 13, 2026, 06:31 UTC in reply to Johannes Schindelin via GitGitGadget on lore

Re: [PATCH v3 09/12] transport-helper: warn when export-marks file cannot be finalized

On Wed, Aug 12, 2026 at 08:03:17AM +0000, Johannes Schindelin via GitGitGadget wrote:
Show 10 quoted lines
> From: Johannes Schindelin <johannes.schindelin@gmx.de>
> 
> When push_refs_with_export() finalizes a successful push, it writes
> the fast-export marks file to a .tmp sibling and rename()s it into
> place. The return value of rename() is currently ignored. If the
> rename fails (permission denied, full disk, or an antivirus product
> locking the destination on Windows), the .tmp file is left behind
> and the existing export_marks file remains stale; the next
> fast-export operation that resumes from it then silently operates on
> inconsistent bookkeeping.

One question here would be whether we should try to unlink the file instead if renaming it into place failed. But not doing so potentially gives the user the ability to fix that issue. So I'm not sure whether that's really a sensible thing to do in the first place.

In any case, the post-image of this patch is a clear improvement as we now enable the user to act on the warning in the first place, whereas previously they wouldn't ever learn about it until the failed rename may cause errors. So overall I think this is okay as-is.

Patrick
Patrick SteinhardtAug 13, 2026, 06:31 UTC in reply to Jeff King on lore

Re: [PATCH v3 00/12] coverity: fix unchecked returns

On Wed, Aug 12, 2026 at 05:34:38PM -0400, Jeff King wrote:
Show 26 quoted lines
> On Wed, Aug 12, 2026 at 10:29:33AM -0700, Junio C Hamano wrote:
> 
> > "Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
> > writes:
> > 
> > > This is the next batch of fixes in response to issues reported by Coverity.
> > >
> > > Changes since v2:
> > >
> > >  * Added a new commit to handle block-writer initialization errors (instead
> > >    of ignoring them).
> > >  * The bw->zstream attribute is now also deinitialized in the error case, as
> > >    suggested by Junio.
> > >  * The commit message of "reftable/block: check deflateInit() return value"
> > >    was rephrased to stop suggesting that silent corruption by zlib would be
> > >    possible before that patch: This turned out to be provably incorrect.
> > >  * When aborting the bisect because dup2() failed, a left-over saved_stdout
> > >    is now also cleaned up.
> > 
> > Everything looks sensible.  I am fine with declaring victory, but
> > does anyone want to second it?
> 
> I cannot claim to have read all of the patches carefully, but this
> version addressed the sole concern I raised, and in the few other
> patches I glanced over I didn't see anything to complain about. So maybe
> consider that a weak second. :)
I didn't spot anything that needs to change, either. Thanks!
Patrick

Back to recent threads