# VC-087 · İki istek aynı kalan miktarı okuyup tek hakkı iki kez kullanıyor

İstekler önce kalan stok veya krediyi okuyor, sonra yeni değeri yazıyor. İkisi de aynı eski değeri görünce ikisi de kabul ediliyor ve veritabanındaki son miktar bu çift harcamayı gizleyebiliyor.

- Önem: YÜKSEK. Etkisi büyük. Trafik artınca ya da istek tekrarlanınca tetiklenir.
- Önem notu: Aynı sınırlı hakkı eşzamanlı kullanabilen istekler ve kabulün gerçek teslim veya kredi harcamasına bağlandığı akış temel alınır. Yalnız görüntülenen sayaçta etki düşer. Yarışın kullanıcı tarafından kasıtlı tetiklenebilmesi ayrıca incelenir.
- Eksen ve kategori: Sağlamlık, 13 Eşzamanlılık ve tutarlılık
- Yığın: Node.js, Her yığın
- Yapay zekâ kodunda: ölçülmedi. Dayanak: uzman görüşü.
- Ne zaman bakılır: İlk yayından önce, Ödeme almadan önce
- CWE: CWE-367
- Checklist ifadesi: Stok, kupon ya da kredi gibi sınırlı haklar eşzamanlı isteklerde iki kez kullanılamıyor.
- Son inceleme: 4 Ekim 2026, Komünite editörlüğü
- Adres: https://vibecheck.komunite.com.tr/madde/stok-kupon-kredi-eszamanli-iki-kez-harcaniyor

## 60 saniyelik kontrol

Yalnız kendi uygulamanda ya da yazılı izin aldığın sistemde dene. Bu bir sızma testi değildir.

1. Kalan miktarı okuyup ayrı sorguyla güncelleyen yolları bul.
2. Yerel testte son hak için iki isteği aynı yazma sınırında beklet.
3. Yalnız bir isteğin kabul edildiğini ve kalan miktarın doğru olduğunu doğrula.
4. Sıfır, negatif ve kesirli miktarın reddedildiğini kontrol et.
5. Başka kaynağın ve yeterli miktarlı olağan isteğin çalıştığını doğrula.

## Ne oluyor

Stokta son adet var. İlk istek kalan miktarı okuyor ve yeterli buluyor. Güncelleme yapmadan önce başka bir istek de aynı miktarı okuyor. İkisi de kendisi için yeterli stok gördüğü için kabul ediliyor. Ardından ikisi de eski miktardan hesapladığı yeni değeri yazıyor. Son stok sıfır görünebilir ama kabul edilen iş sayısı eldeki miktarı aşmıştır.

Bu nedenle yalnız negatif stok aramak yarışı göstermeyebilir. Eski değerin üstüne yazan iki işlem aynı sonucu bırakır. Veritabanındaki son sayı makul görünürken kullanıcıya verilen sözler birbiriyle çelişir. Hem kabul edilen işlemleri hem kalan miktarı birlikte değerlendirmek gerekir. Kayıp güncelleme bu farkı gizleyebilir.

Tek istekle denediğinde bütün adımlar doğru görünür. Sorun, okuma ile kullanma arasında başka işin aynı kaynağı değiştirebilmesidir. JavaScript kodunun tek iş parçacığında çalışması bunu kendiliğinden engellemez. Bekleyen işlemler iç içe geçebilir ve uygulamanın başka örnekleri de aynı veritabanına ulaşabilir. Yeterlilik kararı ile miktar azaltma arasındaki sınır veri katmanında korunmalıdır.

## Gerçek olay

Bu örneğe bağlanan kamuya açık stok kaybı olayı ileri sürülmüyor. [CWE-367](https://cwe.mitre.org/data/definitions/367.html), kontrol edilen durumun kullanılmadan önce değişebilmesini ele alır. Maddenin somut hali kalan miktarın eski okumaya dayanarak tüketilmesidir. [SQLite UPDATE belgesi](https://www.sqlite.org/lang_update.html), koşullu güncellemenin veri katmanında nasıl ifade edildiğini gösterir.

Yerel deney gerçek SQLite üzerinde iki asenkron çağrıyı aynı yazma sınırında buluşturur. Kötü sürüm bu noktaya gelmeden miktarı okumuştur. İyi sürüm yeterlilik koşulunu azaltmayla aynı SQL ifadesinde uygular. Deney rastlantısal zamanlamaya güvenmez. Çok makineli yük testi veya gerçek PostgreSQL kilit ölçümü değildir, belirli iç içe geçişi tekrar üretir.

## Yapay zekâ bunu neden üretiyor

**İş kuralı doğal cümle sırasına çevrilir.** Kullanıcı önce stok kontrol et, sonra azalt dediğinde ajan ayrı okuma ve yazı üretebilir. Bu sıra tek işlemde anlaşılır görünür. Ancak iki çağrı arasındaki süre başka isteklerin de çalışabileceği bir penceredir. İş kuralının atomik sınırı doğal dildeki sıralamadan ayrıca çıkarılmalıdır.

**Yerel deneme seri ilerler.** Ajan ilk isteğin bitmesini bekleyip ikinci isteği gönderirse ikinci çağrı güncel miktarı görür ve reddedilir. Bu test doğru sonucu üretir ama yarış yolunu çalıştırmaz. İki isteğin de eski değeri gördüğü sıra ayrı kurulmalıdır. Başarı yalnız sırayla yapılan denemeden çıkarılamaz.

**Son sayı doğru sanılır.** Ajan stok değerinin negatif olmadığını kontrol edebilir. Eski değeri yazan iki işlemde son sayı yine sıfırdır. Kullanıcıya iki kabul dönmesi gözden kaçarsa test geçer. İşin sonucu yalnız satırın son değerinden oluşmaz. Kabul edilen hakların toplamı da ürün kuralının parçasıdır.

**Uygulama kilidi ortak sanılır.** Ajan süreç içinde bir bayrak veya kuyruk tutabilir. Bu düzen tek süreçte çağrıları sıraya sokar. Başka sunucu örneği aynı bayrağı paylaşmadığında veri üzerindeki yarış sürer. Koruma ortak kaynağın gerçek yazma sınırında kurulmalıdır. Bunlar olası üretim açıklamalarıdır. AI kodlarında ölçülmüş bir hata oranı olarak sunulmuyor.

## Etki

Eldeki miktardan fazla sipariş kabul edilebilir veya aynı kullanım hakkı birden fazla işe harcanabilir. Sonradan iptal, iade veya manuel düzeltme gerekebilir. Kredi sayacı ile kabul edilen işler ayrışınca hangi isteğin geçerli olduğu da belirsizleşir. Etki, kabul yanıtının dışarıda hangi eylemi başlattığına bağlıdır.

Saldırgan yarış penceresini kasıtlı deneyebilir ama sıradan yoğun trafik de yeterlidir. Bir iş anahtarı bu sorunu her zaman çözmez. Farklı kullanıcıların farklı işleri aynı son kaynağa talip olabilir. Her isteğin kendi içinde tekil olması, ortak stokun hepsine yeteceği anlamına gelmez. Tekrar koruması ve ortak miktar koruması ayrı kurallardır.

## Nasıl anlarsın

Miktarı okuyan sorguyu ve azaltan sorguyu bul. Aralarında bekleme, başka sorgu veya dış servis çağrısı var mı? Güncelleme eski değeri koşulsuz mu yazıyor? İstek, gerçekten satır değişip değişmediğine bakmadan başarı mı döndürüyor? Veritabanı kısıtı bulunsa bile bu koşulların nasıl birleştiğini incele.

Yerel testte son hak için iki isteği aynı yazma noktasında beklet. İkisi hazır olunca devam ettir. Yalnız biri kabul edilmeli ve kalan miktar doğru olmalıdır. Ardından stok bitmiş yol, olmayan ürün ve yeterli stoklu başka ürünle dene. Sıfır, negatif ve kesirli miktar da reddedilmelidir. Negatif azaltma miktarı kalan hakkı artıran başka bir arıza oluşturabilir.

## Nasıl düzeltirsin

1. **Denetimi yazıyla birleştir.** Basit sayaçta `available >= quantity` koşulunu güncellemenin içine taşı. Azaltmayı uygulamanın eski okumasıyla değil veritabanındaki mevcut değerle yap. Örnek tek SQL ifadesi kullanır ve miktar sınırını ayrıca doğrular.
2. **Başarıyı gerçek etkiden çıkar.** Etkilenen satır yoksa kaynak bulunmamış veya miktar yetersiz olabilir. İyi örnek bu durumda ret döndürür. Ürünün hata sözleşmesi gerekiyorsa bu nedenleri bilgi sızdırmadan ayır. Koşulun sağlanmamasını başarılı işlem diye gösterme.
3. **Bağlı kayıtları birlikte yaz.** Stok azaltma yanında sipariş veya harcama kaydı varsa aynı transaction içinde tutulmalıdır. Aksi halde miktar doğru korunurken kayıt yazımı bozulabilir. Daha karmaşık kaynak dağıtımında kilit veya sürüm denetimi gerekebilir.
4. **Korumanın kapsamını doğrula.** Süreç içi bayrağın bütün çalışanları kapsadığını varsayma. Kullandığın veritabanının izolasyon ve kilit davranışını incele. Testin gösterdiği iç içe geçişi üretim ortamının bütün yarışlarına genelleme. Başka tablolara yayılan kuralları ayrıca sınırla.

## Bir daha olmasın

Sınırlı her hak için kabul sayısı ile kalan miktarı birlikte sınayan yarış testi tut. İş akışına yeni yan etki eklendiğinde atomik sınırı yeniden incele.

## Sınır

Örnek tek satırdaki miktar azaltmayı kapsar. Dağıtık kilit, çok ürünlü sepet, ödeme ve sipariş kaydı içermez. Kimlik ve ürün erişimi üst sınırda doğrulanmış kabul edilir. Doğru transaction veya kilitle korunan ayrı okuma tek başına bulgu değildir. Tekrar gönderimin aynı iş sayılması da ayrı bir sözleşmedir.

## Düzeltme kodları

### Node.js: Koşullu atomik miktar azaltma

Önce:

```js
// stok/ayir.js, açıklama amaçlı. Ürün ve çağıran yetkisi üst sınırda doğrulanır.
export function setup(db) {
  db.exec(`CREATE TABLE stock(id TEXT PRIMARY KEY, available INTEGER NOT NULL CHECK(available >= 0));
    INSERT INTO stock VALUES ('demo-a', 1), ('demo-b', 5);`);
}
export async function reserve(db, itemId, quantity, beforeWrite = async () => {}) {
  if (!Number.isSafeInteger(quantity) || quantity <= 0 || quantity > 100) {
    throw new Error('Geçersiz miktar');
  }
  const row = db.prepare('SELECT available FROM stock WHERE id = ?').get(itemId);
  if (!row || row.available < quantity) return false;
  await beforeWrite(); // Bu sırada başka istek aynı eski değeri okuyabilir.
  db.prepare('UPDATE stock SET available = ? WHERE id = ?').run(row.available - quantity, itemId);
  return true;
}
```

Sonra:

```js
// stok/ayir.js, açıklama amaçlı. Ürün ve çağıran yetkisi üst sınırda doğrulanır.
export function setup(db) {
  db.exec(`CREATE TABLE stock(id TEXT PRIMARY KEY, available INTEGER NOT NULL CHECK(available >= 0));
    INSERT INTO stock VALUES ('demo-a', 1), ('demo-b', 5);`);
}
export async function reserve(db, itemId, quantity, beforeWrite = async () => {}) {
  if (!Number.isSafeInteger(quantity) || quantity <= 0 || quantity > 100) {
    throw new Error('Geçersiz miktar');
  }
  await beforeWrite(); // Test, iki isteği aynı yazma sınırına getirir.
  const result = db.prepare(`UPDATE stock SET available = available - ?
    WHERE id = ? AND available >= ?`).run(quantity, itemId, quantity);
  return result.changes === 1;
}
// Yeterlilik denetimi ve azaltma tek veritabanı ifadesidir.
// Sipariş veya harcama kaydı eklenecekse aynı transaction içinde tutulmalıdır.
```

Düzeltmeyi kanıtlayan test:

```js
// stok/ayir.test.mjs, açıklama amaçlı. Kontrollü iç içe geçiş, gerçek SQLite.
import test from 'node:test';
import assert from 'node:assert/strict';
import { DatabaseSync } from 'node:sqlite';
const { setup, reserve } = await import(process.env.ORNEK_DOSYA);
test('son adet için yarışan iki işten yalnız biri kabul edilir', async () => {
  const db = new DatabaseSync(':memory:');
  try {
    setup(db);
    let arrivals = 0, release;
    const gate = new Promise(resolve => { release = resolve; });
    const pause = async () => { if (++arrivals === 2) release(); await gate; };
    const results = await Promise.all([
      reserve(db, 'demo-a', 1, pause), reserve(db, 'demo-a', 1, pause),
    ]);
    assert.equal(results.filter(Boolean).length, 1);
    assert.equal(db.prepare("SELECT available FROM stock WHERE id = 'demo-a'").get().available, 0);
    assert.equal(await reserve(db, 'demo-a', 1), false);
    assert.equal(await reserve(db, 'missing', 1), false);
    assert.equal(await reserve(db, 'demo-b', 2), true);
    assert.equal(db.prepare("SELECT available FROM stock WHERE id = 'demo-b'").get().available, 3);
    for (const quantity of [0, -1, 0.5, NaN, 101]) {
      await assert.rejects(reserve(db, 'demo-b', quantity), /Geçersiz miktar/);
    }
    assert.equal(db.prepare("SELECT available FROM stock WHERE id = 'demo-b'").get().available, 3);
  } finally { db.close(); }
});
```

## Ajan kuralı (AGENTS.md)

```md
## Son hak iki kez harcanıyor (vibecheck VC-087)
- Kalan miktar denetimini azaltmayla aynı atomik sınırda yap.
- Başarıyı etkilenen satır sayısından veya eşdeğer kesin sonuçtan belirle.
- Eski okuma değerini yeni miktar olarak koşulsuz yazma.
- Miktarın pozitif ve izinli aralıkta olduğunu doğrula.
- Bağlı harcama kaydını aynı transaction içinde tut.
- Yarış testinde kabul sayısını ve kalıcı miktarı birlikte doğrula.
```

## Kaynaklar

1. [SQLite UPDATE](https://www.sqlite.org/lang_update.html), SQLite
2. [Node.js SQLite parameter binding](https://nodejs.org/api/sqlite.html), Node.js
3. [CWE-367: Time-of-check Time-of-use Race Condition](https://cwe.mitre.org/data/definitions/367.html), MITRE

---

vibecheck · Komünite editörlüğü. Metin CC BY 4.0, prompt ve kural parçaları MIT-0. Kaynak: https://vibecheck.komunite.com.tr/madde/stok-kupon-kredi-eszamanli-iki-kez-harcaniyor
