Skip to content

coverity: avoid dereferencing NULL#2174

Open
dscho wants to merge 12 commits into
gitgitgadget:masterfrom
dscho:coverity-fixes-null-safety
Open

coverity: avoid dereferencing NULL#2174
dscho wants to merge 12 commits into
gitgitgadget:masterfrom
dscho:coverity-fixes-null-safety

Conversation

@dscho

@dscho dscho commented Jul 9, 2026

Copy link
Copy Markdown
Member

This is a continuation of the effort I started in the patch series that became js/coverity-fixes. This next batch adds guards to avoid dereferencing NULL pointers and accessing NULL file descriptors.

Changes since v1:

  • Calling remote_tracking() no longer returns -1 when remote is NULL, but instead BUG()s out.
  • bisect_successful() returns with BISECT_FAILED instead of the -1 that only worked by happenstance.
  • The commit "revision: avoid dereferencing NULL in add_parents_only()" now comes with a regression test.
  • The commit "bisect: ensure non-NULL head before using it" no longer claims that the fixed bug can be triggered with the current code base.
  • The missing shallow commit's OID is no longer computed twice.
  • A follow-up commit was folded into this patch series that lets write_one_shallow() avoid the rolling buffers of oid_to_hex(), as suggested by Junio. It technically does not fit the goal of this patch series (fixing issues pointed out by Coverity), but was asked for explicitly.

dscho added 2 commits July 9, 2026 07:11
The outer loop in `diffcore_merge_broken()` sets `q->queue[j]` to NULL
when it merges a broken pair back together, and has a NULL check to skip
such entries on subsequent iterations. The inner loop, however, lacks
this guard: when it scans forward looking for a matching peer, it can
encounter a slot that was NULLed by a previous outer-loop iteration and
dereference it unconditionally.

In practice this requires at least two broken pairs whose peers
both survive rename/copy detection and appear later in the queue,
which is rare but not impossible.

Add the same `if (!pp) continue` guard to the inner loop.

Pointed out by Coverity.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
The `repo_get_commit_tree()` function can return NULL when a commit's
tree object is not available (e.g., the commit was parsed but its
maybe_tree field is unset and the commit is not in the commit-graph). In
cmd_diff(), the return value is immediately dereferenced via ->object
without a NULL check, which would crash if the tree cannot be loaded.

Add an explicit NULL check and die with a descriptive message.

Pointed out by Coverity.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
@dscho dscho self-assigned this Jul 9, 2026
@dscho

dscho commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

/submit

@gitgitgadget

gitgitgadget Bot commented Jul 9, 2026

Copy link
Copy Markdown

Submitted as pull.2174.git.1783590159.gitgitgadget@gmail.com

To fetch this version into FETCH_HEAD:

git fetch https://github.com/gitgitgadget/git/ pr-2174/dscho/coverity-fixes-null-safety-v1

To fetch this version to local tag pr-2174/dscho/coverity-fixes-null-safety-v1:

git fetch --no-tags https://github.com/gitgitgadget/git/ tag pr-2174/dscho/coverity-fixes-null-safety-v1

Comment thread shallow.c
@@ -371,7 +371,7 @@ static int write_one_shallow(const struct commit_graft *graft, void *cb_data)
if (!c || !(c->object.flags & SEEN)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

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

> diff --git a/shallow.c b/shallow.c
> index 07cae44ae5..3d2230351e 100644
> --- a/shallow.c
> +++ b/shallow.c
> @@ -371,7 +371,7 @@ static int write_one_shallow(const struct commit_graft *graft, void *cb_data)
>  		if (!c || !(c->object.flags & SEEN)) {
>  			if (data->flags & VERBOSE)
>  				printf("Removing %s from .git/shallow\n",
> -				       oid_to_hex(&c->object.oid));
> +				       oid_to_hex(&graft->oid));
>  			return 0;

Haha.  We come into this block and emit this message when we may not
even have a valid 'c', yet we use c->object.oid there.  It makes
perfect sense to use graft->oid here instead, as your patch does.

However, its hexadecimal representation has already been computed in
the local variable 'hex', and the "happy path" code after this
section seems to assume that 'hex' is still valid (even though
oid_to_hex() uses rotating 4-element buffer, which makes the
assumption a risky one).

We should use "hex" here instead of oid_to_hex(&graft->oid), which
does not add to the existing risk.  In addition, if we add something
like:

                struct write_shallow_data *data = cb_data;
        -	const char *hex = oid_to_hex(&graft->oid);
        +	char hex[GIT_MAX_HEXSZ + 1];
        +
        +       oid_to_hex_r(hex, &graft->oid);
                if (graft->nr_parent != -1)
                        return 0;

to the beginning of the function, we can get rid of existing
riskiness entirely.

Comment thread diffcore-break.c
@@ -289,6 +289,8 @@ void diffcore_merge_broken(void)
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

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

> From: Johannes Schindelin <johannes.schindelin@gmx.de>
>
> The outer loop in `diffcore_merge_broken()` sets `q->queue[j]` to NULL
> when it merges a broken pair back together, and has a NULL check to skip
> such entries on subsequent iterations. The inner loop, however, lacks
> this guard: when it scans forward looking for a matching peer, it can
> encounter a slot that was NULLed by a previous outer-loop iteration and
> dereference it unconditionally.
>
> In practice this requires at least two broken pairs whose peers
> both survive rename/copy detection and appear later in the queue,
> which is rare but not impossible.

Interesting find.  This is an ancient part of the codebase that
nobody has touched in the past 21 years since eeaa460314 ([PATCH]
diff: Update -B heuristics., 2005-06-03) introduced it ;-).

Well spotted.

> Add the same `if (!pp) continue` guard to the inner loop.
>
> Pointed out by Coverity.
>
> Assisted-by: Claude Opus 4.6
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
>  diffcore-break.c | 2 ++
>  1 file changed, 2 insertions(+)
>
> diff --git a/diffcore-break.c b/diffcore-break.c
> index 17b5ad1fed..b5bcc956cc 100644
> --- a/diffcore-break.c
> +++ b/diffcore-break.c
> @@ -289,6 +289,8 @@ void diffcore_merge_broken(void)
>  			 */
>  			for (j = i + 1; j < q->nr; j++) {
>  				struct diff_filepair *pp = q->queue[j];
> +				if (!pp)
> +					continue;
>  				if (pp->broken_pair &&
>  				    !strcmp(pp->one->path, pp->two->path) &&
>  				    !strcmp(p->one->path, pp->two->path)) {

Comment thread builtin/diff.c
@@ -579,9 +579,13 @@ int cmd_diff(int argc,
obj = deref_tag(the_repository, obj, NULL, 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

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

> diff --git a/builtin/diff.c b/builtin/diff.c
> index 4b46e394ce..18b1083e98 100644
> --- a/builtin/diff.c
> +++ b/builtin/diff.c
> @@ -579,9 +579,13 @@ int cmd_diff(int argc,
>  		obj = deref_tag(the_repository, obj, NULL, 0);
>  		if (!obj)
>  			die(_("invalid object '%s' given."), name);
> -		if (obj->type == OBJ_COMMIT)
> -			obj = &repo_get_commit_tree(the_repository,
> -						    ((struct commit *)obj))->object;
> +		if (obj->type == OBJ_COMMIT) {
> +			struct tree *tree = repo_get_commit_tree(
> +				the_repository, (struct commit *)obj);
> +			if (!tree)
> +				die(_("unable to read tree object for commit '%s'"), name);
> +			obj = &tree->object;
> +		}

Obviously correct.

>  		if (obj->type == OBJ_TREE) {
>  			if (sdiff.skip && bitmap_get(sdiff.skip, i))

Comment thread remote.c
@@ -2681,6 +2681,8 @@ static int remote_tracking(struct remote *remote, const char *refname,
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

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

> However, it requires quite involved reasoning to reach that conclusion,
> and is therefore fragile. Just return -1 ("no tracking ref") when there
> is no remote to work with.

In a case like this, where the function is designed not to be called
with NULL remote, I would prefer to have an explicit BUG() rather
than sweeping the problem under the rug.  That would make sure your
investigation and involved reasoning done here remain relevant if
the BUG() triggers due to careless changes to the caller in the
future.

Thanks.

> Pointed out by Coverity.
>
> Assisted-by: Claude Opus 4.6
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
>  remote.c | 2 ++
>  1 file changed, 2 insertions(+)
>
> diff --git a/remote.c b/remote.c
> index 00723b385e..34d0367f11 100644
> --- a/remote.c
> +++ b/remote.c
> @@ -2681,6 +2681,8 @@ static int remote_tracking(struct remote *remote, const char *refname,
>  {
>  	char *dst;
>  
> +	if (!remote)
> +		return -1; /* no remote to look up tracking ref */
>  	dst = apply_refspecs(&remote->fetch, refname);
>  	if (!dst)
>  		return -1; /* no tracking ref for refname at remote */

Comment thread reftable/stack.c
@@ -171,7 +171,8 @@ void reftable_stack_destroy(struct reftable_stack *st)
st->merged = NULL;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

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

> From: Johannes Schindelin <johannes.schindelin@gmx.de>
>
> When reftable_new_stack() fails partway through initialization
> (e.g., reftable_buf_addstr returns an OOM error before
> reftable_buf_detach assigns p->list_file), it jumps to the error
> path which calls reftable_stack_destroy(p). At that point,
> p->list_file is still NULL because the detach never happened.
>
> reftable_stack_destroy() passes st->list_file unconditionally to
> read_lines(), which calls open(filename, O_RDONLY). Passing NULL
> to open() is undefined behavior and will typically crash.
>
> Guard the read_lines() call with a NULL check on st->list_file.
> When list_file is NULL, there are no table files to clean up
> anyway, so skipping read_lines is the correct behavior.

Nice spotting and recovery.  Well done.

>
> Pointed out by Coverity.
>
> Assisted-by: Claude Opus 4.6
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
>  reftable/stack.c | 3 ++-
>  1 file changed, 2 insertions(+), 1 deletion(-)
>
> diff --git a/reftable/stack.c b/reftable/stack.c
> index 1fba96ddb3..3fc3c0b2d1 100644
> --- a/reftable/stack.c
> +++ b/reftable/stack.c
> @@ -171,7 +171,8 @@ void reftable_stack_destroy(struct reftable_stack *st)
>  		st->merged = NULL;
>  	}
>  
> -	err = read_lines(st->list_file, &names);
> +	if (st->list_file)
> +		err = read_lines(st->list_file, &names);
>  	if (err < 0) {
>  		REFTABLE_FREE_AND_NULL(names);
>  	}

Comment thread builtin/mailsplit.c
@@ -225,14 +225,14 @@ static int split_mbox(const char *file, const char *dir, int allow_bare,
FILE *f = !strcmp(file, "-") ? stdin : fopen(file, "r");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

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

> diff --git a/builtin/mailsplit.c b/builtin/mailsplit.c
> index 264df6259a..0993418e63 100644
> --- a/builtin/mailsplit.c
> +++ b/builtin/mailsplit.c
> @@ -225,14 +225,14 @@ static int split_mbox(const char *file, const char *dir, int allow_bare,
>  	FILE *f = !strcmp(file, "-") ? stdin : fopen(file, "r");
>  	int file_done = 0;
>  
> -	if (isatty(fileno(f)))
> -		warning(_("reading patches from stdin/tty..."));
> -
>  	if (!f) {
>  		error_errno("cannot open mbox %s", file);
>  		goto out;
>  	}
>  
> +	if (isatty(fileno(f)))
> +		warning(_("reading patches from stdin/tty..."));
> +
>  	do {
>  		peek = fgetc(f);
>  		if (peek == EOF) {

Ah, obviously correct.  Cannot believe nobody noticed this since it
was first written in 7b20af6a06 (am/apply: warn if we end up reading
patches from terminal, 2022-03-03).

Thanks.

Comment thread builtin/bisect.c
@@ -663,6 +663,11 @@ static int bisect_successful(struct bisect_terms *terms)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

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

> diff --git a/builtin/bisect.c b/builtin/bisect.c
> index e7c2d2f3bb..6ff600c856 100644
> --- a/builtin/bisect.c
> +++ b/builtin/bisect.c
> @@ -663,6 +663,11 @@ static int bisect_successful(struct bisect_terms *terms)
>  
>  	refs_read_ref(get_main_ref_store(the_repository), bad_ref, &oid);
>  	commit = lookup_commit_reference_by_name(bad_ref);
> +	if (!commit) {
> +		res = error(_("could not find commit for '%s'"), bad_ref);
> +		free(bad_ref);
> +		return res;
> +	}

Catching this case as an error is the right thing to do, but there is
a bit of an impedance mismatch between the return value from error()
and the status passed around in the bisect codebase.

The bisect.h header defines an enum bisect_error type, and I think
the sole caller of this function, bisect_next(), expects to see
BISECT_FAILED.  It may happen to be the same -1 that error()
returns, but for longer term maintainability, I would prefer to see
it done more like:

	error(_("..."));
	free(bad_ref);
	return BISECT_FAILED;

or something along those lines.

Thanks.

>  	repo_format_commit_message(the_repository, commit, "%s", &commit_name,
>  				   &pp);

Comment thread revision.c
@@ -1903,8 +1903,13 @@ static int add_parents_only(struct rev_info *revs, const char *arg_, int flags,
return 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

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

> From: Johannes Schindelin <johannes.schindelin@gmx.de>
>
> This function resolves revision suffixes like commit^@ (all parents),
> commit^! (commit minus parents), and commit^-N (exclude Nth parent). It
> calls `get_reference()` in a loop to peel through tag objects until it
> reaches a commit.
>
> The existing NULL check after `get_reference()` only handles the
> ignore_missing case, but get_reference() can return NULL through three
> distinct paths:

Nicely spotted.  It sounds like something a test can ensure does not
to regress in the future, unless I am misreading this explanation.
Could you include such a test?

Thanks.

> diff --git a/revision.c b/revision.c
> index e91d7e1f11..7f3999b551 100644
> --- a/revision.c
> +++ b/revision.c
> @@ -1903,8 +1903,13 @@ static int add_parents_only(struct rev_info *revs, const char *arg_, int flags,
>  		return 0;
>  	while (1) {
>  		it = get_reference(revs, arg, &oid, 0);
> -		if (!it && revs->ignore_missing)
> -			return 0;
> +		if (!it) {
> +			if (revs->ignore_missing)
> +				return 0;
> +			if (revs->do_not_die_on_missing_objects)
> +				return 0;
> +			return -1;
> +		}
>  		if (it->type != OBJ_TAG)
>  			break;
>  		if (!((struct tag*)it)->tagged)

Comment thread pack-bitmap.c
@@ -523,6 +523,10 @@ static int open_midx_bitmap_1(struct bitmap_index *bitmap_git,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

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

> This can happen in practice with incremental MIDX chains: the base MIDX
> may have been written without `--write-bitmap-index`, or the bitmap may
> have been pruned while the incremental layer's bitmap still references
> it.
>
> Check the return value and go to the cleanup label (which unmaps the
> current bitmap and returns -1) so the caller falls back to non-bitmap
> object enumeration, matching the handling of other bitmap loading
> failures in the same function.

Nicely reasoned.  It would have been nicer to CC those who are more
familiar with the area, though.

Cc'ed Taylor for incremental MIDX expertise just in case.

Thanks.

>
> Pointed out by Coverity.
>
> Assisted-by: Claude Opus 4.6
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
>  pack-bitmap.c | 4 ++++
>  1 file changed, 4 insertions(+)
>
> diff --git a/pack-bitmap.c b/pack-bitmap.c
> index e8a82945cc..ca7998c10b 100644
> --- a/pack-bitmap.c
> +++ b/pack-bitmap.c
> @@ -523,6 +523,10 @@ static int open_midx_bitmap_1(struct bitmap_index *bitmap_git,
>  
>  	if (midx->base_midx) {
>  		bitmap_git->base = prepare_midx_bitmap_git(midx->base_midx);
> +		if (!bitmap_git->base) {
> +			warning(_("could not open bitmap for base MIDX"));
> +			goto cleanup;
> +		}
>  		bitmap_git->base_nr = bitmap_git->base->base_nr + 1;
>  	} else {
>  		bitmap_git->base_nr = 0;

Comment thread builtin/bisect.c
@@ -806,9 +811,11 @@ static enum bisect_error bisect_start(struct bisect_terms *terms, int argc,
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

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

> When `refs_resolve_ref_unsafe()` is called to resolve HEAD, and returns
> NULL (e.g., HEAD does not exist as a proper ref), the code falls back to
> `repo_get_oid("HEAD")` to try to resolve the OID directly. If that
> succeeds, execution continues with `head` still set to NULL.
>
> Later, that variable is passed to `repo_get_oid()` and `starts_with()`,
> both of which would dereference the NULL pointer.
>
> The scenario "`refs_resolve_ref_unsafe()` returns NULL but
> `repo_get_oid()` succeeds" can happen when HEAD is a detached bare OID
> that the ref backend cannot resolve symbolically (a potential edge case
> with the reftable backend) but the OID itself is valid. In this case,
> the bisect-start file does not yet exist (this is a fresh "git bisect
> start"), so the else branch is taken with the NULL `head`.

I agree that setting head to the string "HEAD" is a good solution to
ensure that !starts_with(), !repo_get_oid(), and skip_prefix() are
not called with NULL.

However, I am not sure I understand your "can happen" scenario.

I naively thought that the only case where HEAD does not resolve to
an object correctly is when HEAD is a symbolic ref pointing to an
unborn branch.

Is the bug in your "can happen" scenario something we can
demonstrate?  If so, could you add a test to prevent regressions in
the future?

Thanks.


> Simply assign "HEAD" to `head` as a fallback to address this.
>
> Pointed out by Coverity.
>
> Assisted-by: Claude Opus 4.6
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
>  builtin/bisect.c | 4 +++-
>  1 file changed, 3 insertions(+), 1 deletion(-)
>
> diff --git a/builtin/bisect.c b/builtin/bisect.c
> index 6ff600c856..a69771c6d3 100644
> --- a/builtin/bisect.c
> +++ b/builtin/bisect.c
> @@ -811,9 +811,11 @@ static enum bisect_error bisect_start(struct bisect_terms *terms, int argc,
>  	 */
>  	head = refs_resolve_ref_unsafe(get_main_ref_store(the_repository),
>  				       "HEAD", 0, &head_oid, &flags);
> -	if (!head)
> +	if (!head) {
>  		if (repo_get_oid(the_repository, "HEAD", &head_oid))
>  			return error(_("bad HEAD - I need a HEAD"));
> +		head = "HEAD";
> +	}
>  
>  	/*
>  	 * Check if we are bisecting

dscho added 10 commits July 10, 2026 09:28
The `remote_tracking()` function unconditionally dereferences
`remote->fetch` without checking whether remote is NULL.

In practice, this never happens because the only caller (`apply_cas()`)
guards the calls to this function by checking the `use_tracking` and
`use_tracking_for_rest` attributes.

However, it requires quite involved reasoning to reach that conclusion,
and is therefore fragile. Just return -1 ("no tracking ref") when there
is no remote to work with.

Pointed out by Coverity.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
When reftable_new_stack() fails partway through initialization
(e.g., reftable_buf_addstr returns an OOM error before
reftable_buf_detach assigns p->list_file), it jumps to the error
path which calls reftable_stack_destroy(p). At that point,
p->list_file is still NULL because the detach never happened.

reftable_stack_destroy() passes st->list_file unconditionally to
read_lines(), which calls open(filename, O_RDONLY). Passing NULL
to open() is undefined behavior and will typically crash.

Guard the read_lines() call with a NULL check on st->list_file.
When list_file is NULL, there are no table files to clean up
anyway, so skipping read_lines is the correct behavior.

Pointed out by Coverity.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
The `split_mbox()` function calls fileno(f) to check whether the input
is a terminal, but the NULL check for f (from `fopen()`) does not happen
until later. When the file cannot be opened, f is NULL, and
`fileno(NULL)` is undefined behavior, typically crashing with a
segmentation fault.

Move the NULL check above the `isatty()`/`fileno()` call so the error
path is taken before any use of the potentially-NULL handle.

Pointed out by Coverity.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
When `lookup_commit_reference_by_name()` is called to find the first bad
commit, the result is passed to `repo_format_commit_message()`
immediately, which dereferences commit without checking for NULL.

However, the commit could be NULL, even though in practice this is
unlikely because `bisect_successful()` is only called after a successful
bisect run has identified the bad commit, but the ref could still become
dangling due to a concurrent gc or repository corruption.

Pointed out by Coverity.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
The `peel_committish()` function calls `repo_peel_to_type()` to convert
the given object to a commit, but does not check the return value. When
the object exists but cannot be peeled to a commit (e.g., a tree or blob
OID is passed as --onto), the return value is NULL. Add an explicit NULL
check and die with a descriptive message in that case.

Pointed out by Coverity.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
This function resolves revision suffixes like commit^@ (all parents),
commit^! (commit minus parents), and commit^-N (exclude Nth parent). It
calls `get_reference()` in a loop to peel through tag objects until it
reaches a commit.

The existing NULL check after `get_reference()` only handles the
ignore_missing case, but get_reference() can return NULL through three
distinct paths:

  1. revs->ignore_missing: the caller asked to silently skip missing
     objects.

  2. revs->exclude_promisor_objects: the object is a lazy promisor
     object that should be excluded from the walk.

  3. revs->do_not_die_on_missing_objects: the caller wants to record
     missing OIDs for later reporting (used by `git rev-list
     --missing=print`) rather than dying.

In the latter two instances, the code falls through to dereference the
NULL pointer.

Handle all three cases explicitly:

  - ignore_missing: return 0, matching the existing behavior and
    the pattern in `handle_revision_arg()`.

  - do_not_die_on_missing_objects: return 0. The missing OID has already
    been recorded in `revs->missing_commits` by `get_reference()`.
    Returning 0 is consistent with `handle_revision_arg()` and
    `process_parents()`, both of which continue without error when this flag
    is set. The broader codebase pattern for this flag is "record and
    continue": list-objects.c, builtin/rev-list.c, and process_parents
    all skip the die/error and keep walking.

  - everything else (only the `exclude_promisor_objects` case in
    practice): return -1, consistent with `handle_revision_arg()` where
    the condition only matches `ignore_missing` or
    `do_not_die_on_missing_objects`, falling through to ret = -1 for the
    promisor case.

Note: the callers of `add_parents_only()` in
`handle_revision_pseudo_opt()` treat any nonzero return as "handled"
(`if (add_parents_only(...)) { ret = 0; }`), so the -1 for the promisor
case is indistinguishable from success there. This means a
promisor-excluded tag target referenced via commit^@ would be silently
skipped rather than producing an error.  This is a pre-existing
limitation of the caller's return value handling and not made worse by
this change; the alternative (a NULL dereference crash) _would be_
strictly worse.

Pointed out by Coverity.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
When `prepare_midx_bitmap_git()` is called to load the bitmap for a
chained MIDX's base layer, if the base MIDX does not have an associated
bitmap file (e.g., it was not generated, or was deleted by gc), the
return value is NULL. It is then stored in `bitmap_git->base` and
immediately dereferenced on the next line.

This can happen in practice with incremental MIDX chains: the base MIDX
may have been written without `--write-bitmap-index`, or the bitmap may
have been pruned while the incremental layer's bitmap still references
it.

Check the return value and go to the cleanup label (which unmaps the
current bitmap and returns -1) so the caller falls back to non-bitmap
object enumeration, matching the handling of other bitmap loading
failures in the same function.

Pointed out by Coverity.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
When `refs_resolve_ref_unsafe()` is called to resolve HEAD, and returns
NULL (e.g., HEAD does not exist as a proper ref), the code falls back to
`repo_get_oid("HEAD")` to try to resolve the OID directly. If that
succeeds, execution continues with `head` still set to NULL.

Later, that variable is passed to `repo_get_oid()` and `starts_with()`,
both of which would dereference the NULL pointer.

A concrete trigger for `refs_resolve_ref_unsafe()` returning NULL while
`repo_get_oid()` succeeds could not be constructed against the ref
backends currently in the tree; the naive case (a symbolic HEAD pointing
at a nonexistent branch, in either the files or the reftable backend)
fails in both calls consistently and returns via the existing
`error(_("bad HEAD - I need a HEAD"))` path.  Coverity, however, flags
the leftover use of `head` after the outer `if (!head)` on a formal
reading: `head` is still NULL at that point, and both `starts_with(head,
...)` and the second `repo_get_oid(..., head, ...)` in the else-branch
would dereference it if that state were ever reached.

Removing the outer check would risk regressing to a crash if a future
ref backend ever manages to hit the "returns NULL for HEAD but has a
valid OID for HEAD" state.  Assigning the literal string "HEAD" as a
safe fallback documents the intent and satisfies the analyzer without
changing behavior in any code path we can currently reach.

Assisted-by: Claude Opus 4.7
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
After `write_one_shallow()` calls `lookup_commit()` to find the commit
object for a shallow graft entry, it then checks `if (!c || ...)`.
Inside that block, when the VERBOSE flag is set, it prints the OID being
removed, via `c->object.oid`. But `c` can be NULL (the first condition
in the `||` check).

This happens when a shallow graft entry references a commit object that
is not in the object store (e.g., after a partial fetch or in a
corrupted repository). In that case, `lookup_commit()` returns NULL
because the object cannot be found, the SEEN_ONLY check correctly
decides to remove this entry from .git/shallow, but the verbose message
crashes before the removal can complete.

Use `graft->oid` instead of `c->object.oid` for the message. The graft
entry's OID is the same value (it was used as the lookup key) and is
always available regardless of whether the commit object exists.

Pointed out by Coverity.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
The previous fix reuses the local `hex` variable that is already
computed at the top of `write_one_shallow()`. That works today, but
`oid_to_hex()` returns a pointer into a small rotating buffer, so it is
not stable across an unrelated call to `oid_to_hex()` from the same
thread. A future edit that adds such a call between the assignment and
the last user of `hex` would silently corrupt the output.

Move `write_one_shallow()` off the rotating buffer entirely by using a
local buffer instead. The current users of that `hex` variable are
unchanged.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Assisted-by: Claude Opus 4.7
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
@dscho dscho force-pushed the coverity-fixes-null-safety branch from 9f3a239 to 2ef74b5 Compare July 10, 2026 10:05
@dscho

dscho commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

/submit

@gitgitgadget

gitgitgadget Bot commented Jul 10, 2026

Copy link
Copy Markdown

Submitted as pull.2174.v2.git.1783683577.gitgitgadget@gmail.com

To fetch this version into FETCH_HEAD:

git fetch https://github.com/gitgitgadget/git/ pr-2174/dscho/coverity-fixes-null-safety-v2

To fetch this version to local tag pr-2174/dscho/coverity-fixes-null-safety-v2:

git fetch --no-tags https://github.com/gitgitgadget/git/ tag pr-2174/dscho/coverity-fixes-null-safety-v2

@gitgitgadget

gitgitgadget Bot commented Jul 10, 2026

Copy link
Copy Markdown

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

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

> This is a continuation of the effort I started in the patch series that
> became js/coverity-fixes. This next batch adds guards to avoid dereferencing
> NULL pointers and accessing NULL file descriptors.
>
> Changes since v1:
>
>  * Calling remote_tracking() no longer returns -1 when remote is NULL, but
>    instead BUG()s out.
>  * bisect_successful() returns with BISECT_FAILED instead of the -1 that
>    only worked by happenstance.
>  * The commit "revision: avoid dereferencing NULL in add_parents_only()" now
>    comes with a regression test.
>  * The commit "bisect: ensure non-NULL head before using it" no longer
>    claims that the fixed bug can be triggered with the current code base.
>  * The missing shallow commit's OID is no longer computed twice.
>  * A follow-up commit was folded into this patch series that lets
>    write_one_shallow() avoid the rolling buffers of oid_to_hex(), as
>    suggested by Junio. It technically does not fit the goal of this patch
>    series (fixing issues pointed out by Coverity), but was asked for
>    explicitly.
> ...
> Range-diff vs v1:
> ...

I found everything including the new patch good.  Unless others find
more issues in this round in a few days, let's mark the topic for
'next'.

Thanks.

@gitgitgadget

gitgitgadget Bot commented Jul 10, 2026

Copy link
Copy Markdown

This patch series was integrated into seen via git@df86bf5.

@gitgitgadget gitgitgadget Bot added the seen label Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant