Copy Blob shares the source's page ranges and append-block list with the copy: writing to the copy changes the source, and writing to a restored blob changes its snapshot

オープン 初心者向け
#2,890 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
2/5
見積もり時間
1〜3時間
初心者へのやさしさ
78/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
typescript
領域
backend, databases

調査の方向性

Issueで挙げられている startCopyFromURL と copyFromURL の経路について、まず src/blob/persistence/LokiBlobMetadataStore.ts を確認してください。次に createSnapshot を調べ、ページ範囲と確定済みブロック配列をどのようにコピーするかを確認してください。スクリプトにある page blob、snapshot の復元、append blob のケースを再現してください。コピーした blob への書き込みで元の blob や snapshot が変化しなくなり、各 blob が期待どおりのデータと ETag の動作を返せば完了です。

索引モデルが issue の本文から書いたものです。

説明

Which service(blob, file, queue, table) does this issue concern?

Blob

Which version of the Azurite was used?

3.37.0 (also seen on 3.29.0)

Where do you get Azurite? (npm, DockerHub, NuGet, Visual Studio Code Extension)

Docker image mcr.microsoft.com/azure-storage/azurite:3.37.0 (same digest as latest)

What's the Node.js version?

v22.23.2 (the one in the image)

What problem was encountered?

After Copy Blob copies a page blob, the copy and the source share one list of page ranges. A Put
Page on either blob then changes the content of both. The other blob's ETag does not change, so a
client cannot see that its data changed. Append blobs share their list of
committed blocks in the same way.

Three cases (script below):

  • Copy, then write the copy. p1 is a 1 KiB page blob that is never written. Copy Blob
    p1 -> p2, then Put Page 0-511 with b on p2. Get Blob on p1 now returns 512 b bytes
    followed by 512 zero bytes, with the same ETag as before.
  • Restore a snapshot, then write the blob. p3 holds a in 0-511. Snapshot Blob p3,
    restore the snapshot over p3 with Copy Blob (x-ms-copy-source: .../p3?snapshot=<id>), then
    Put Page 0-511 with c on p3. The snapshot now reads c. The backup that the restore came
    from has been overwritten by the next write to the restored blob.
  • Append blobs. a1 holds x. Copy Blob a1 -> a2, Append Block y to a2, then Append
    Block z to a1. Get Blob on a1 returns xy, not xz: the block appended to the copy is
    now part of the source, and the source's own z is not visible. Before the last append,
    x-ms-blob-committed-block-count of a1 was already 2.

The documentation describes the copy as a new blob that receives the source's data, and a
snapshot as read-only. Copy Blob, remarks:

When you're copying from a page blob, Blob Storage creates a destination page blob of the source
blob's length. Initially, the page blob contains all zeroes. Then the source page ranges are
enumerated, and non-empty ranges are copied.

You can perform a copy operation to promote a snapshot over its base blob, as long as it's in an
online tier (hot, cool, or cold). In this way, you can restore an earlier version of a blob. The
snapshot remains, but its destination is overwritten with a copy that can be both read and
written.

Snapshot Blob, remarks:

Snapshots provide read-only versions of blobs. After you create a snapshot, you can read, copy,
or delete it, but you can't modify it.

Put Page: "The Put Page
operation writes a range of pages to a page blob." That is, to the blob in the request URL.

Append Block: "The
Append Block operation commits a new block of data to the end of an existing append blob."

Expected: in the first case p1 stays all zeroes; in the second the snapshot keeps a; in the
third a1 reads xz.

Steps to reproduce the issue?
docker run -d -p 10000:10000 mcr.microsoft.com/azure-storage/azurite:3.37.0 azurite-blob --blobHost 0.0.0.0

Then:

B=http://127.0.0.1:10000/devstoreaccount1
# Account SAS derived from Azurite's public default development key, not a cloud credential.
SAS='sv=2023-11-03&ss=b&srt=sco&sp=rwdl&se=2099-01-01&sig=gF53rwiyjUw0Grh2NB8D%2Bw%2FK1kP1kspe6Cj3FCMjg%2F0%3D'
V='x-ms-version: 2023-11-03'
req()   { label=$1; shift; printf '%-42s %s\n' "$label" "$(curl -s -o /dev/null -w '%{http_code}' -X PUT -H "$V" -H 'Content-Length: 0' "$@")"; }
page()  { printf '%-42s %s\n' "$1" "$(head -c 512 /dev/zero | tr '\0' "$2" | curl -s -o /dev/null -w '%{http_code}' -X PUT -H "$V" \
            -H 'x-ms-page-write: update' -H 'x-ms-range: bytes=0-511' --data-binary @- "$3")"; }
first() { printf '%-42s%s\n' "$1" "$(curl -s -H "$V" "$2" | od -An -c -N4)"; }

req 'create container' "$B/pages?restype=container&$SAS"
# A. p1: a 1 KiB page blob that is never written. p2: a copy of p1. Write 'b' to p2.
req 'create page blob p1 (1024 bytes)' -H 'x-ms-blob-type: PageBlob' -H 'x-ms-blob-content-length: 1024' "$B/pages/p1?$SAS"
req 'Copy Blob p1 -> p2' -H "x-ms-copy-source: $B/pages/p1" "$B/pages/p2?$SAS"
page 'Put Page p2 bytes 0-511 = b' b "$B/pages/p2?comp=page&$SAS"
first 'first 4 bytes of p1:' "$B/pages/p1?$SAS"
# B. p3 holds 'a'. Snapshot it, restore the snapshot over p3 with Copy Blob, write 'c' to p3.
req 'create page blob p3 (1024 bytes)' -H 'x-ms-blob-type: PageBlob' -H 'x-ms-blob-content-length: 1024' "$B/pages/p3?$SAS"
page 'Put Page p3 bytes 0-511 = a' a "$B/pages/p3?comp=page&$SAS"
SNAP=$(curl -s -o /dev/null -D - -X PUT -H "$V" -H 'Content-Length: 0' "$B/pages/p3?comp=snapshot&$SAS" \
       | tr -d '\r' | awk -F': ' 'tolower($1)=="x-ms-snapshot" {print $2}')
echo "Snapshot Blob p3: x-ms-snapshot $SNAP"
first 'first 4 bytes of the snapshot:' "$B/pages/p3?snapshot=$SNAP&$SAS"
req 'Copy Blob p3?snapshot -> p3 (restore)' -H "x-ms-copy-source: $B/pages/p3?snapshot=$SNAP" "$B/pages/p3?$SAS"
page 'Put Page p3 bytes 0-511 = c' c "$B/pages/p3?comp=page&$SAS"
first 'first 4 bytes of the snapshot:' "$B/pages/p3?snapshot=$SNAP&$SAS"

# C. Append blobs: a1 holds 'x'. Copy a1 -> a2, append 'y' to a2, then append 'z' to a1.
append() { printf '%-42s %s\n' "Append Block $2 = $1" "$(curl -s -o /dev/null -w '%{http_code}' -X PUT -H "$V" --data-binary "$1" "$B/pages/$2?comp=appendblock&$SAS")"; }
req 'create append blob a1' -H 'x-ms-blob-type: AppendBlob' "$B/pages/a1?$SAS"
append x a1
req 'Copy Blob a1 -> a2' -H "x-ms-copy-source: $B/pages/a1" "$B/pages/a2?$SAS"
append y a2
append z a1
echo "Get Blob a1: $(curl -s -H "$V" "$B/pages/a1?$SAS")"

Output on 3.37.0:

create container                           201
create page blob p1 (1024 bytes)           201
Copy Blob p1 -> p2                         202
Put Page p2 bytes 0-511 = b                201
first 4 bytes of p1:                         b   b   b   b
create page blob p3 (1024 bytes)           201
Put Page p3 bytes 0-511 = a                201
Snapshot Blob p3: x-ms-snapshot 2026-11-01T18:24:13.0870000Z
first 4 bytes of the snapshot:               a   a   a   a
Copy Blob p3?snapshot -> p3 (restore)      202
Put Page p3 bytes 0-511 = c                201
first 4 bytes of the snapshot:               c   c   c   c
create append blob a1                      201
Append Block a1 = x                        201
Copy Blob a1 -> a2                         202
Append Block a2 = y                        201
Append Block a1 = z                        201
Get Blob a1: xy

Expected: \0 \0 \0 \0 for p1, a a a a for the snapshot at the end, and xz for a1. In our runs the ETag
of p1 was the same before and after the write to p2 (for example "0x1DF07064A5C5EC0" both
times).

Have you found a mitigation/solution?

Mitigation: after copying a page or append blob, do not write to the copy or the source.

Potential cause. LokiBlobMetadataStore.startCopyFromURL builds the new blob under the comment
// Deep clone a copied blob but sets pageRangesInOrder: sourceBlob.pageRangesInOrder
(src/blob/persistence/LokiBlobMetadataStore.ts:2017, inserted at :2060); copyFromURL does the
same (:2208, :2253). The blobs collection is created without clone
(:181-183), so the source document and the copy hold the same array object.
uploadPages and clearRange then change that array in place through
PageBlobRangesManager.mergeRange / clearRange, which use splice
(src/blob/handlers/PageBlobRangesManager.ts:74, :113, :168; called from
LokiBlobMetadataStore.ts:2804 and :2869). The next lines of both copy paths do the same with
committedBlocksInOrder: sourceBlob.committedBlocksInOrder (:2026, :2217), and
appendBlock pushes onto that array (doc.committedBlocksInOrder.push(block), :2494-2495).
createSnapshot already copies both arrays (.slice(), :1145-1153), which is why a snapshot on
its own is safe until a copy is made from it.

Suggested fix. Copy both arrays in both copy paths, as createSnapshot does, for example
pageRangesInOrder: sourceBlob.pageRangesInOrder === undefined ? undefined : sourceBlob.pageRangesInOrder.slice()
and the same for committedBlocksInOrder. mergeRange and clearRange splice new range objects
into the array rather than changing the existing ones, so a shallow copy looks enough.

主要言語
TypeScript
スター
2.3k
フォーク
400
平均マージ
4日 9時間
マージ済み PR(30日)
23

環境構築

  • Dockerfile または Docker Compose ファイルあり
  • プルリクエストのテンプレートあり
  • コントリビューションガイドなし

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

Azure/Azurite のほかの issue

Azure/Azurite の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。