[gsession] StorageFile.SetSession 非原子写入导致并发读请求看到空 session
メンテナーはふだん 2 日以内に返信
まだ誰も着手していません。
- #4814 @waterWang による — マージされずにクローズ
評価
調査の方向性
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;
GetSessionreturns 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 を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
gogf/gf のほかの issue
-
feature inactive
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
メンテナーはふだん 2 日以内に返信
-
cmd/gf: gen ctrl merge mode takes an unaliased import ending in the api version as the api package対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープン
難易度 3/5 1〜2日 初心者へのやさしさ 72/100
メンテナーはふだん 2 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 45/100
メンテナーはふだん 2 日以内に返信
-
database/gdb: Model.Partition() is a no-op, partition names never reach the generated SQL再び着手できるかも このイシューのプルリクエストはマージされずにクローズされました。 オープンbug discuss help wanted
難易度 4/5 3〜5日 初心者へのやさしさ 25/100
メンテナーはふだん 2 日以内に返信
-
gdb: `Result.Structs` / `Model.Scan` into `*[]map[string]interface{}` silently returns empty maps instead of data or error対応中かも @LuisFigueroaG が 2 日前に担当しました。 オープン
難易度 3/5 1〜2日 初心者へのやさしさ 45/100
メンテナーはふだん 2 日以内に返信
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
-
Response matcher: a changed integer above 2^53 passes because JSON numbers are compared as float64オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
メンテナーはふだん 1 日以内に返信
-
dec2hex rejects integer literals and accepts string literals in SQL対応中かも @NopeCharon が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
Open-Meteo integration: selectable weather-models for temperature- and PV-forecasts対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープンenhancement tariffs
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
evcc-io/evcc#35698 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信