私は、最近、破損したパックファイルを含むリポジトリを提示され、データが回復可能かどうかを尋ねられました。 この事後分析では、問題を調査して修正するために私が行った手順について説明します。 他の人がこのプロセスを興味深いと思うかもしれないし、同じ状況の誰かを助ける事になるかもしれないと思ったからです。

注意: このケースではリポジトリの適切なコピーは利用できませんでした。 破損したオブジェクトを他の場所から取得できる、はるかに簡単なケースについては、 this howto を参照してください。

私はfsckで始め、1つのオブジェクトで問題が見つかりました(出力を読みやすくするために、また、後で私が引用しやすくするために、以下で $pack と $obj を使用しました):

    $ git fsck
    error: $pack SHA1 checksum mismatch
    error: index CRC mismatch for object $obj from $pack at offset 51653873
    error: inflate: data stream error (incorrect data check)
    error: cannot unpack $obj from $pack at offset 51653873

パックチェックサムが失敗したということは、バイトがどこかで変更されたことを意味し、それはおそらく言及されたオブジェクトにあります(インデックスチェックサムとzlibの両方が失敗したため)。

私はzlibのソースコードを読んで、「incorrect data check」とは、zlibデータの最後にある adler-32 チェックサムが展開したデータと一致しなかったことを意味することがわかりました。 したがって、データをzlibを介してステップ実行しても、CRCが一致しないことが分かる最後の時点まで失敗しないので、役に立ちません。 問題のあるバイトは、オブジェクトデータのどこにでもある可能性があります。

私が最初にしたことは、壊れたデータをパックファイルから引き出すことでした。 私はオブジェクトの大きさを知る必要がありました:

    $ git show-index <$idx | cut -d' ' -f1 | sort -n | grep -A1 51653873
    51653873
    51664736

show-indexは、オブジェクトとそのオフセットのリストを提供します。 オフセット以外のすべてを破棄し、次にそれらを並べ替えて、興味深いオフセット(上記のfsck出力から取得)の直後に次のオブジェクトのオフセットが続くようにします。 これで、オブジェクトデータの長さが10863バイトであることがわかり、以下のコマンドで取得できます:

  dd if=$pack of=object bs=1 skip=51653873 count=10863

私はデータの16進ダンプを調べて、明らかなボゴシティを探しました(たとえば、4Kのゼロの連続はファイルシステムの破損の良い兆候です)。 しかし、すべてがかなり合理的に見えました。

「オブジェクト」ファイルは、zlibに直接送り込むには適していないことに注意してください。 可変長のgitパックオブジェクトヘッダーがあります。 zlibデータを直接操作できるように、これを取り除きます。 手動で作業するか(形式は gitformat-pack(5) で説明されています)、デバッガーで操作することができます。 私は後者を行い、以下のような有効なパックを作成しました:

    # pack magic and version
    printf 'PACK\0\0\0\2' >tmp.pack
    # pack has one object
    printf '\0\0\0\1' >>tmp.pack
    # now add our object data
    cat object >>tmp.pack
    # and then append the pack trailer
    /path/to/git.git/t/helper/test-tool sha1 -b <tmp.pack >trailer
    cat trailer >>tmp.pack

次に、デバッガーで git index-pack tmp.pack を実行します(unpack_raw_entryで停止します)。 これを行うと、3バイトのヘッダーがあることがわかりました(ヘッダー自体は適切なタイプとサイズでした)。 なので私はそれらを以下のように取り除きました:

    dd if=object of=zlib bs=1 skip=3

カスタムCプログラムを使用して、zlibでの解凍を介して結果を実行しました。 そして、それはエラーを報告はしましたが、正しい数の出力バイトを取得しました(つまり、上記でデコードしたgitのサイズヘッダーと一致しました)。 しかし、結果を git hash-object にフィードバックしても、同じsha1は生成されませんでした。 つまり、間違ったバイトがいくつかありましたが、どれかわからないということです。 このファイルはたまたまCのソースコードだったので、明らかに何か問題があることに気付くといいのですが、私は気づきませんでした。 それはコンパイルもできました!

また、リポジトリ内の同じパスの他のバージョンと比較してみました。意味のないdiffの一部があることを期待していました。 しかし残念ながら、これはたまたまリポジトリ内のこの特定のファイルの唯一のリビジョンであったため、比較するものは何もありませんでした。

なので私は別のアプローチを取りました。 破損は1バイトに限定されているという推測の下で、各バイトを個別に処理するプログラムを作成し、結果を解凍させてみました。 オブジェクトはわずか10Kに圧縮されていたため、約250万回の試行に成功し、それは数分かかりました。

私が使用したプログラムはコレです:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <zlib.h>

static int try_zlib(unsigned char *buf, int len)
{
        /* make this absurdly large so we don't have to loop */
        static unsigned char out[1024*1024];
        z_stream z;
        int ret;

        memset(&z, 0, sizeof(z));
        inflateInit(&z);

        z.next_in = buf;
        z.avail_in = len;
        z.next_out = out;
        z.avail_out = sizeof(out);

        ret = inflate(&z, 0);
        inflateEnd(&z);
        return ret >= 0;
}

/* eye candy */
static int counter = 0;
static void progress(int sig)
{
        fprintf(stderr, "\r%d", counter);
        alarm(1);
}

int main(void)
{
        /* oversized so we can read the whole buffer in */
        unsigned char buf[1024*1024];
        int len;
        unsigned i, j;

        signal(SIGALRM, progress);
        alarm(1);

        len = read(0, buf, sizeof(buf));
        for (i = 0; i < len; i++) {
                unsigned char c = buf[i];
                for (j = 0; j <= 0xff; j++) {
                        buf[i] = j;

                        counter++;
                        if (try_zlib(buf, len))
                                printf("i=%d, j=%x\n", i, j);
                }
                buf[i] = c;
        }

        alarm(0);
        fprintf(stderr, "\n");
        return 0;
}

私は以下のようにしてコンパイルして実行しました:

  gcc -Wall -Werror -O3 munge.c -o munge -lz
  ./munge <zlib

初期の段階でいくつかの誤検知がありました(zlibヘッダーに「no data」と記述した場合、zlibはそれで問題ないと見なします :) )。 しかし、私は途中でヒットしました:

  i=5642, j=c7

私はそれを最後まで実行させ、最後にさらにいくつかのヒットを取得しました(壊れたデータと一致するようにCRCを変更していました)。 したがって、この途中のヒットが問題の原因である可能性が高いです。

16進エディターでバイトを微調整し、結果をzlibで解凍して(エラーなし!)、出力を「git hash-object」にパイプして、壊れたオブジェクトのsha1を報告することで確認しました。 成功です!

私はパックファイル自体を以下のようにして修正しました:

  chmod +w $pack
  printf '\xc7' | dd of=$pack bs=1 seek=51659518 conv=notrunc
  chmod -w $pack

\xc7 は、「munge」プログラムが見つけた置換バイトに由来します。 オフセット 51659518 は、元のオブジェクトオフセット(51653873)を取得し、「munge」(5642)によって検出された置換オフセットを追加してから、削除した3バイトのgitヘッダーを追加して取得します。

その後、 「git fsck」はクリーンに実行されました。

破損自体に関しては、それが確かに1バイトであったことは幸運でした。 実際、それは1ビットであることが判明しました。 バイト0xc7 が 0xc5 に破損しました。 したがって、おそらくそれは欠陥のあるハードウェア、または宇宙線によって引き起こされたのでしょう。

そして、何が悪かったのかを見るために解凍した出力を見るという中止された試みは? 私は永遠に見て、それを見つけることができなかったかもしれません。 破損したデータを解凍したものと実際のデータの違いは以下のとおりです:

  -       cp = strtok (arg, "+");
  +       cp = strtok (arg, ".");

それは1バイトを微調整しましたが、それでも、まったく異なることをしただけの、有効で読み取り可能なC言語のソースコードになりました! 一つの収穫として、不運でない日であれば、ほとんどのランダムな変更はC言語のソースコードを壊してしまうので、zlibの出力を見ることは実際に役に立ったかもしれません。

しかし、さらに重要なことは、gitのハッシュとチェックサムは、別のシステムでは簡単に検出されない可能性のある問題に気付いた事です。その結果、コンパイルはできましたが、興味深いバグが発生したことでしょう(手当たりしだいのコミットのせいにされたかもしれません)。

The adventure continues…

私はまたやらかしてしまいました! 同じ事を、新しいハードウェアで。 この時点での想定は、古いディスクがpackfileを破損し、その破損が新しいハードウェアに移行されたというものです(rsyncなどによって行われ、移行時にfsckが行われなかったため)。

今回、影響を受けたブロブは20メガバイトを超えていましたが、これは大きすぎて力づくでとは行きませんでした。 私は上記の手順に従って、 zlib ファイルを作成しました。 次に、以下の inflate プログラムを使用して、破損したデータをそこからプルしました。 その出力を調べると、ファイルのどこに破損があったかについてのヒントが得られました。 しかし、今回はzlibの内容ではなく、ファイル自体を操作していました。 そのため、オブジェクトのsha1と破損のおおよその領域を知って、以下の sha1-munge プログラムを使用して、正しいバイトを力づくで割り出しました。

これが解凍(inflate)プログラムです(基本的には gunzip ですが、 .gz ヘッダー処理はありません):

#include <stdio.h>
#include <string.h>
#include <zlib.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
        /*
         * oversized so we can read the whole buffer in;
         * this could actually be switched to streaming
         * to avoid any memory limitations
         */
        static unsigned char buf[25 * 1024 * 1024];
        static unsigned char out[25 * 1024 * 1024];
        int len;
        z_stream z;
        int ret;

        len = read(0, buf, sizeof(buf));
        memset(&z, 0, sizeof(z));
        inflateInit(&z);

        z.next_in = buf;
        z.avail_in = len;
        z.next_out = out;
        z.avail_out = sizeof(out);

        ret = inflate(&z, 0);
        if (ret != Z_OK && ret != Z_STREAM_END)
                fprintf(stderr, "initial inflate failed (%d)\n", ret);

        fprintf(stderr, "outputting %lu bytes", z.total_out);
        fwrite(out, 1, z.total_out, stdout);
        return 0;
}

そして、以下が sha1-munge プログラムです:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <openssl/sha.h>
#include <stdlib.h>

/* eye candy */
static int counter = 0;
static void progress(int sig)
{
        fprintf(stderr, "\r%d", counter);
        alarm(1);
}

static const signed char hexval_table[256] = {
         -1, -1, -1, -1, -1, -1, -1, -1,                /* 00-07 */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* 08-0f */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* 10-17 */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* 18-1f */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* 20-27 */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* 28-2f */
          0,  1,  2,  3,  4,  5,  6,  7,                /* 30-37 */
          8,  9, -1, -1, -1, -1, -1, -1,                /* 38-3f */
         -1, 10, 11, 12, 13, 14, 15, -1,                /* 40-47 */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* 48-4f */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* 50-57 */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* 58-5f */
         -1, 10, 11, 12, 13, 14, 15, -1,                /* 60-67 */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* 68-67 */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* 70-77 */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* 78-7f */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* 80-87 */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* 88-8f */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* 90-97 */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* 98-9f */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* a0-a7 */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* a8-af */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* b0-b7 */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* b8-bf */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* c0-c7 */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* c8-cf */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* d0-d7 */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* d8-df */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* e0-e7 */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* e8-ef */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* f0-f7 */
         -1, -1, -1, -1, -1, -1, -1, -1,                /* f8-ff */
};

static inline unsigned int hexval(unsigned char c)
{
return hexval_table[c];
}

static int get_sha1_hex(const char *hex, unsigned char *sha1)
{
        int i;
        for (i = 0; i < 20; i++) {
                unsigned int val;
                /*
                 * hex[1]=='\0' is caught when val is checked below,
                 * but if hex[0] is NUL we have to avoid reading
                 * past the end of the string:
                 */
                if (!hex[0])
                        return -1;
                val = (hexval(hex[0]) << 4) | hexval(hex[1]);
                if (val & ~0xff)
                        return -1;
                *sha1++ = val;
                hex += 2;
        }
        return 0;
}

int main(int argc, char **argv)
{
        /* oversized so we can read the whole buffer in */
        static unsigned char buf[25 * 1024 * 1024];
        char header[32];
        int header_len;
        unsigned char have[20], want[20];
        int start, len;
        SHA_CTX orig;
        unsigned i, j;

        if (!argv[1] || get_sha1_hex(argv[1], want)) {
                fprintf(stderr, "usage: sha1-munge <sha1> [start] <file.in\n");
                return 1;
        }

        if (argv[2])
                start = atoi(argv[2]);
        else
                start = 0;

        len = read(0, buf, sizeof(buf));
        header_len = sprintf(header, "blob %d", len) + 1;
        fprintf(stderr, "using header: %s\n", header);

        /*
         * We keep a running sha1 so that if you are munging
         * near the end of the file, we do not have to re-sha1
         * the unchanged earlier bytes
         */
        SHA1_Init(&orig);
        SHA1_Update(&orig, header, header_len);
        if (start)
                SHA1_Update(&orig, buf, start);

        signal(SIGALRM, progress);
        alarm(1);

        for (i = start; i < len; i++) {
                unsigned char c;
                SHA_CTX x;

#if 0
                /*
                 * deletion -- this would not actually work in practice,
                 * I think, because we've already committed to a
                 * particular size in the header. Ditto for addition
                 * below. In those cases, you'd have to do the whole
                 * sha1 from scratch, or possibly keep three running
                 * "orig" sha1 computations going.
                 * JP: 削除 -- 私が考えるに、
                 * これは実際にはうまくいかないと思います。
                 * なぜなら、私たちはすでにヘッダーで特定のサイズに
                 * コミットしているからです。以下の追加(addition)も
                 * 同様です。このような場合、sha1全体を一からやり直すか、
                 * あるいは3つの「orig」sha1計算を実行し続ける必要が
                 * あるでしょう。
                 */
                memcpy(&x, &orig, sizeof(x));
                SHA1_Update(&x, buf + i + 1, len - i - 1);
                SHA1_Final(have, &x);
                if (!memcmp(have, want, 20))
                        printf("i=%d, deletion\n", i);
#endif

                /*
                 * replacement -- note that this tries each of the 256
                 * possible bytes. If you suspect a single-bit flip,
                 * it would be much shorter to just try the 8
                 * bit-flipped variants.
                 * JP: 置換 -- これは256の可能なバイトをそれぞれ
                 * 試行することに注意してください。もし1ビット反転が
                 * 疑われるなら、8ビット反転したものを試す方が
                 * ずっと短いでしょう。
                 */
                c = buf[i];
                for (j = 0; j <= 0xff; j++) {
                        buf[i] = j;

                        memcpy(&x, &orig, sizeof(x));
                        SHA1_Update(&x, buf + i, len - i);
                        SHA1_Final(have, &x);
                        if (!memcmp(have, want, 20))
                                printf("i=%d, j=%02x\n", i, j);
                }
                buf[i] = c;

#if 0
                /* addition */
                for (j = 0; j <= 0xff; j++) {
                        unsigned char extra = j;
                        memcpy(&x, &orig, sizeof(x));
                        SHA1_Update(&x, &extra, 1);
                        SHA1_Update(&x, buf + i, len - i);
                        SHA1_Final(have, &x);
                        if (!memcmp(have, want, 20))
                                printf("i=%d, addition=%02x", i, j);
                }
#endif

                SHA1_Update(&orig, buf + i, 1);
                counter++;
        }

        alarm(0);
        fprintf(stderr, "\r%d\n", counter);
        return 0;
}