blame: harden ignore-revs parser and tag peeling
Currently, blame.ignoreRevsFile and --ignore-revs-file only read paths explicitly configured by the user, which cannot point to an attacker-controlled file under Git's threat model. An upcoming commit will teach git-blame(1) and git-annotate(1) to automatically read the HEAD:.git-blame-ignore-revs blob by default if it exists, exposing the ignore-revs parser (oidset_parse_file_carefully() in oidset.c) and its tag-peeling callback (peel_to_commit_oid() in builtin/blame.c) to upstream-controlled content at a well-known path. Harden both code paths before enabling the default blob: - In oidset_parse_file_carefully(), strbuf_getline() reads up to the next newline and records the full line length in sb.len, including any embedded NUL bytes. However, strchr(sb.buf, '#') and parse_oid_hex_algop(sb.buf, &oid, &p, algop) treat sb.buf as a NUL-terminated string. If a line contains an embedded NUL byte after a valid object name (such as "<oid>\0garbage" or "<oid>\0# comment"), *p is '\0' and trailing bytes on the line are silently ignored. Reject any line containing an embedded NUL byte via memchr() before stripping comments and whitespace. - In peel_to_commit_oid(), odb_read_object_info() is called without OBJECT_INFO_SKIP_FETCH_OBJECT or OBJECT_INFO_QUICK, and deref_tag() calls parse_object() on tag targets without checking whether the target object exists locally first. In a partial clone, any missing commit OID or tag target listed in the ignore-revs file would trigger lazy promisor fetches and pack directory rescans during git-blame(1). Use odb_read_object_info_extended() with OBJECT_INFO_LOOKUP_REPLACE | OBJECT_INFO_SKIP_FETCH_OBJECT | OBJECT_INFO_QUICK and peel OBJ_TAG objects one layer per iteration, verifying that each target object exists locally and matches the tag's declared type before parsing it. Signed-off-by: Ravi Mistry <rmistry@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>seen
parent
6de20f6092
commit
f3dfec9283
|
|
@ -911,21 +911,37 @@ static int is_a_rev(const char *name)
|
|||
static int peel_to_commit_oid(struct object_id *oid_ret, void *cbdata)
|
||||
{
|
||||
struct repository *r = ((struct blame_scoreboard *)cbdata)->repo;
|
||||
enum object_type expected_type = OBJ_ANY;
|
||||
struct object_id oid;
|
||||
|
||||
oidcpy(&oid, oid_ret);
|
||||
while (1) {
|
||||
unsigned flags = OBJECT_INFO_LOOKUP_REPLACE |
|
||||
OBJECT_INFO_SKIP_FETCH_OBJECT |
|
||||
OBJECT_INFO_QUICK;
|
||||
struct object_info oi = OBJECT_INFO_INIT;
|
||||
enum object_type kind;
|
||||
struct object *obj;
|
||||
int kind = odb_read_object_info(r->objects, &oid, NULL);
|
||||
|
||||
oi.typep = &kind;
|
||||
if (odb_read_object_info_extended(r->objects, &oid, &oi,
|
||||
flags) < 0)
|
||||
return -1;
|
||||
if (expected_type != OBJ_ANY && kind != expected_type)
|
||||
return -1;
|
||||
if (kind == OBJ_COMMIT) {
|
||||
oidcpy(oid_ret, &oid);
|
||||
return 0;
|
||||
}
|
||||
if (kind != OBJ_TAG)
|
||||
return -1;
|
||||
obj = deref_tag(r, parse_object(r, &oid), NULL, 0);
|
||||
obj = parse_object(r, &oid);
|
||||
if (!obj || obj->type != OBJ_TAG)
|
||||
return -1;
|
||||
obj = ((struct tag *)obj)->tagged;
|
||||
if (!obj)
|
||||
return -1;
|
||||
expected_type = obj->type;
|
||||
oidcpy(&oid, &obj->oid);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
3
oidset.c
3
oidset.c
|
|
@ -85,6 +85,9 @@ void oidset_parse_file_carefully(struct oidset *set, const char *path,
|
|||
const char *p;
|
||||
const char *name;
|
||||
|
||||
if (memchr(sb.buf, '\0', sb.len))
|
||||
die("invalid object name: %s", sb.buf);
|
||||
|
||||
/*
|
||||
* Allow trailing comments, leading whitespace
|
||||
* (including before commits), and empty or whitespace
|
||||
|
|
|
|||
|
|
@ -327,4 +327,42 @@ test_expect_success ignore_merge '
|
|||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_expect_success 'ignore-revs-file rejects lines with embedded NUL bytes' '
|
||||
rev_b=$(git rev-parse B) &&
|
||||
printf "%sQgarbage\n" "$rev_b" | q_to_nul >ignore_nul &&
|
||||
test_must_fail git blame file --ignore-revs-file ignore_nul 2>err &&
|
||||
test_grep "invalid object name:" err &&
|
||||
|
||||
printf "%sQ# comment\n" "$rev_b" | q_to_nul >ignore_nul_comment &&
|
||||
test_must_fail git blame file --ignore-revs-file ignore_nul_comment 2>err &&
|
||||
test_grep "invalid object name:" err
|
||||
'
|
||||
|
||||
test_expect_success 'ignore-revs-file peels chained tags and skips missing tag targets' '
|
||||
test_write_lines BB L2-modified L3 L4 L5 L6 L7 L8 CC >file &&
|
||||
git add file &&
|
||||
test_tick &&
|
||||
git commit -m D &&
|
||||
git tag -a -m "tag 1" D_TAG1 HEAD &&
|
||||
git tag -a -m "tag 2" D_TAG2 D_TAG1 &&
|
||||
git rev-parse D_TAG2 >ignore_tag_chain &&
|
||||
git blame --line-porcelain file --ignore-revs-file ignore_tag_chain >blame_raw &&
|
||||
sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 2/s/ .*//p" blame_raw >actual &&
|
||||
git rev-parse A >expect &&
|
||||
test_cmp expect actual &&
|
||||
|
||||
test_config extensions.partialClone origin &&
|
||||
test_config remote.origin.promisor true &&
|
||||
test_config remote.origin.url /nonexistent &&
|
||||
missing_oid=$(test_oid deadbeef) &&
|
||||
bad_tag=$(printf "object %s\ntype commit\ntag bad-tag\ntagger T <t@example.com> 0 +0000\n\nmsg\n" "$missing_oid" |
|
||||
git hash-object -t tag -w --stdin) &&
|
||||
test_write_lines "$missing_oid" "$bad_tag" >ignore_bad_tag &&
|
||||
git blame --line-porcelain file --ignore-revs-file ignore_bad_tag >blame_raw 2>err &&
|
||||
test_must_be_empty err &&
|
||||
sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 2/s/ .*//p" blame_raw >actual &&
|
||||
git rev-parse HEAD >expect &&
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_done
|
||||
|
|
|
|||
Loading…
Reference in New Issue