dm-verity — Merkle Tree 기반 블록 검증
dm-verity는 Linux Device Mapper 프레임워크 위에서 Merkle Tree 기반의 읽기 전용 블록 무결성 검증을 제공합니다.
Android Verified Boot(AVB)의 핵심 메커니즘으로, 빌드 시 생성된 Merkle Tree의 루트 해시를
런타임에 블록 단위로 검증하여 시스템 파티션의 변조를 탐지합니다.
Forward Error Correction(FEC)을 통한 자동 복구, dm-verity-loadpin을 통한 모듈 로드 제한,
그리고 배치 검증 최적화까지 커널 내부 구현(drivers/md/dm-verity-target.c)을 심층 분석합니다.
- dm-integrity — 읽기/쓰기 무결성 보호, 저널링, AEAD 결합
- dm-crypt — 블록 레벨 투명 암호화, LUKS, 성능 최적화
- Device Mapper / LVM — DM 프레임워크 기본
핵심 요약
- Device Mapper — 블록 디바이스 위에 가상 매핑(Mapping) 레이어를 제공하는 커널 프레임워크
- dm-verity — Merkle Tree 기반 읽기 전용(Read-Only) 블록 무결성 검증 (Android Verified Boot 핵심)
- FEC (Forward Error Correction) — Reed-Solomon 부호를 사용한 손상 블록 자동 복구
- dm-verity-loadpin — dm-verity로 검증된 파티션에서만 커널 모듈/펌웨어 로드 허용
단계별 이해
- Device Mapper 레이어 이해
커널의 블록 I/O 스택에서 DM은 bio를 가로채 target 드라이버에 전달합니다. - dm-verity로 읽기 검증
빌드 시 Merkle Tree를 생성하고, 런타임에 블록 단위로 해시를 검증합니다. - Android Verified Boot 체인
부트로더 → vbmeta 서명 검증 → 커널 cmdline에 루트 해시 전달 → dm-verity 활성화 순서로 무결성을 보장합니다. - FEC로 자동 복구
Reed-Solomon 부호를 추가하여 비트 에러로 인한 검증 실패 시 자동 복구를 시도합니다. - LoadPin으로 모듈 로드 제한
dm-verity로 검증된 파티션에서만 커널 모듈과 펌웨어를 로드하도록 강제합니다.
Device Mapper 개요
Device Mapper(DM)는 Linux 커널의 블록 레이어에서 가상 블록 디바이스를 생성하는 프레임워크입니다.
drivers/md/dm.c에 구현된 DM 코어는 struct mapped_device로 가상 디바이스를 표현하고,
struct dm_table로 매핑 테이블을 관리합니다. 각 target 드라이버(struct target_type)는
특정 기능(암호화, 검증, 스냅샷 등)을 구현하며, DM 코어가 bio를 적절한 target에 라우팅(Routing)합니다.
DM은 원래 Heinz Mauelshagen이 LVM(Logical Volume Manager)을 위해 개발했으며,
현재는 dm-crypt, dm-verity, dm-integrity, dm-thin, dm-cache, dm-snapshot 등
다양한 target을 통해 리눅스 스토리지 스택의 핵심 계층으로 자리잡았습니다.
사용자 공간(User Space)에서는 dmsetup, cryptsetup, veritysetup,
integritysetup 등의 도구로 DM 디바이스를 관리합니다.
DM 코어의 핵심 자료 구조를 살펴봅니다:
/* include/linux/device-mapper.h */
struct target_type {
uint64_t features;
const char *name;
struct module *module;
int (*ctr)(struct dm_target *ti, unsigned argc, char **argv);
void (*dtr)(struct dm_target *ti);
int (*map)(struct dm_target *ti, struct bio *bio);
int (*end_io)(struct dm_target *ti, struct bio *bio,
blk_status_t *error);
void (*status)(struct dm_target *ti, status_type_t type,
unsigned status_flags, char *result,
unsigned maxlen);
int (*iterate_devices)(struct dm_target *ti,
iterate_devices_callout_fn fn, void *data);
};
struct dm_target {
struct dm_table *table;
struct target_type *type;
sector_t begin; /* 매핑 시작 섹터 */
sector_t len; /* 매핑 길이 (섹터) */
void *private; /* target 별 사적 데이터 */
unsigned num_discard_bios;
unsigned num_secure_erase_bios;
unsigned num_write_zeroes_bios;
};
DM의 I/O 경로는 다음과 같이 진행됩니다: 사용자 공간에서 가상 디바이스(/dev/dm-N)에 I/O를 발행하면,
dm_submit_bio()가 호출됩니다. 이 함수는 dm_table에서 bio가 속하는 target을 찾아
해당 target의 map() 콜백(Callback)을 호출합니다. target은 bio를 변환(remap, split, encrypt 등)하여
하위 디바이스로 전달합니다. map() 콜백의 반환값에 따라 DM 코어가 bio의 후속 처리를 결정합니다:
DM_MAPIO_REMAPPED는 bio가 하위 디바이스로 redirect되었음을,
DM_MAPIO_SUBMITTED는 target이 bio를 완전히 처리했음을 의미합니다.
| DM 구성 요소 | 역할 | 소스 파일 |
|---|---|---|
mapped_device | 가상 블록 디바이스 표현 | drivers/md/dm.c |
dm_table | target 매핑 테이블 | drivers/md/dm-table.c |
dm_target | 개별 매핑 세그먼트 | drivers/md/dm.h |
dm_io | I/O 요청 추적 | drivers/md/dm.c |
dm_ioctl | 사용자 공간 인터페이스 | drivers/md/dm-ioctl.c |
dm_bufio | 해시/메타데이터 블록 캐시(Cache) | drivers/md/dm-bufio.c |
시작섹터 길이 target_name [target_args...] 형식입니다.
예: 0 2097152 verity 1 /dev/sda1 /dev/sda2 4096 4096 262144 1 sha256 root_hash salt
/* drivers/md/dm-ioctl.c - 주요 ioctl 명령 */
/* 사용자 공간 (dmsetup) <-> 커널 DM 인터페이스 */
/* DM_DEV_CREATE: 새 mapped_device 생성 */
/* DM_DEV_REMOVE: mapped_device 제거 */
/* DM_TABLE_LOAD: 매핑 테이블 로드 */
/* DM_DEV_SUSPEND: I/O 일시 중지 (테이블 교체 준비) */
/* DM_DEV_STATUS: 디바이스 상태 조회 */
/* DM_TABLE_STATUS: 테이블 상태 (dmsetup status) */
/* DM_TARGET_MSG: target에 메시지 전달 (dmsetup message) */
static int table_load(struct file *filp,
struct dm_ioctl *param,
size_t param_size)
{
struct dm_table *t;
struct mapped_device *md;
/* 1. 테이블 생성 */
r = dm_table_create(&t, get_mode(param), param->target_count, md);
/* 2. 각 target 파싱 및 추가 */
for (i = 0; i < param->target_count; i++) {
dm_table_add_target(t, type_name, start, len, params);
/* 내부에서 target_type->ctr() 호출 */
}
/* 3. 테이블 완료 (무결성 검증) */
r = dm_table_complete(t);
/* 4. 테이블을 mapped_device에 연결 */
dm_swap_table(md, t);
return 0;
}
/* bio 라우팅 - dm_submit_bio() 핵심 경로 */
static void dm_submit_bio(struct bio *bio)
{
struct mapped_device *md = bio->bi_bdev->bd_disk->private_data;
struct dm_table *map;
struct dm_target *ti;
map = dm_get_live_table(md, &srcu_idx);
/* bio가 속하는 target 찾기 */
ti = dm_table_find_target(map, bio->bi_iter.bi_sector);
/* target의 map() 콜백 호출 */
r = ti->type->map(ti, bio);
switch (r) {
case DM_MAPIO_SUBMITTED:
break; /* target이 처리 완료 */
case DM_MAPIO_REMAPPED:
submit_bio_noacct(bio); /* 하위 디바이스로 전달 */
break;
case DM_MAPIO_REQUEUE:
bio_io_error(bio); /* 재시도 */
break;
}
dm_put_live_table(md, srcu_idx);
}
물리 디바이스 -> dm-integrity -> dm-crypt -> ext4
이 경우 각 레이어의 map()이 순서대로 호출됩니다.
스택 깊이가 깊을수록 I/O 지연(Latency)이 증가하므로, AEAD 모드처럼
단일 레이어에서 암호화+무결성을 처리하는 것이 성능상 유리합니다.
dm-verity 개요
dm-verity는 읽기 전용 블록 디바이스의 무결성을 검증하는 Device Mapper target입니다.
ChromeOS에서 최초 도입되어 Android Verified Boot(AVB)의 핵심 메커니즘으로 자리잡았습니다.
커널 소스 drivers/md/dm-verity-target.c에 구현되어 있으며,
빌드 시점에 생성된 Merkle Tree의 루트 해시를 런타임에 블록 단위로 검증합니다.
dm-verity의 핵심 원리는 간단합니다: 디스크의 모든 4KB 블록에 대해 SHA-256(또는 다른 해시) 값을 계산하고, 이 해시들을 다시 해싱하여 트리 구조를 만듭니다. 최종 루트 해시 하나만 신뢰할 수 있으면 전체 디스크의 무결성을 검증할 수 있습니다. 읽기 요청이 들어올 때마다 해당 블록의 해시를 Merkle Tree에서 확인하여 변조를 탐지합니다.
/* drivers/md/dm-verity.h */
struct dm_verity {
struct dm_dev *data_dev; /* 데이터 디바이스 */
struct dm_dev *hash_dev; /* 해시 트리 디바이스 */
struct dm_target *ti;
struct dm_verity_fec *fec; /* FEC 옵션 */
unsigned int data_dev_block_bits; /* 데이터 블록 크기 비트 */
unsigned int hash_dev_block_bits; /* 해시 블록 크기 비트 */
unsigned int hash_per_block_bits; /* 블록당 해시 수 비트 */
unsigned int levels; /* 트리 레벨 수 */
unsigned int digest_size; /* 해시 출력 크기 */
unsigned int salt_size;
const char *alg_name; /* "sha256" 등 */
u8 *root_digest; /* 루트 해시 */
u8 *salt; /* salt 값 */
sector_t data_blocks; /* 총 데이터 블록 수 */
sector_t hash_start; /* 해시 영역 시작 */
sector_t hash_blocks; /* 해시 블록 수 */
enum verity_mode mode; /* EIO, logging, restart, panic */
unsigned int validated_blocks;
struct workqueue_struct *verify_wq;
};
DM_VERITY_MODE_EIO(기본): 검증 실패 시 -EIO 반환,
DM_VERITY_MODE_LOGGING: 로그만 기록하고 I/O 허용,
DM_VERITY_MODE_RESTART: 시스템 재부팅,
DM_VERITY_MODE_PANIC: 커널 패닉(Kernel Panic) 유발
| 속성 | dm-verity | 비교 대상 |
|---|---|---|
| 방향 | 읽기 전용 | dm-integrity: 읽기/쓰기 |
| 검증 단위 | 블록 (4KB 기본) | 파일 단위: fs-verity |
| 해시 저장 | 별도 파티션/영역 | dm-integrity: 인라인 태그 |
| 주요 용도 | Verified Boot, 컨테이너(Container) 이미지 | 서버 디스크 무결성 |
| 오버헤드(Overhead) | 읽기 시 해시 계산 + 트리 탐색 | 읽기/쓰기 모두 오버헤드 |
| 커널 소스 | dm-verity-target.c | dm-integrity.c |
Merkle Tree 구조
dm-verity의 Merkle Tree는 데이터 블록들의 해시를 계층적으로 구성합니다. 리프(leaf) 레벨에서 각 데이터 블록(4KB)의 해시(SHA-256 기준 32바이트)를 계산하고, 이 해시들을 하나의 해시 블록(역시 4KB)에 모아 다시 해싱합니다. 4KB 블록에 32바이트 해시가 128개 들어가므로, 각 레벨에서 팬아웃(fan-out)은 128입니다. 1GB 데이터의 경우 SHA-256 기준으로 약 8MB + 64KB + 512B + 32B = 약 8.06MB의 해시 공간이 필요하며, 이는 전체 데이터의 약 0.8%에 불과합니다.
/* Merkle Tree 레벨별 블록 수 계산 (커널 내부) */
/* drivers/md/dm-verity-target.c: verity_ctr() */
static int verity_hash_levels(struct dm_verity *v)
{
sector_t hash_position;
int levels = 0;
sector_t data_blocks = v->data_blocks;
/* hash_per_block = 블록_크기 / digest_size
* SHA-256, 4KB 블록: 4096/32 = 128 */
while (data_blocks > 1) {
data_blocks = DIV_ROUND_UP(data_blocks,
1 << v->hash_per_block_bits);
levels++;
}
v->levels = levels;
/* 각 레벨의 해시 블록 시작 위치 계산 */
hash_position = 0;
for (int i = levels - 1; i >= 0; i--) {
v->hash_level_block[i] = hash_position;
data_blocks = DIV_ROUND_UP(v->data_blocks,
(sector_t)1 << (v->hash_per_block_bits * (i + 1)));
hash_position += data_blocks;
}
return 0;
}
veritysetup format의 --hash 옵션으로 지정합니다.
dm-verity 설정
dm-verity를 설정하려면 먼저 veritysetup 도구로 Merkle Tree를 생성하고,
이를 DM 테이블에 로드합니다. veritysetup은 cryptsetup 패키지에 포함되어 있습니다.
설정 과정은 오프라인 해시 생성과 온라인 검증 활성화 2단계로 나뉩니다.
# 1. 데이터 디바이스에 대한 해시 트리 생성
$ veritysetup format /dev/sda1 /dev/sda2
VERITY header information for /dev/sda2
UUID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Hash type: 1
Data blocks: 262144
Data block size: 4096
Hash block size: 4096
Hash algorithm: sha256
Salt: abcdef0123456789abcdef0123456789...
Root hash: 4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d...
# 2. dm-verity 디바이스 활성화
$ veritysetup open /dev/sda1 verified_root /dev/sda2 \
4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d...
# 3. 읽기 전용으로 마운트
$ mount -o ro /dev/mapper/verified_root /mnt/verified
# 4. dmsetup으로 직접 설정하는 경우
$ echo "0 2097152 verity 1 /dev/sda1 /dev/sda2 4096 4096 \
262144 1 sha256 \
4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d... \
abcdef0123456789abcdef0123456789..." | dmsetup create verified_root
| 필드 | 설명 | 예시 값 |
|---|---|---|
| version | verity 포맷 버전 (현재 1) | 1 |
| data_dev | 데이터 블록 디바이스 | /dev/sda1 |
| hash_dev | 해시 트리 디바이스 | /dev/sda2 |
| data_block_size | 데이터 블록 크기 | 4096 |
| hash_block_size | 해시 블록 크기 | 4096 |
| num_data_blocks | 데이터 블록 수 | 262144 |
| hash_start_block | 해시 데이터 시작 블록 | 1 |
| algorithm | 해시 알고리즘 | sha256 |
| root_hash | Merkle Tree 루트 해시 | 64자 hex |
| salt | 해시 솔트 (선택) | hex 또는 - |
커널 내부 구현
dm-verity의 커널 구현은 drivers/md/dm-verity-target.c에 있습니다.
핵심 함수는 verity_map()과 verity_end_io()이며,
워크큐 기반으로 해시 검증을 수행합니다. 읽기 bio가 도착하면 verity_map()이
bi_end_io를 교체하고 데이터 디바이스로 remap합니다. 데이터 읽기가 완료되면
verity_end_io()가 워크큐에서 해시 검증을 스케줄합니다.
/* drivers/md/dm-verity-target.c */
static int verity_map(struct dm_target *ti, struct bio *bio)
{
struct dm_verity *v = ti->private;
struct dm_verity_io *io;
/* 쓰기 요청은 거부 (읽기 전용) */
if (bio_data_dir(bio) == WRITE) {
bio->bi_status = BLK_STS_IOERR;
bio_endio(bio);
return DM_MAPIO_SUBMITTED;
}
io = dm_per_bio_data(bio, ti->per_io_data_size);
io->v = v;
io->orig_bi_end_io = bio->bi_end_io;
io->block = bio->bi_iter.bi_sector >> (v->data_dev_block_bits - SECTOR_SHIFT);
io->n_blocks = bio->bi_iter.bi_size >> v->data_dev_block_bits;
bio->bi_end_io = verity_end_io;
bio_set_dev(bio, v->data_dev->bdev);
verity_prefetch_io(v, io);
return DM_MAPIO_REMAPPED;
}
static void verity_end_io(struct bio *bio)
{
struct dm_verity_io *io = dm_per_bio_data(bio, ...);
if (bio->bi_status) {
verity_finish_io(io, bio->bi_status);
return;
}
INIT_WORK(&io->work, verity_work);
queue_work(io->v->verify_wq, &io->work);
}
verity_verify_io()는 각 데이터 블록에 대해 bottom-up으로 Merkle Tree를 순회합니다:
데이터 블록의 해시를 계산하고, 이를 Level 0 해시와 비교한 뒤,
Level 0 해시 블록의 해시를 Level 1과 비교하는 방식으로 루트까지 올라갑니다.
한 번 검증된 해시 블록은 dm_bufio 캐시에 보관되어 재검증을 피합니다.
/* drivers/md/dm-verity-target.c - verity_verify_io() 핵심 */
static int verity_verify_io(struct dm_verity_io *io)
{
struct dm_verity *v = io->v;
struct bvec_iter start;
unsigned int b;
for (b = 0; b < io->n_blocks; b++) {
int r;
sector_t cur_block = io->block + b;
struct ahash_request *req = verity_io_hash_req(v, io);
/* 1단계: 데이터 블록 해시 계산 */
r = verity_hash_for_block(v, io, cur_block,
verity_io_want_digest(v, io),
&is_zero);
if (unlikely(r < 0))
return r;
/* 2단계: Merkle Tree 검증 (bottom-up) */
r = verity_verify_level(v, io, cur_block, 0,
verity_io_want_digest(v, io));
if (r)
return r;
}
return 0;
}
/* 해시 블록 prefetch - 성능 최적화 핵심 */
static void verity_prefetch_io(struct dm_verity *v,
struct dm_verity_io *io)
{
int i;
for (i = v->levels - 1; i >= 0; i--) {
sector_t hash_block_start, hash_block_end;
/* 이 bio가 접근할 해시 블록 범위 계산 */
hash_block_start = io->block >> (v->hash_per_block_bits * (i + 1));
hash_block_end = (io->block + io->n_blocks - 1) >>
(v->hash_per_block_bits * (i + 1));
hash_block_start += v->hash_level_block[i];
hash_block_end += v->hash_level_block[i];
/* dm_bufio를 통해 비동기 prefetch */
dm_bufio_prefetch(v->bufio, hash_block_start,
hash_block_end - hash_block_start + 1);
}
}
check_at_most_once 옵션을 활성화하면 각 블록이 최대 1회만 검증됩니다.
검증된 블록은 비트맵(Bitmap)으로 추적되어 재검증을 건너뜁니다.
이 옵션은 신뢰할 수 있는 스토리지(예: 읽기 전용 미디어)에서
성능을 크게 개선하지만, 메모리를 추가로 소비합니다.
1TB 디바이스 기준 약 32MB의 비트맵 메모리가 필요합니다.
/* dm-verity target 등록 - 모듈 초기화 */
static struct target_type verity_target = {
.name = "verity",
.version = {1, 9, 0},
.module = THIS_MODULE,
.ctr = verity_ctr,
.dtr = verity_dtr,
.map = verity_map,
.status = verity_status,
.prepare_ioctl = verity_prepare_ioctl,
.iterate_devices = verity_iterate_devices,
.io_hints = verity_io_hints,
};
/* verity_ctr() 주요 파라미터 파싱 */
static int verity_ctr(struct dm_target *ti,
unsigned int argc, char **argv)
{
struct dm_verity *v;
int r, i;
unsigned int num;
v = kzalloc(sizeof(*v), GFP_KERNEL);
v->ti = ti;
/* argv[0]: version
* argv[1]: data_dev
* argv[2]: hash_dev
* argv[3]: data_block_size
* argv[4]: hash_block_size
* argv[5]: num_data_blocks
* argv[6]: hash_start_block
* argv[7]: algorithm
* argv[8]: root_hash
* argv[9]: salt (또는 "-") */
r = dm_get_device(ti, argv[1], FMODE_READ, &v->data_dev);
r = dm_get_device(ti, argv[2], FMODE_READ, &v->hash_dev);
/* Crypto API로 해시 알고리즘 할당 */
v->tfm = crypto_alloc_ahash(argv[7], 0, 0);
v->digest_size = crypto_ahash_digestsize(v->tfm);
/* Merkle Tree 레벨 계산 */
verity_hash_levels(v);
/* dm_bufio 클라이언트 생성 (해시 블록 캐시) */
v->bufio = dm_bufio_client_create(v->hash_dev->bdev,
1 << v->hash_dev_block_bits, 1, 0,
NULL, NULL, 0);
/* 검증 워크큐 생성 */
v->verify_wq = alloc_workqueue("kverityd",
WQ_CPU_INTENSIVE | WQ_MEM_RECLAIM, num_online_cpus());
/* 선택적 파라미터: FEC, check_at_most_once 등 */
r = verity_parse_opt_args(ti, v, argc - 10, argv + 10);
ti->private = v;
return 0;
}
| verity_ctr 선택 파라미터 | 설명 | 커널 버전 |
|---|---|---|
check_at_most_once | 블록당 최대 1회 검증 | 4.17+ |
ignore_corruption | 검증 실패 시 I/O 허용 (로깅만) | 4.4+ |
restart_on_corruption | 검증 실패 시 시스템 재부팅 | 4.4+ |
panic_on_corruption | 검증 실패 시 커널 패닉 | 4.17+ |
use_fec_from_device | FEC 디바이스 지정 | 4.4+ |
fec_roots | RS 패리티 바이트 수 | 4.4+ |
fec_blocks | FEC 보호 블록 수 | 4.4+ |
fec_start | FEC 데이터 시작 블록 | 4.4+ |
root_hash_sig_key_desc | 루트 해시 서명 검증(Signature Verification) keyring 키 | 5.4+ |
dm-verity의 성능 특성을 이해하는 것이 중요합니다. 각 4KB 데이터 블록을 읽을 때마다
SHA-256 해시 1회 + Merkle Tree 레벨 수만큼의 추가 해시 검증이 필요합니다.
단, dm_bufio 캐시 덕분에 상위 레벨 해시 블록은 대부분 캐시 히트되며,
실질적인 오버헤드는 리프 레벨 해시 1회 + 해시 블록 읽기 1-2회 정도입니다.
SHA-256의 소프트웨어 처리량(Throughput)은 약 500MB/s이며, SHA-NI 하드웨어 가속 시 2-3GB/s까지 향상됩니다.
Android Verified Boot
Android Verified Boot(AVB)는 dm-verity를 기반으로 시스템 파티션의 무결성을 보장합니다. AVB 2.0(libavb)은 부트로더에서 vbmeta 파티션을 검증하고, vbmeta에 포함된 dm-verity 루트 해시를 커널의 dm-verity target에 전달합니다. 이 체인을 통해 부트로더 → 커널 → 시스템 파티션까지 전체 부팅 경로의 무결성이 보장됩니다.
# Android 디바이스에서 dm-verity 상태 확인
$ adb shell dmctl list devices
system-verity : active
vendor-verity : active
# vbmeta 구조 확인
$ avbtool info_image --image vbmeta.img
Header Block: 256 bytes
Authentication Block: 576 bytes
Algorithm: SHA256_RSA4096
Rollback Index: 1
Descriptors:
Hashtree descriptor:
dm-verity Version: 1
Image Size: 3221225472 bytes
Tree Offset: 3221225472
Tree Size: 25427968
Data Block Size: 4096
Hash Algorithm: sha256
FEC num roots: 2
Root Digest: abc123...
| AVB 부팅 상태 | 설명 | 사용자 표시 |
|---|---|---|
| GREEN | 모든 검증 통과, 공식 키 | 정상 부팅 |
| YELLOW | 검증 통과, 사용자 커스텀 키 | 경고 표시 |
| ORANGE | 부트로더 언락, 검증 비활성 | 경고 + 5초 대기 |
| RED | 검증 실패 | 부팅 거부 또는 경고 |
# Android dm-verity 커맨드라인 (init에서 파싱)
# 부트로더가 커널 cmdline에 삽입하는 dm-verity 파라미터
androidboot.veritymode=enforcing
dm="1 vroot none ro 1,
0 6291456 verity 1
PARTUUID=a1b2c3d4 PARTUUID=e5f6g7h8
4096 4096 786432 786433 sha256
4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b
0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
10 restart_on_corruption ignore_zero_blocks use_fec_from_device
PARTUUID=i9j0k1l2 fec_roots 2 fec_blocks 786432 fec_start 786432"
# Android init에서 dm-verity 설정 과정
# 1. first_stage_init: 부트 이미지에서 fstab 로드
# 2. fs_mgr: fstab의 verify 옵션 파싱
# 3. fs_mgr_setup_verity(): dm-verity 테이블 생성
# 4. dm_ioctl(DM_TABLE_LOAD): 커널에 테이블 로드
# 5. dm_ioctl(DM_DEV_SUSPEND): 디바이스 활성화
# 6. mount(): 검증된 디바이스 마운트
# Android fstab.device 예시
# system /system ext4 ro,barrier=1 wait,verify,first_stage_mount
# vendor /vendor ext4 ro,barrier=1 wait,verify,first_stage_mount
# vbmeta에 rollback protection 인덱스 설정
$ avbtool make_vbmeta_image --output vbmeta.img --key /path/to/key.pem --algorithm SHA256_RSA4096 --rollback_index 3 --chain_partition system:1:/path/to/system_key.avbpubkey --chain_partition vendor:2:/path/to/vendor_key.avbpubkey
--rollback_index 0을 사용하세요.
| Android 파티션 | 보호 방식 | 검증 타이밍 |
|---|---|---|
| boot | AVB 서명 검증 (전체 이미지) | 부트로더 |
| init_boot | AVB 서명 검증 | 부트로더 |
| system | dm-verity (Merkle Tree + FEC) | first_stage_init |
| vendor | dm-verity | first_stage_init |
| product | dm-verity | first_stage_init |
| system_ext | dm-verity | first_stage_init |
| vbmeta | RSA-4096 서명 | 부트로더 |
| userdata | dm-crypt (FBE) 또는 ICE | vold |
| metadata | dm-default-key (메타데이터 암호화) | init |
Android 12+에서는 File-Based Encryption(FBE)이 기본이며,
dm-default-key target으로 메타데이터 암호화를 추가합니다.
기존의 Full-Disk Encryption(FDE, dm-crypt 단일)은 deprecated되었습니다.
FBE는 각 사용자의 CE(Credential Encrypted) 키와 DE(Device Encrypted) 키를
분리하여, 잠금 화면에서도 알람이나 전화 수신이 가능합니다.
Forward Error Correction (FEC)
dm-verity FEC는 Reed-Solomon 부호를 사용하여 손상된 블록을 자동 복구합니다.
drivers/md/dm-verity-fec.c에 구현되어 있으며,
Android에서는 시스템 파티션에 약 0.8%의 FEC 데이터를 추가하여
비트 에러로 인한 검증 실패 시 자동 복구를 시도합니다.
FEC는 dm-verity의 선택 기능으로, veritysetup format에
--fec-device와 --fec-roots 옵션을 지정하여 활성화합니다.
/* drivers/md/dm-verity-fec.h */
struct dm_verity_fec {
struct dm_dev *dev; /* FEC 데이터 디바이스 */
sector_t start; /* FEC 데이터 시작 섹터 */
sector_t blocks; /* FEC 블록 수 */
sector_t rounds; /* RS 인코딩 라운드 수 */
unsigned int rsn; /* RS 블록 크기 (255 기본) */
unsigned int roots; /* RS 패리티 바이트 수 (2 기본) */
struct dm_bufio_client *bufio;
mempool_t rs_pool; /* RS 컨텍스트 풀 */
};
# FEC가 포함된 dm-verity 설정
$ veritysetup format --fec-device=/dev/sda3 --fec-roots=2 \
/dev/sda1 /dev/sda2
$ veritysetup open --fec-device=/dev/sda3 --fec-roots=2 \
/dev/sda1 verified_root /dev/sda2 ROOT_HASH
--fec-roots=2로 설정하면 RS(255,253)이 적용되어
전체 데이터의 약 0.8%만 추가 공간이 필요합니다. roots 값을 높이면 복구 능력이 증가하지만
공간 오버헤드도 비례하여 증가합니다.
FEC 디코딩 과정을 상세히 살펴보면, dm-verity가 검증 실패를 감지하면
verity_fec_decode()를 호출합니다. 이 함수는 손상된 블록이 포함된
RS 블록을 찾아 decode_rs8()으로 디코딩을 시도합니다.
RS(255,253)에서는 최대 1바이트의 에러를 정정할 수 있으며,
erasure 위치를 알면 최대 2바이트까지 복구 가능합니다.
FEC 복구 성공 시 정정된 데이터가 반환되고, 실패 시 원래의 검증 실패 오류가 전달됩니다.
| FEC 파라미터 | RS 코드 | 패리티 | 공간 오버헤드 | 복구 능력 |
|---|---|---|---|---|
| roots=2 | RS(255,253) | 2바이트/블록 | ~0.8% | 1 에러/블록 |
| roots=4 | RS(255,251) | 4바이트/블록 | ~1.6% | 2 에러/블록 |
| roots=8 | RS(255,247) | 8바이트/블록 | ~3.1% | 4 에러/블록 |
| roots=24 | RS(255,231) | 24바이트/블록 | ~9.4% | 12 에러/블록 |
dm-user / dm-verity-loadpin
dm-user는 사용자 공간에서 Device Mapper target을 구현할 수 있게 하는 메커니즘입니다. ChromeOS와 Android에서 보안 부팅 체인을 강화하는 데 사용됩니다. dm-verity-loadpin은 커널 모듈(Kernel Module)과 펌웨어(Firmware) 로딩을 dm-verity로 검증된 파티션으로 제한합니다.
/* security/loadpin/loadpin.c */
/* LoadPin: 모든 커널 모듈/펌웨어가
* 동일한 검증된 파일시스템에서만 로드되도록 강제 */
static int loadpin_read_file(struct file *file,
enum kernel_read_file_id id,
bool contents)
{
struct super_block *load_root = READ_ONCE(pinned_root);
if (!load_root) {
/* 첫 번째 로드: 이 파일시스템을 "pin" */
pinned_root = file->f_path.mnt->mnt_sb;
return 0;
}
/* 이후 모든 로드는 pinned root와 동일한 fs에서만 허용 */
if (file->f_path.mnt->mnt_sb != load_root) {
pr_info("LoadPin: denied loading from different fs\n");
return -EPERM;
}
return 0;
}
/* dm-verity + LoadPin = 검증된 파티션에서만 커널 모듈 로드 */
# ChromeOS dm-verity + LoadPin 부팅 체인
# 1. 부트로더가 커널 서명 검증
# 2. 커널이 rootfs에 dm-verity 활성화
# 3. LoadPin이 첫 번째 모듈 로드 경로를 기록
# 4. 이후 모든 모듈/펌웨어는 동일 경로에서만 로드 허용
# Android: dm-verity + dm-user (snapuserd)
# snapuserd는 Virtual A/B OTA 업데이트에서
# copy-on-write 스냅샷을 사용자 공간에서 처리
$ ps aux | grep snapuserd
root 1234 0.5 0.2 snapuserd /dev/dm-user/system_cow
dm-user와 snapuserd를
사용하여 OTA 업데이트 중 copy-on-write 스냅샷을 관리합니다.
dm-verity 위에 dm-snapshot(또는 dm-user 기반 COW)을 쌓아
업데이트 중에도 시스템 무결성을 유지합니다.
배치 검증 최적화
dm-verity의 읽기 성능은 Merkle Tree 해시 블록의 I/O 오버헤드에 크게 좌우됩니다. 커널은 해시 블록 프리페치(prefetch), dm-bufio 캐싱, readahead 조합을 통해 대용량 순차 읽기 시나리오에서 검증 오버헤드를 최소화합니다.
| 최적화 기법 | 구현 위치 | 효과 |
|---|---|---|
| 해시 블록 프리페치 | verity_prefetch_io() | 데이터 읽기 전 해시 블록을 미리 로드 |
| dm-bufio 페이지 캐시(Page Cache) | dm_bufio_read() | 해시 블록을 메모리에 캐싱하여 디스크 I/O 절약 |
| 프리페치 클러스터 | DM_VERITY_DEFAULT_PREFETCH_SIZE | 한 번에 여러 해시 블록을 묶어 읽기 |
| readahead 연동 | 블록 레이어 readahead | 순차 읽기 시 데이터+해시 블록 사전 로드 |
| FEC 인터리빙 최적화 | verity_fec_decode() | FEC 블록도 클러스터 프리페치 |
/* dm-verity-target.c: 프리페치 워커 */
static void verity_prefetch_io(struct work_struct *work)
{
struct dm_verity_prefetch_work *pw =
container_of(work, struct dm_verity_prefetch_work, work);
struct dm_verity *v = pw->v;
int i;
/* Merkle Tree 각 레벨의 해시 블록을 프리페치 */
for (i = v->levels - 1; i >= 0; i--) {
sector_t hash_block_start, hash_block_end;
/* 데이터 블록 범위 → 해시 블록 범위 변환 */
verity_hash_at_level(v, pw->block, i,
&hash_block_start, NULL);
verity_hash_at_level(v, pw->block + pw->n_blocks - 1,
i, &hash_block_end, NULL);
/* dm-bufio prefetch: 비동기 읽기 요청 */
dm_bufio_prefetch(v->bufio, hash_block_start,
hash_block_end - hash_block_start + 1);
}
kfree(pw);
}
/* dm-verity map 경로에서 프리페치 스케줄링 */
static void verity_submit_prefetch(struct dm_verity *v,
struct dm_verity_io *io)
{
struct dm_verity_prefetch_work *pw;
pw = kmalloc(sizeof(*pw), GFP_NOIO);
if (!pw)
return;
INIT_WORK(&pw->work, verity_prefetch_io);
pw->v = v;
pw->block = io->block;
pw->n_blocks = io->n_blocks;
queue_work(v->verify_wq, &pw->work);
}
# dm-bufio 캐시 상태 확인
cat /sys/module/dm_bufio/parameters/current_allocated_bytes
# 67108864 (64MB)
# 캐시 크기 조정 (해시 트리 전체를 캐싱할 만큼 확보)
# 1GB 디바이스 + SHA-256 + 4KB 블록 = 약 8MB 해시 트리
echo 134217728 | sudo tee /sys/module/dm_bufio/parameters/max_cache_size_bytes
# 128MB로 확대
# readahead 크기 조정 (DM 디바이스)
blockdev --getra /dev/dm-0
# 256 (128KB, 기본값)
sudo blockdev --setra 2048 /dev/dm-0
# 1MB로 확대 → 순차 읽기 성능 향상
# 프리페치 효과 측정: fio 순차 읽기
fio --name=seqread --filename=/dev/dm-0 --rw=read \
--bs=128k --numjobs=4 --iodepth=32 --size=1G \
--direct=1 --group_reporting
# dm-bufio 캐시 히트율 확인
cat /sys/module/dm_bufio/parameters/current_allocated_bytes
# peak vs current로 캐시 워킹셋 파악
cat /sys/module/dm_bufio/parameters/peak_allocated_bytes
max_cache_size_bytes를 해시 트리 전체 크기 이상으로
설정하면 steady-state에서 해시 블록의 디스크 I/O가 0에 수렴합니다.
Android에서는 부팅 초기에 전체 해시 트리를 프리로드하는 최적화를 적용하여
verity_prefetch_io 워커가 부팅 직후 해시 트리를 워밍업합니다.
블록 레벨 해시 체인 검증
dm-verity의 블록 레벨 검증은 데이터 블록 읽기 요청이 들어올 때마다 Merkle Tree를 bottom-up으로 순회하며
해시를 검증합니다. 이 과정에서 각 레벨의 해시 블록을 dm_bufio 캐시에서 읽어와
계산된 해시와 저장된 해시를 비교합니다. 한 번 검증된 해시 블록은 캐시에 "verified" 상태로 표시되어
재검증 비용을 절약합니다.
/* drivers/md/dm-verity-target.c - 레벨별 해시 검증 */
static int verity_verify_level(struct dm_verity *v,
struct dm_verity_io *io,
sector_t block, int level,
bool skip_unverified,
u8 *want_digest)
{
struct dm_buffer *buf;
struct buffer_aux *aux;
u8 *data;
sector_t hash_block;
unsigned int offset;
/* 해당 블록의 해시 블록 위치와 오프셋 계산 */
verity_hash_at_level(v, block, level, &hash_block, &offset);
/* dm_bufio로 해시 블록 읽기 (캐시 우선) */
data = dm_bufio_read(v->bufio, hash_block, &buf);
if (IS_ERR(data))
return PTR_ERR(data);
aux = dm_bufio_get_aux_data(buf);
/* 이미 검증된 해시 블록인지 확인 */
if (!aux->hash_verified) {
if (skip_unverified) {
dm_bufio_release(buf);
return 1; /* 미검증 블록 건너뜀 */
}
/* 상위 레벨에서 이 해시 블록의 해시를 검증 */
r = verity_verify_level(v, io, hash_block,
level + 1, 0, NULL);
if (r)
goto release;
aux->hash_verified = 1; /* 검증 완료 표시 */
}
/* 기대 해시 값 추출 (offset 위치) */
memcpy(want_digest, data + offset, v->digest_size);
release:
dm_bufio_release(buf);
return r;
}
/* 해시 블록 위치 계산 유틸리티 */
static void verity_hash_at_level(struct dm_verity *v,
sector_t block, int level,
sector_t *hash_block,
unsigned int *offset)
{
sector_t position;
/* 블록 번호를 해당 레벨의 팬아웃으로 나누어 위치 계산
* SHA-256, 4KB 블록: hash_per_block_bits = 7 (128개) */
position = block >> (v->hash_per_block_bits * (level + 1));
*hash_block = v->hash_level_block[level] + position;
if (offset) {
position = (block >> (v->hash_per_block_bits * level))
& (((sector_t)1 << v->hash_per_block_bits) - 1);
*offset = position * v->digest_size;
}
}
verity_verify_level()은 재귀적으로 상위 레벨을 검증합니다.
Level 0에서 시작하면 Level 1, Level 2, ... Root까지 올라갑니다.
aux->hash_verified 플래그 덕분에 이미 검증된 해시 블록은
즉시 반환되어 중복 검증을 피합니다. 순차 읽기에서 인접 블록들은
동일한 해시 블록을 공유하므로 캐시 히트율이 매우 높습니다.
FEC 내부 복구 메커니즘
Forward Error Correction(FEC)의 내부 동작은 Reed-Solomon 부호의 인터리빙(Interleaving)에 기반합니다.
dm-verity FEC는 데이터와 해시 영역 전체를 RS 블록 단위로 인터리빙하여 인코딩합니다.
검증 실패 시 verity_fec_decode()가 호출되어 손상된 바이트의 위치를 특정하고
RS 디코딩으로 복구를 시도합니다.
/* drivers/md/dm-verity-fec.c - FEC 디코딩 핵심 */
static int fec_decode_rsb(struct dm_verity *v,
struct dm_verity_io *io,
struct dm_verity_fec_io *fio,
u64 rsb, unsigned int offset,
bool use_erasures)
{
int r, neras = 0;
unsigned int i, pos;
u8 *block = fio->rsb;
/* RS 코드워드 구성: 인터리빙된 바이트 수집 */
for (i = 0; i < v->fec->rsn; i++) {
u64 byte_pos = rsb + i * v->fec->rounds;
sector_t block_num = byte_pos / (1 << v->data_dev_block_bits);
/* 데이터/해시 영역에서 바이트 읽기 */
r = fec_read_bufs(v, io, rsb, byte_pos, block + i);
if (r)
return r;
}
/* FEC 패리티 바이트 읽기 */
for (i = 0; i < v->fec->roots; i++) {
r = fec_read_parity(v, rsb, i, &block[v->fec->rsn + i]);
if (r)
return r;
}
/* erasure 위치 힌트 (verity 실패 블록의 바이트) */
if (use_erasures) {
pos = offset / v->fec->rounds;
fio->erasures[neras++] = pos;
}
/* Reed-Solomon 디코딩 수행 */
r = decode_rs8(fio->rs, block, block + v->fec->rsn,
v->fec->rsn, NULL, neras,
fio->erasures, 0, NULL);
if (r < 0)
return r; /* 복구 불가 */
/* 정정된 바이트를 원래 위치에 기록 */
fec_write_corrected(v, fio, rsb, block);
return 0; /* r = 정정된 바이트 수 */
}
/* FEC 복구 진입점 */
int verity_fec_decode(struct dm_verity *v,
struct dm_verity_io *io,
enum verity_block_type type,
sector_t block, u8 *dest,
struct bvec_iter *iter)
{
struct dm_verity_fec_io *fio = fec_io(io);
int r;
if (!v->fec)
return -EOPNOTSUPP;
/* 손상 블록의 모든 RS 코드워드를 디코딩 시도 */
for (offset = 0; offset < 1 << v->data_dev_block_bits; offset++) {
u64 rsb = fec_rsb(v, block, offset);
r = fec_decode_rsb(v, io, fio, rsb, offset, 1);
if (r < 0)
return r;
}
/* 정정된 데이터로 해시 재검증 */
r = verity_hash(v, verity_io_hash_req(v, io),
dest, 1 << v->data_dev_block_bits,
verity_io_real_digest(v, io));
if (r)
return r;
return memcmp(verity_io_real_digest(v, io),
verity_io_want_digest(v, io),
v->digest_size) ? -EILSEQ : 0;
}
dm-verity + dm-integrity 결합 구성
dm-verity와 dm-integrity는 서로 다른 사용 사례를 위해 설계되었지만, 특정 시나리오에서는 두 가지를 결합하여 사용할 수 있습니다. 읽기 전용 시스템 파티션에는 dm-verity를, 읽기/쓰기 데이터 파티션에는 dm-integrity를 적용하여 전체 스토리지 스택의 무결성을 보장하는 구성이 대표적입니다.
# 결합 구성 예제: dm-verity (시스템) + dm-crypt+dm-integrity (데이터)
## 1단계: 시스템 파티션 - dm-verity 설정
# 빌드 시 해시 트리 생성
$ veritysetup format --hash sha256 --fec-device /dev/sda3 --fec-roots 2 \
/dev/sda1 /dev/sda2
# Root hash: 4a5b6c7d...
# 부팅 시 활성화
$ veritysetup open /dev/sda1 system_verified /dev/sda2 \
--fec-device=/dev/sda3 --fec-roots=2 \
4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d...
$ mount -o ro /dev/mapper/system_verified /system
## 2단계: 데이터 파티션 - dm-crypt + dm-integrity (AEAD) 설정
$ cryptsetup luksFormat --type luks2 \
--cipher aes-xts-plain64 \
--integrity hmac-sha256 \
--sector-size 4096 \
--key-size 512 \
/dev/sda4
# dm-integrity가 자동으로 하위 레이어에 생성됨
$ cryptsetup open /dev/sda4 data_encrypted
$ mount /dev/mapper/data_encrypted /data
## 스택 확인
$ dmsetup ls --tree
data_encrypted (254:2)
\`-data_encrypted_dif (254:1) # dm-integrity 레이어
system_verified (254:0) # dm-verity 레이어
## 각 레이어 상태 확인
$ dmsetup status system_verified
# 0 4194304 verity V 0 C 0
$ dmsetup status data_encrypted_dif
# 0 2097152 integrity 0 0 recalculating
| 구성 요소 | 보호 대상 | 검증 방식 | 읽기/쓰기 |
|---|---|---|---|
| dm-verity | 시스템 파티션 (OS, 앱) | Merkle Tree + FEC | 읽기 전용 |
| dm-crypt | 데이터 파티션 (사용자 데이터) | AES-XTS 암호화 | 읽기/쓰기 |
| dm-integrity | 데이터 파티션 무결성 | HMAC-SHA256 태그 | 읽기/쓰기 |
| dm-verity FEC | 시스템 파티션 복구 | Reed-Solomon | 읽기 전용 |
성능 벤치마크 및 튜닝
dm-verity의 성능 오버헤드는 해시 계산 비용과 해시 블록 I/O 비용으로 나뉩니다. 최신 CPU의 SHA-NI 확장(Extension)을 활용하면 해시 계산 비용은 크게 줄일 수 있으며, dm-bufio 캐시를 충분히 확보하면 해시 블록 I/O도 최소화할 수 있습니다.
# dm-verity 성능 벤치마크 방법
## 1. 기준 성능 측정 (dm-verity 없이)
$ fio --name=baseline --filename=/dev/sda1 --rw=read \
--bs=4k --numjobs=4 --iodepth=32 --size=1G \
--direct=1 --group_reporting
# 결과 예: IOPS=150k, BW=585MB/s (NVMe SSD)
## 2. dm-verity 성능 측정
$ fio --name=verity --filename=/dev/mapper/verified_root --rw=read \
--bs=4k --numjobs=4 --iodepth=32 --size=1G \
--direct=1 --group_reporting
# 결과 예: IOPS=130k, BW=507MB/s (약 13% 오버헤드)
## 3. 순차 읽기 비교 (대용량 블록)
$ fio --name=seq_base --filename=/dev/sda1 --rw=read \
--bs=128k --numjobs=4 --iodepth=16 --size=4G \
--direct=1 --group_reporting
# 결과 예: BW=3.2GB/s
$ fio --name=seq_verity --filename=/dev/mapper/verified_root --rw=read \
--bs=128k --numjobs=4 --iodepth=16 --size=4G \
--direct=1 --group_reporting
# 결과 예: BW=2.9GB/s (약 9% 오버헤드, 캐시 워밍업 후)
## 4. SHA-NI 하드웨어 가속 확인
$ grep sha_ni /proc/cpuinfo | head -1
# flags: ... sha_ni ...
# 또는 Crypto API에서 확인
$ cat /proc/crypto | grep -A5 sha256
# driver: sha256-ni (하드웨어 가속)
# driver: sha256-ssse3 (SSSE3 최적화)
# driver: sha256-generic (소프트웨어)
## 5. 해시 알고리즘별 성능 비교
# openssl speed 벤치마크
$ openssl speed sha256 sha512 blake2b512
# SHA-256 (SHA-NI): ~2.5 GB/s
# SHA-256 (소프트웨어): ~500 MB/s
# SHA-512: ~700 MB/s
# BLAKE2b: ~1.2 GB/s (소프트웨어)
| 시나리오 | 4KB 랜덤 읽기 | 128KB 순차 읽기 | 주요 병목 |
|---|---|---|---|
| dm-verity 없음 (기준) | 150K IOPS | 3.2 GB/s | 디바이스 한계 |
| dm-verity SHA-256 (SHA-NI) | 130K IOPS (-13%) | 2.9 GB/s (-9%) | 해시 계산 |
| dm-verity SHA-256 (소프트웨어) | 95K IOPS (-37%) | 2.1 GB/s (-34%) | CPU 해시 |
| dm-verity + check_at_most_once | 140K IOPS (-7%) | 3.0 GB/s (-6%) | 초기만 검증 |
| dm-verity SHA-256 + FEC | 128K IOPS (-15%) | 2.8 GB/s (-12%) | FEC 메타 읽기 |
# dm-verity 성능 튜닝 가이드
## 1. dm-bufio 캐시 크기 최적화
# 해시 트리 크기 계산: 4GB 데이터 / 128 팬아웃 = ~32MB 해시
# 해시 트리 전체를 캐싱하도록 설정
$ echo 67108864 | sudo tee /sys/module/dm_bufio/parameters/max_cache_size_bytes
# 64MB (해시 트리 32MB + 여유)
## 2. readahead 최적화
# dm-verity 디바이스의 readahead 확대
$ sudo blockdev --setra 4096 /dev/dm-0
# 2MB readahead (순차 읽기 성능 개선)
## 3. check_at_most_once 활성화
# dmsetup 테이블에 옵션 추가
$ echo "0 $SIZE verity 1 $DATA_DEV $HASH_DEV 4096 4096 \
$BLOCKS 1 sha256 $ROOT_HASH $SALT \
1 check_at_most_once" | dmsetup create verified
## 4. 해시 알고리즘 변경 (BLAKE2b로 성능 개선)
$ veritysetup format --hash blake2b-256 /dev/sda1 /dev/sda2
## 5. CPU 어피니티 조정 (kverityd 워크큐)
# kverityd 스레드를 성능 코어에 고정
$ taskset -p 0xFF $(pgrep -f kverityd)
## 6. 부팅 시 해시 트리 프리로드
# dd로 해시 디바이스 전체를 읽어 페이지 캐시에 로드
$ dd if=/dev/sda2 of=/dev/null bs=1M status=progress
## 7. 모니터링: dm-bufio 캐시 히트율 추적
$ watch -n 1 'echo "allocated: $(cat /sys/module/dm_bufio/parameters/current_allocated_bytes) \
peak: $(cat /sys/module/dm_bufio/parameters/peak_allocated_bytes)"'
check_at_most_once 옵션 활성화 (신뢰 환경),
(4) readahead를 2MB 이상으로 설정하세요.
NVMe SSD 환경에서 이 조합은 순차 읽기 오버헤드를 5% 미만으로 줄입니다.
디버깅 및 트러블슈팅
dm-verity 문제 진단에는 dmsetup status, 커널 로그, ftrace 등의 도구를 활용합니다.
검증 실패 발생 시 원인 파악과 복구 방법을 단계별로 살펴봅니다.
# 1. dm-verity 상태 확인
$ dmsetup status verified_root
# 출력 형식: start_sector num_sectors verity V C
# 0 4194304 verity V 0 C 0
# V = 검증된 블록 수, C = corruption 감지 수
# FEC 포함 상태
$ dmsetup status verified_root
# 0 4194304 verity V 1048576 C 3 F 2 U 1
# V=검증 성공, C=corruption, F=FEC 복구 성공, U=복구 실패
# 2. dm-verity 테이블 확인
$ dmsetup table verified_root
# 0 4194304 verity 1 /dev/sda1 /dev/sda2 4096 4096 ...
# 3. 커널 로그에서 verity 메시지 확인
$ dmesg | grep -i verity
# device-mapper: verity: sha256 using implementation "sha256-ni"
# device-mapper: verity: 254:0: data block 12345 is corrupted
# 4. 검증 실패 블록 위치 추적
# 커널 로그에서 손상된 블록 번호 확인
$ dmesg | grep "is corrupted"
# device-mapper: verity: 254:0: data block 12345 is corrupted
# 블록 12345 = 바이트 오프셋 50,585,600 (12345 * 4096)
# 5. 해시 수동 검증
# 특정 블록의 해시를 직접 계산하여 비교
$ dd if=/dev/sda1 bs=4096 skip=12345 count=1 2>/dev/null | sha256sum
# 실제 해시: abc123...
$ veritysetup verify /dev/sda1 /dev/sda2 ROOT_HASH
# Verification failed at position 50585600 (block 12345)
# 6. ftrace로 verity I/O 경로 추적
$ echo function_graph | sudo tee /sys/kernel/tracing/current_tracer
$ echo verity_map | sudo tee /sys/kernel/tracing/set_graph_function
$ echo 1 | sudo tee /sys/kernel/tracing/tracing_on
# ... I/O 발생 ...
$ cat /sys/kernel/tracing/trace
# 7. perf로 성능 프로파일링
$ perf record -g -p $(pgrep -f kverityd) -- sleep 10
$ perf report --no-children
# sha256_ni_transform: 45% (해시 계산)
# dm_bufio_read: 20% (해시 블록 읽기)
# verity_verify_io: 15% (검증 로직)
| 증상 | 원인 | 해결 방법 |
|---|---|---|
| 부팅 시 dm-verity 실패 | 루트 해시 불일치 | 올바른 루트 해시로 재설정, vbmeta 재서명 |
| 특정 파일 읽기 시 -EIO | 해당 블록 데이터 손상 | FEC 활성화 확인, 파티션 재플래싱 |
| 대량 검증 실패 (C 값 급증) | 디스크 물리 결함 | 디스크 교체, SMART 상태 확인 |
| 성능 저하 (읽기 속도 느림) | dm-bufio 캐시 부족 | max_cache_size_bytes 증가 |
kverityd CPU 100% | SHA-NI 미지원, 소프트웨어 해싱 | CPU 지원 확인, BLAKE2b 전환 고려 |
| FEC 복구 실패 (U 값 증가) | 에러 범위 초과 | roots 값 증가, 파티션 재생성 |
# dm-verity 트러블슈팅 스크립트
#!/bin/bash
# dm-verity 상태 종합 진단
DM_NAME="${1:-verified_root}"
echo "=== dm-verity 디바이스 정보 ==="
dmsetup info "$DM_NAME"
echo -e "\n=== 테이블 ==="
dmsetup table "$DM_NAME"
echo -e "\n=== 상태 ==="
dmsetup status "$DM_NAME"
echo -e "\n=== dm-bufio 캐시 ==="
echo "현재: $(cat /sys/module/dm_bufio/parameters/current_allocated_bytes) bytes"
echo "최대: $(cat /sys/module/dm_bufio/parameters/max_cache_size_bytes) bytes"
echo "피크: $(cat /sys/module/dm_bufio/parameters/peak_allocated_bytes) bytes"
echo -e "\n=== Crypto API 해시 드라이버 ==="
cat /proc/crypto | grep -B1 -A5 "name.*sha256"
echo -e "\n=== 커널 로그 (verity 관련) ==="
dmesg | grep -i verity | tail -20
echo -e "\n=== kverityd 워크큐 ==="
ps aux | grep kverityd
EIO 모드에서는 해당 블록 읽기만 실패하고 시스템은 계속 동작하지만,
restart_on_corruption 모드에서는 시스템이 즉시 재부팅됩니다.
프로덕션(Production) 환경에서는 restart_on_corruption과 FEC를 함께 사용하여
복구 가능한 에러는 자동 정정하고, 복구 불가능한 에러에서는 재부팅하는 것이 권장됩니다.
관련 문서
- dm-integrity — 읽기/쓰기 무결성 보호, 저널링, AEAD 결합
- dm-crypt — 블록 레벨 투명 암호화, LUKS, 성능 최적화
- Device Mapper / LVM — DM 프레임워크 기본, dm-linear, dm-thin, LVM2
- 블록 I/O — 블록 레이어 아키텍처, bio 구조, I/O 경로
- 파일시스템 개요 — VFS, 마운트(Mount), superblock
- Linux Crypto Framework — 커널 암호 API, skcipher, AEAD, ahash
- Secure Boot — UEFI Secure Boot, MOK, 커널 서명
- 커널 문서:
Documentation/admin-guide/device-mapper/verity.rst - Android Verified Boot: AVB 소스
- dm-verity 커널 소스:
drivers/md/dm-verity-target.c,dm-verity-fec.c - 커널 문서 — dm-verity: kernel.org dm-verity 문서
- dm-verity 커널 소스 (Bootlin): dm-verity-target.c
- LWN.net — dm-verity 소개: Verified boot with dm-verity
- cryptsetup/veritysetup: gitlab.com/cryptsetup