[gsession] StorageFile.SetSession 非原子写入导致并发读请求看到空 session

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

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

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

  • #4814 @waterWang による — マージされずにクローズ

評価

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

調査の方向性

gsession_storage_file.go の2つのパスを読む: 189-203行あたりの SetSession(O_TRUNC で開いた後、2回の別々の Write 呼び出し)と、141-170の GetSession。後者は8バイト以下の内容を空のセッションとして扱う。まず、SetSession が同じセッションIDを書き換えている間に GetSession を同時に呼び出すテストで race を再現し、8バイトの gtime.TimestampMilli() プレフィックス形式を維持すること。開始前に、クローズ済みで未マージの PR #4814 に以前の試みがないか確認する。完了とは、SetSession が一時ファイルに書き込んでアトミックにリネームし、既存の gsession storage テストが引き続き通ることを意味する。

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

説明

Description

StorageFile.SetSession writes session data using a non-atomic file write pattern (open with O_TRUNC then two separate Write calls), creating a race window where concurrent readers see an empty or partially-written file.

Impact

Under concurrent requests sharing the same session ID (common in SPA applications where page switching fires multiple API calls simultaneously), intermittent authentication failures occur:

  • Session data is lost; GetSession returns nil
  • User appears "not logged in" and gets redirected to login

Root Cause

Write path (gsession_storage_file.go:189-203)
file, err := gfile.OpenWithFlagPerm(
    path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm,
)
// ...
file.Write(gbinary.EncodeInt64(gtime.TimestampMilli()))  // 8 bytes timestamp
file.Write(content)                                         // JSON body

O_TRUNC empties the file immediately. Then two separate file.Write() calls create a real gap between writing the timestamp header and the JSON body.

Read path (gsession_storage_file.go:141-170)
content = gfile.GetBytes(path)
if len(content) > 8 {
    // parse JSON
}
return nil, nil  // content <= 8 bytes → empty session
Race window
Writer(SetSession)                  Reader(GetSession)
│                                    │
├─O_TRUNC open → file is empty      │
│                                    ├─gfile.GetBytes() → 0-8 bytes
│                                    │ len<=8 → returns nil session
│                                    │ → user appears not logged in!
├─file.Write(timestamp)[8 bytes]    │
├─file.Write(content)[JSON body]    │
│ write complete                     │

Production Evidence

Real log (3ms window, same session ID):

12:16:47.467  devices/all          userId=1  ← normal
12:16:47.470  software-systems/all userId=0  ← user_id key missing! (race hit)
12:16:47.472  network-lines/all    userId=1  ← normal

Proposed Fix: atomic write via temp file + rename

func (s *StorageFile) SetSession(ctx context.Context, sessionId string, sessionData *gmap.StrAnyMap, ttl time.Duration) error {
    intlog.Printf(ctx, "StorageFile.SetSession: %s, %v, %v", sessionId, sessionData, ttl)
    path := s.sessionFilePath(sessionId)
    content, err := json.Marshal(sessionData)
    if err != nil {
        return err
    }
    if s.cryptoEnabled {
        content, err = gaes.Encrypt(content, DefaultStorageFileCryptoKey)
        if err != nil {
            return err
        }
    }

    // Build content with 8-byte timestamp prefix (same format)
    timestamp := gbinary.EncodeInt64(gtime.TimestampMilli())
    data := make([]byte, 8+len(content))
    copy(data[:8], timestamp)
    copy(data[8:], content)

    // Step 1: write to temp file (invisible to readers)
    tmpPath := path + ".tmp"
    if err := os.WriteFile(tmpPath, data, os.ModePerm); err != nil {
        return gerror.Wrapf(err, `write data failed to file "%s"`, tmpPath)
    }

    // Step 2: atomic rename (appears atomically to readers)
    if err := os.Rename(tmpPath, path); err != nil {
        os.Remove(tmpPath)
        return gerror.Wrapf(err, `rename "%s" -> "%s" failed`, tmpPath, path)
    }
    return nil
}

os.Rename is atomic on the same filesystem (POSIX guarantee), so readers always see a complete file.

主要言語
Go
スター
13.3k
フォーク
1.7k
平均マージ
4日 11時間
マージ済み PR(30日)
42

環境構築

このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

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

gogf/gf のほかの issue

gogf/gf の issue をすべて見る

似ている issue

Go の issue をもっと見る

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

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