Misaligned `uint32_t` load in `sha1_compression_states` when hashing a git object whose header length is not a multiple of 4

未關閉
#7,350 0 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視

還沒有人認領這個 Issue。

評估

難度
3/5
預估耗時
1-2 天
新手友好度
68/100
Issue 類型
缺陷
描述清晰度
描述清楚
活躍度
冷清
技術堆疊
c, git
領域
cryptography

研究方向

從 src/util/hash/sha1dc/sha1.c 中的 SHA1DCUpdate() 開始,檢查 SHA1DC_ALLOW_UNALIGNED_ACCESS 如何選擇區塊處理路徑;檢查從 src/util/hash.c:git_hash_vec() 開始的呼叫鏈。使用 sanitizer 建置和提供的 PoC 重現問題,然後確認對相同物件進行雜湊時不再回報未對齊載入,並且仍會傳回預期的物件 ID。

由索引模型根據 Issue 內容生成。

描述

Summary

When git_object_id_from_buffer() computes the object ID of a blob whose serialized header length is not a multiple of 4, the SHA1DC implementation performs a const uint32_t load from a misaligned address. SHA1DCUpdate() (src/util/hash/sha1dc/sha1.c:1843) casts the input buffer pointer directly to uint32_t* and passes it to sha1_process()sha1_compression_states() (sha1.c:391), which dereferences it.

The misalignment is manufactured inside the library, not by the caller. git_hash_vec() hashes the git object header ("blob <size>\0") and the data as two sequential vectors. When the header length is not a multiple of 4, the SHA1 context's internal total counter becomes non-multiple-of-4, so the "fill" step in SHA1DCUpdate() advances the data pointer by a non-multiple-of-4 offset (fill = 64 - left). The subsequent while (len >= 64) loop then casts this now-misaligned pointer to uint32_t*.

The caller passes a properly aligned std::vector<uint8_t> buffer to a public, non-deprecated API. No caller contract is violated. The root cause is that SHA1DC_ALLOW_UNALIGNED_ACCESS is defined unconditionally on x86 (sha1.c:32 → :126 → :127) with no effective platform guard, compiling out the safe memcpy-into-aligned-buffer path (sha1.c:1845-1846) that already exists in the same function.

Version

$ git describe --tags
v1.9.0-429-g32b564e63

Pinned commit: 32b564e63f9639eaf5ee90fb7a95b3a650156cbd

Description

SHA1DCUpdate() processes incoming data in 64-byte blocks. When a partial block is already buffered (left = ctx->total & 63), it first copies fill = 64 - left bytes from the new buffer into the aligned ctx->buffer to complete a block, then advances buf by fill:

// sha1.c:1819-1856 (abridged)
void SHA1DCUpdate(SHA1_CTX *ctx, const char *buf, size_t len)
{
    unsigned left, fill;
    if (len == 0) return;
    left = ctx->total & 63;
    fill = 64 - left;

    if (left && len >= fill) {
        ctx->total += fill;
        memcpy(ctx->buffer + left, buf, fill);          // safe: into aligned buffer
        sha1_process(ctx, (uint32_t*)(ctx->buffer));    // safe: ctx->buffer is aligned
        buf += fill;                                    // <-- buf advances by `fill`
        len -= fill;
        left = 0;
    }
    while (len >= 64)
    {
        ctx->total += 64;
#if defined(SHA1DC_ALLOW_UNALIGNED_ACCESS)
        sha1_process(ctx, (uint32_t*)(buf));            // <-- CRASH: buf may be misaligned
#else
        memcpy(ctx->buffer, buf, 64);                   // safe path (dead on x86)
        sha1_process(ctx, (uint32_t*)(ctx->buffer));
#endif
        buf += 64;
        len -= 64;
    }
    ...
}

git_hash_vec() calls SHA1DCUpdate() once for the header and once for the data:

// src/util/hash.c:118-142 (abridged)
int git_hash_vec(unsigned char *out, git_str_vec *vec, size_t n, git_hash_algorithm_t algorithm)
{
    git_hash_ctx ctx;
    ...
    for (i = 0; i < n; i++) {
        if ((error = git_hash_update(&ctx, vec[i].data, vec[i].len)) < 0)   // header, then data
            goto done;
    }
    ...
}

When the header length is not a multiple of 4, ctx->total after the header is non-multiple-of-4. For the data vector, fill = 64 - (total % 64) is then also non-multiple-of-4, so after buf += fill the data pointer is misaligned for uint32_t. The while (len >= 64) loop then dereferences it as uint32_t*.

The header is built by git_odb__format_object_header():

// src/libgit2/odb.c:86-106 (abridged)
int git_odb__format_object_header(size_t *written, char *hdr, size_t hdr_size,
                                  git_object_size_t obj_len, git_object_t obj_type)
{
    const char *type_str = git_object_type2string(obj_type);   // "blob"
    ...
    len = p_snprintf(hdr, hdr_max, "%s %"PRId64, type_str, (int64_t)obj_len);
    *written = (size_t)(len + 1);   // includes the trailing NUL
    ...
}

So a blob of size 128 produces the header "blob 128\0" = 9 bytes. 9 % 4 = 1, triggering the misalignment. The data length (128) is chosen so that after the fill step (fill = 64 - 9 = 55), enough data remains (128 - 55 = 73 ≥ 64) to enter the misaligned while loop.

PoC Code

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <vector>
#include "git2.h"

/*
 * Object data length. The resulting header is "blob 128\0" = 9 bytes.
 * 9 % 4 = 1, so the header length is not a multiple of 4. After SHA1DCUpdate
 * processes the 9-byte header, the data vector's fill offset is
 * 64 - 9 = 55 (55 % 4 = 3), leaving the data pointer misaligned for uint32_t.
 * 128 - 55 = 73 >= 64, so the misaligned sha1_process() loop is entered.
 */
static const size_t OBJ_LEN = 128;

int main(void)
{
    if (git_libgit2_init() < 0) {
        fprintf(stderr, "git_libgit2_init failed\n");
        return 1;
    }

    /* std::vector data is max_align_t-aligned (16 on x86-64), so the base
     * is 4-byte aligned; only the internal fill offset makes it misaligned. */
    std::vector<unsigned char> data(OBJ_LEN, 'A');

    /* Public API. opts zeroed by GIT_OBJECT_ID_OPTIONS_INIT;
     * object_type left 0 -> normalize_options() defaults it to GIT_OBJECT_BLOB,
     * oid_type left 0 -> defaults to GIT_OID_DEFAULT (SHA1). */
    git_object_id_options opts = GIT_OBJECT_ID_OPTIONS_INIT;
    opts.object_type = GIT_OBJECT_BLOB;

    git_oid oid;
    /* Hashes a blob. Triggers the misaligned load inside SHA1DC. */
    int rc = git_object_id_from_buffer(&oid, data.data(), data.size(), &opts);

    printf("git_object_id_from_buffer returned %d\n", rc);

    git_libgit2_shutdown();
    return 0;
}

Stack Trace

src/util/hash/sha1dc/sha1.c:391:2: runtime error: load of misaligned address 0x775d233e0077 for type 'const uint32_t' (aka 'const unsigned int'), which requires 4 byte alignment
0x775d233e0077: note: pointer points here
 41 41 41 41 41  41 41 41 41 41 41 41 41  41 41 41 41 41 41 41 41  41 41 41 41 41 41 41 41  41 41 41
             ^ 
    #0 0x6482555b08e2 in sha1_compression_states src/util/hash/sha1dc/sha1.c:391:2
    #1 0x6482555b1cc4 in sha1_process src/util/hash/sha1dc/sha1.c:1725:2
    #2 0x6482555b1611 in SHA1DCUpdate src/util/hash/sha1dc/sha1.c:1843:3
    #3 0x64825558e719 in git_hash_sha1_update src/util/hash/collisiondetect.c:35:2
    #4 0x6482555563b8 in git_hash_update src/util/hash.c:72:10
    #5 0x648255556a25 in git_hash_vec src/util/hash.c:132:16
    #6 0x6482553d112b in id_from_buffer src/libgit2/object.c:875:9
    #7 0x6482553ccd81 in git_object_id_from_buffer src/libgit2/object.c:902:10
    #8 0x6482553b9865 in main poc.cpp:36:14
    #9 0x7a9d240021c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #10 0x7a9d2400228a in __libc_start_main csu/../csu/libc-start.c:360:3
    #11 0x6482552ce6f4 in _start (poc+0x3eb6f4) (BuildId: a72440934c9e1f00dcd374ca91386c810be789ee)

SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior src/util/hash/sha1dc/sha1.c:391:2 

Reproduction Step

# 1. Checkout libgit2 at the version above
git clone https://github.com/libgit2/libgit2.git
cd libgit2
git checkout 32b564e63f9639eaf5ee90fb7a95b3a650156cbd

# 2. Build the library with ASan+UBSan
SAN_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -ftrivial-auto-var-init=zero -g -O0 -DSHA1DC_FORCE_UNALIGNED_ACCESS"
CC=/usr/bin/clang CXX=/usr/bin/clang++ \
CFLAGS="$SAN_FLAGS" CXXFLAGS="$SAN_FLAGS" LDFLAGS="-fsanitize=address,undefined" \
cmake -S . -B build_san \
  -DBUILD_TESTS=OFF -DBUILD_FUZZERS=OFF -DBUILD_SHARED_LIBS=OFF \
  -DBUILD_CLAR=OFF -DCMAKE_BUILD_TYPE=Debug \
  -DUSE_SSH=OFF -DUSE_HTTPS=SecureTransport -DUSE_HTTP_PARSER=builtin \
  -DREGEX_BACKEND=builtin -DUSE_BUNDLED_ZLIB=ON
cmake --build build_san -j

# 3. Build the standalone PoC against the sanitizer build
clang++ -g -O0 -fstandalone-debug -fno-omit-frame-pointer \
    -ftrivial-auto-var-init=zero -fsanitize=address,undefined,fuzzer-no-link \
    -Ibuild_san/include poc.cpp -o poc \
    build_san/libgit2.a \
    -framework CoreFoundation -framework Security -framework GSS -lz -liconv

# 4. Run
ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=0 ./poc

The crash is deterministic and reproduces on every run.

Submission Statement

This report was produced by FuzzAnything's AI-assisted library fuzzer and manually verified by a team member. We reviewed the PoC against the upstream API documentation — call order, parameters, and memory ownership — and found no API misuse.

Signed-off-by: FuzzAnything fuzzanything@gmail.com

主要語言
C
星號
10.6k
分支
2.7k
PR 合併指標
30 天內沒有已合併 PR

貢獻指南

開啟貢獻指南

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

libgit2/libgit2 的其他 Issue

查看 libgit2/libgit2 的全部 Issue

相似的 Issue

更多 C Issue

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。