Post

Linux'ta Dosya ve Yetki Problemlerini Kernel Seviyesinde Anlamak

🇹🇷 Program çalışıyor ama config dosyasını okumuyor mu? 'Permission denied' hatası alıyorsunuz ama root musunuz? open, stat, access ve container yalıtım dünyasında kaybolmadan dosya sistemi sorunlarını çözme rehberi.

Linux'ta Dosya ve Yetki Problemlerini Kernel Seviyesinde Anlamak

Önceki yazımızda strace ile Debugging Ne Zaman Yeterli Değildir? başlığı altında, bu güçlü aracın sınırlarını ve üretim ortamındaki risklerini konuştuk.

Peki ya strace her şey yolunda derken programınız sessizce “Erişim Engellendi” hatasıyla boğuşuyorsa? Loglarda hata yok, CPU normal, ama uygulama çalışmıyor.

Bir DevOps mühendisinin en büyük kabusu programın çökmesi değil, sessizce yanlış çalışmasıdır. Çöken program iz bırakır; çökemeyen program ise sizi hayalet kovalamaya gönderir.

Bu yazıda Docker konteynerlarından karmaşık izin yapılarına, sembolik linklerden görünmez karakterlere kadar Linux dosya sisteminin (VFS) “karanlık madde”sini inceleyeceğiz. “Dosya orada işte!” diye bağırdığınız o anların arkasındaki Kernel bürokrasisini, stat, access ve open çağrılarının gizli dansını birlikte çözeceğiz.

📌 Bu Yazıda Ne Öğreneceksin?

Bu yazının sonunda:

  • Root kullanıcısı olsanız bile neden “Permission Denied” hatası alabileceğinizi anlayacaksınız
  • stat, access ve open çağrılarının arasındaki farkın neden hayati önem taşıdığını göreceksiniz
  • “Dosya orada ama program görmüyor” dediğinizde, sorunun Relative Path, Namespace veya Symlink kaynaklı olup olmadığını çözebileceksiniz
  • Container (Docker/K8s) dünyasında dosyaların nasıl başka bir boyuta ışınlandığını öğreneceksiniz
  • Yazılım güvenliğindeki en sinsi açıklardan biri olan Race Condition (TOCTOU) saldırısının nasıl çalıştığını kavrayacaksınız

Kısacası; Dosya sisteminin sadece klasörler ve izinlerden ibaret olmadığını, arkada dönen devasa Kernel bürokrasisini yönetmeyi öğreneceksiniz.

🗄️ 1. Büyük Resim: Bürokrasi ve Arşiv Odası

Linux’ta (ve genel olarak UNIX sistemlerde) bir programın dosyaya erişim sürecini anlamak için, teknik jargonları bir kenara bırakıp basit bir devlet dairesi metaforu kullanalım. Bu metafor, soyut kavramları somutlaştırmamıza yardımcı olacak.

  • Programınız (Process): Vatandaş. Bir işlemi gerçekleştirmek istiyor (Örn: Nüfus kaydını almak).
  • Kernel (Çekirdek): Arşiv Memuru. Arşiv odasına (Diske) girme yetkisi olan tek kişi. Silahlı nöbetçi.
  • Dosya Sistemi (Filesystem): Dev, karmaşık ve labirent gibi bir arşiv odası. Milyonlarca dosya (klasör) raflarda duruyor.
  • Syscall (Sistem Çağrısı): Vatandaşın memura uzattığı, standart formatta bir dilekçe formu.

Şunu asla unutmayın: Programınız (Vatandaş) asla, ama asla doğrudan arşiv odasına giremez. Diskteki manyetik plakalar üzerindeki “0” ve “1”lere doğrudan dokunamaz. Donanımla konuşma yetkisi sadece Kernel’dadır. Programınızın yapabileceği tek şey, camın arkasındaki memura (Kernel) bir form uzatıp beklemektir.

Dosya işlemleri için en sık kullanılan 3 “dilekçe” türü şunlardır:

sequenceDiagram
    participant App as Vatandaş (Process)
    participant Kernel as Memur (Kernel)
    participant FS as Arşiv Odası (Disk)

    Note over App, Kernel: A. Dosya Var mı? (stat)
    App->>Kernel: Soru: "Bu dosya var mı?" (stat)
    Kernel->>FS: Kayıtlara (Inode) Bak
    FS-->>Kernel: Kayıt No: 42 (Mevcut)
    Kernel-->>App: Cevap: "Evet, var."

    Note over App, Kernel: B. İznim Var mı? (access)
    App->>Kernel: Soru: "Okuyabilir miyim?" (access)
    Note right of Kernel: Kimlik Kontrolü (UID/GID vs Permission Bits)
    Kernel-->>App: Cevap: "Evet, geçebilirsin."

    Note over App, Kernel: C. Dosyayı Ver! (open)
    App->>Kernel: Emir: "Dosyayı aç" (open)
    Kernel->>FS: Dosyayı Getir
    FS-->>Kernel: Veri Bloğu
    Kernel-->>App: Sonuç: Al sana File Descriptor (No: 3)

A. stat() Dilekçesi: “Böyle bir dosya var mı?”

Vatandaş camı tıklatır ve sorar: “Memur bey, ‘config.json’ adında bir dosya envanterde var mı? Varsa boyutu ne kadar? Kim oluşturmuş?”

Memur (Kernel) içeri gider, kataloğa (Inode Table) bakar ve geri döner:

  • Durum 1: “Evet kayıtlarımızda var. 2048 bayt büyüklüğünde, sahibi sensin, son değiştirilme tarihi de dün. Kimlik Kartı Numarası (Inode): 42” (Success)
  • Durum 2: “Hayır, kayıtlarımızda böyle bir isim geçmiyor.” (ENOENT - No such file or directory)

🧠 Teknik Detay: Inode (Index Node)

Inode (Index Node), dosyanın kimlik kartıdır. Linux’ta her dosyanın benzersiz bir Inode numarası vardır. Inode şunları tutar: Dosya boyutu, sahibi, izinleri, oluşturulma tarihi ve diskin neresinde olduğu. Şunu tutmaz: Dosyanın ADI! Dosya adı, sadece klasör (directory) içinde tutulan bir etikettir. Bu yüzden dosya adını değiştirmek (mv) çok hızlıdır; sadece etiketi değiştirirsiniz, dosyanın kendisine (Inode) dokunmazsınız. stat komutu işte bu kimlik kartını (Inode) okur.

Kritik Nokta: Memur dosyayı getirmedi. İçini açıp okumadı. Sadece varlığını ve meta-verilerini (metadata) teyit etti. Yüksek seviyeli dillerde os.path.exists() veya File.Exists() fonksiyonları arkada bu çağrıyı yapar.

B. access() Dilekçesi: “Buna yetkim var mı?”

Vatandaş heyecanla sorar: “Peki, bu dosyayı okumaya iznim var mı? Alıp eve götürebilir miyim?”

Memur, vatandaşın kimliğine (User ID, Group ID) bakar. Sonra dosyanın üzerindeki mühre (Permission Bits - rwx) bakar.

  • Eğer vatandaş dosyanın sahibiyse ve ‘r’ (read) hakkı varsa: “Geçebilirsin.”
  • Eğer vatandaş dosyanın sahibi değilse ve ‘other’ grubunda da izin yoksa: “Yassah hemşerim. Git buradan.” (EACCES - Permission Denied)

C. open() Dilekçesi: “Dosyayı getir!”

Vatandaş artık emreder: “Tamam memur bey, o dosyayı aç ve bana ver. Okumaya başlıyorum.”

Bu en maliyetli işlemdir. Memur arşiv odasına gider:

  1. Dosyanın fiziksel konumunu bulur.
  2. Diskin o sektörünü okur.
  3. Vatandaş için geçici bir takip numarası oluşturur (File Descriptor).
  4. Cama bu numarayı yapıştırır ve vatandaşa der ki: “Al, senin dosya numaran 3. Bundan sonra bana dosyanın adını söyleme, sadece ‘3 numaralı dosyadan 100 sayfa oku’ de.”

🧠 Teknik Detay: File Descriptor (Dosya Tanımlayıcısı)

Dosya tanımlayıcısı, Kernel’ın “açık dosyaları” takip etmek için verdiği pozitif bir tam sayıdır.

  • Neden Önemli? Program her işlemde uzun uzun /var/log/myapp.log demek yerine sadece 3 der. Bu çok daha hızlıdır.
  • Analoji: Bankadaki gişe numaranız gibidir. Müşteri hizmetlerine (Kernel) gittiğinizde adınızı değil, fiş numaranızı söylersiniz.

Sorun şu ki; Python, Java, Node.js veya Go gibi yüksek seviyeli diller, bu bürokratik adımları sizden gizler. Siz sadece f = open("file.txt") yazarsınız. Ama arka planda bu üçlü dans (hatta bazen dörtlü, lstat vs.) döner. Ve dansın bir adımı, en ufak bir ritim bozukluğunda aksadığında, programınız sessizce hata verir veya yanlış davranır.

İşte biz strace ile bu camın arkasına geçip, memurun defterine bakacağız.

🕵️‍♂️ 2. Vaka Analizi: “Dosyam Burada, Gözümün Önünde!”

En klasik senaryoyla başlayalım ve derinleşelim. Bir Python web uygulaması yazdınız. Uygulamanızın hemen yanında config.yaml duruyor. ls -l yapıyorsunuz, dosya orada. İzinler tam. Ama uygulamayı çalıştırdığınızda loglarda şunu görüyorsunuz:

[ERROR] Configuration file 'config.yaml' not found. Reverting to defaults.

Yazılımcı refleksiyle hemen koda bakarsınız:

1
2
# app.py
config_file = open("config.yaml", "r")

“Kod doğru! Dosya adı doğru! Neden çalışmıyor?!”

Hemen cerrah önlüğümüzü giyelim (strace) ve programın gerçekten ne yapmaya çalıştığına bakalım.

1
2
# Servisi strace ile başlatıp dosya sistemi çağrılarını filtreliyoruz
strace -e trace=open,openat,stat,newfstatat,access python3 app.py

Çıktı terminale akıyor ve şu satırları yakalıyorsunuz:

1
2
3
openat(AT_FDCWD, "/usr/lib/python3.8/config.yaml", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/myapp/config.yaml", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/kullanici/config.yaml", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)

🧠 Teknik Detay: Neden open değil de openat?

Dikkat ettiyseniz strace çıktısında open yerine openat var. Modern Linux sistemleri, “Thread Safety” ve performans için openat kullanır. AT_FDCWD argümanı, “Bana verdiğin görece yolu (relative path), o anki çalışma dizinini (CWD) baz alarak çöz” demektir. Bu sayede kernel, dosya yolunu hesaplarken daha az CPU harcar.

Ve aniden aydınlanma yaşarsınız. Program, sizin sandığınız config.yaml dosyasına bakmıyor! Ya kullandığınız bir kütüphane varsayılan yolları (search paths) tarıyor, ya da PYTHONPATH ortam değişkeni farklı ayarlanmış.

⚠️ Göreli Yol (Relative Path) Tuzağı

Bazen de çıktı şöyledir ama hata devam eder:

1
openat(AT_FDCWD, "config.yaml", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)

Burada ENOENT (Error NO ENTry) hatasını görüyoruz. Ama openat fonksiyonunun ilk parametresine dikkat edin: AT_FDCWD. Bu, “Current Working Directory” (Mevcut Çalışma Dizini) demektir. Peki o anki çalışma dizini neresi?

strace bunu bize getcwd syscall’ı yoksa doğrudan göstermez. Ama ls -l /proc/<PID>/cwd ile görebiliriz. Programın çalışma dizini, programın bulunduğu yer değil, komutun çalıştırıldığı yerdir.

Senaryo:

  • Uygulamanız /opt/myapp/app.py içinde.
  • Config dosyanız /opt/myapp/config.yaml içinde.
  • Siz terminalde /root dizinindesiniz.
  • Komutu şöyle çalıştırdınız: python3 /opt/myapp/app.py

Bu durumda Python süreci /root dizininde başlar. Kodunuz open("config.yaml") dediğinde, sistem /root/config.yaml dosyasını arar. Ve tabii ki bulamaz.

💡 Profesyonel Çözüm: Scriptlerinizde veya uygulamalarınızda asla “Göreli Yol” (Relative Path) kullanmayın. Dosya yollarını her zaman çalıştırılan dosyanın konumuna göre dinamik olarak hesaplayın.

Python Örneği:

1
2
3
4
5
6
7
8
import os
# Bu dosyanın (app.py) tam yolunu al
base_dir = os.path.dirname(os.path.abspath(__file__))
# Config dosyasının tam yolunu oluştur
config_path = os.path.join(base_dir, 'config.yaml')

with open(config_path, 'r') as f:
    ...

Bu taktikle, programı ister /tmp dizininden, ister uzaydan çağırın, her zaman doğru dosyayı bulur.

🚫 3. Vaka Analizi: “Ama Ben Root’um!” (Permission Denied)

Linux dünyasında yeni olanların en sık düştüğü yanılgı: “Root kullanıcısı (Süper Kullanıcı) her şeyi yapabilir.” Eskiden bu doğruydu. Ama modern, güvenliği sıkılaştırılmış Linux sistemlerinde Root bile olsanız duvara toslayabileceğiniz çok senaryo var.

Diyelim ki bir log dosyasına (/var/log/myapp.log) yazmaya çalışıyorsunuz ama EACCES (Permission Denied) alıyorsunuz.

1
2
# Debug sırasında
strace -e openat,write logger-app

Çıktı acımasız:

1
openat(AT_FDCWD, "/var/log/myapp.log", O_WRONLY|O_CREAT|O_APPEND, 0666) = -1 EACCES (Permission denied)

Root’sunuz. ls -l yaptınız, dosya izinleri rw-r--r--. Hatta çaresizlikten chmod 777 yaptınız. Hala hata veriyor. Neden?

İşte şüpheli listemiz:

A. Read-Only Filesystem (Salt Okunur Dosya Sistemi)

Container ve Microservice dünyasında (Docker/Kubernetes), “Immutable Infrastructure” prensibi gereği dosya sistemleri genellikle Read-Only (Salt Okunur) olarak bağlanır (mount edilir). Uygulamanız hacklense bile saldırgan sistem dosyalarını değiştiremesin diye.

Sadece /tmp veya özel tanımlanmış Volume alanlarına yazma izniniz vardır. Bunu kontrol etmek için mount komutuna bakalım:

1
2
3
mount | grep " / "
# Çıktı:
# /dev/sda1 on / type ext4 (ro,relatime)

O parantez içindeki minik ro harfleri, “Read-Only” demektir. Kök dizine hiçbir şey yazamazsınız. İsterseniz Dünya’nın iplerini elinde tutan Root olun, Kernel fiziksel olarak yazmayı reddeder.

B. Görünmez Kalkanlar: SELinux & AppArmor

Standart Linux izinleri (User/Group/Other) “Discretionary Access Control” (DAC) olarak bilinir. Yani inisiyatif dosya sahibindedir. Ancak kurumsal dağıtımlarda (RHEL, CentOS, Ubuntu Server) “Mandatory Access Control” (MAC) devreye girer.

SELinux veya AppArmor, her sürece bir Profil atar. Örneğin: “Apache süreci (httpd), sadece /var/www/html okuyabilir ve /var/log/httpd yazabilir. /etc/shadow dosyasına dokunamaz, Root olsa bile!”

Siz Root olarak Apache’yi çalıştırırsınız. Apache bir açık bulup /etc/shadow okumaya çalışır. Kernel bakar: “Tamam UID=0 (Root), normalde izin var. AMA AppArmor profilinde bu işlem YASAK.” Ve EACCES hatasını yapıştırır.

Nasıl Tespit Edilir? Bu hatalar standart loglara düşmez. Audit loglarına bakmanız gerekir.

1
2
3
4
5
6
7
# Kernel mesajlarını izle (En hızlı yöntem)
dmesg | tail -n 20

# Veya AppArmor loglarını tara
grep "DENIED" /var/log/syslog
# Örnek Çıktı:
# [1234.56] audit: type=1400 audit(16000000:1): apparmor="DENIED" operation="open" profile="/usr/bin/logger-app" name="/var/log/myapp.log"

Gördüğünüz gibi, katil uşak değil, AppArmor çıktı.

Peki Çözüm Ne?

Hemen setenforce 0 veya aa-complain ile korumayı kapatıp deneme yapın. Eğer çalışıyorsa, profili güncellemeniz gerekir. Production’da korumayı tamamen kapatmak yerine, sadece ilgili sürece özel izin (capability) tanımlamalısınız.

C. Attribute Tuzağı (chattr)

Bazen sistem yöneticileri (veya rootkit yemiş sunucularda saldırganlar), kritik dosyaların silinmesini önlemek için Ext4 Attribute özelliklerini kullanır. Bir dosyaya immutable (+i) bayrağı set edilirse, o dosyayı kimse silemez, adını değiştiremez ve üzerine yazamaz. Root bile.

Kontrol komutu lsattr:

1
2
lsattr /var/log/myapp.log
# ----i---------e---- /var/log/myapp.log

O baştaki i harfini görüyor musunuz? O “Dokunulmazlık” demek. Root bile olsanız bu dosyaya yazamazsınız. Ancak Root yetkisini kullanarak bu korumayı kaldırabilirsiniz:

1
sudo chattr -i /var/log/myapp.log
flowchart TD
    Start("Erişim Reddedildi!") --> Root{"Root musun?"}
    
    Root -- "Hayır" --> Perms{"Dosya İzinleri?"}
    Perms -- "İzin Yok" --> Chown["chown / chmod"]
    
    Root -- "Evet" --> RO{"Read-Only FS?"}
    RO -- "Evet" --> Mount["mount -o remount,rw"]
    
    RO -- "Hayır" --> Attr{"Immutable (i)?"}
    Attr -- "Evet" --> Chattr["chattr -i"]
    
    Attr -- "Hayır" --> MAC{"AppArmor/SELinux?"}
    MAC -- "Evet (Loglarda Var)" --> Security["Profil Güncelle"]
    MAC -- "Hayır" --> Strange["Gariplikler..."]
    
    Strange --> NS["Namespace / PrivateTmp?"]
    
    style Start stroke:#d84315,stroke-width:2px
    style Security stroke:#304ffe,stroke-width:2px
    style Chown stroke:#2e7d32,stroke-width:2px

📦 4. Modern Dünyanın Tuzakları: Container’lar ve Namespace İzolasyonu

Eskiden sunucu dediğimiz şey tek bir gerçeklikti. Tek bir dosya sistemi, tek bir process listesi vardı. Şimdi ise Container teknolojileri sayesinde her process kendi Matrix simülasyonunda (Namespace) yaşıyor. Process, tüm sistemi gördüğünü sanıyor ama aslında sadece ona ayrılan küçük bir odayı görüyor.

🧠 Teknik Detay: Namespace (İsim Alanı)

Namespace, işlemi (process) sistemin geri kalanından izole eden sanal bir duvardır.

  • Mount Namespace: İşlemin sadece kendi diskini görmesini sağlar.
  • PID Namespace: İşlemin sadece kendi süreçlerini görmesini sağlar.
  • Analoji: Otel odası gibidir. Odanın elektriğini keserseniz (Namespace içinde işlem yaparsanız), yan oda (Host sistemi) bundan etkilenmez.

Systemd PrivateTmp=true Tuzağı

Systemd, modern Linux’un kalbidir ve servisleri izole etmek için harika özelliklere sahiptir. Bunlardan biri PrivateTmp. Servis dosyasında (/etc/systemd/system/myapp.service) şu satır varsa:

1
2
[Service]
PrivateTmp=true

Servisiniz /tmp/dosya.txt oluşturduğunda, bu dosya gerçek sistemdeki /tmp altında oluşmaz! Systemd, o servis için özel, izole bir /tmp alanı yaratır (Mount Namespace). Dosya aslında şurada bir yerdedir: /tmp/systemd-private-<hash>-myapp.service-<hash>/tmp/dosya.txt

Siz dışarıdan (Host makineden) /tmp dizinine bakıp “Dosya oluşmamış, program bozuk!” dersiniz. Oysa program görevini yapmıştır, sadece farklı bir boyuttadır.

Nasıl yakalarız? strace çıktısında dosya yolunun yine /tmp/dosya.txt olduğunu görürsünüz. Bu sizi yanıltır. Çözüm, o sürecin gördüğü dosya sistemine girmektir (nsenter):

1
2
3
4
5
6
7
8
9
10
11
# 1. Hedef sürecin PID'sini bulun
PID=$(pidof my-web-app)

# 2. O sürecin dosya sistemi namespace'ine (mnt) geçiş yapın
# --mount: Hedef sürecin Mount tablosunu (gördüğü dosyaları) yükle
# --target: Hangi sürecin dünyasına gireceğimizi belirt
sudo nsenter --target $PID --mount /bin/bash

# 3. Artık o sürecin gördüğü dünyadasınız!
ls -l /tmp/dosya.txt
# Ve dosya orada! Çıkmak için 'exit' yazın.

OverlayFS ve “Copy-Up” Gariplikleri

Docker gibi container sistemleri OverlayFS kullanır. Bu, katmanlı bir dosya sistemidir. Alt katmanda (LowerDir) işletim sistemi imajı (Image) bulunur ve salt okunurdur. Üst katmanda (UpperDir) ise sizin yaptığınız değişiklikler tutulur.

Siz bir dosyayı değiştirmek istediğinizde (open(..., O_WRONLY)), OverlayFS o dosyanın bir kopyasını alt katmandan üst katmana taşır (Copy-Up işlemi).

🧠 Teknik Detay: OverlayFS ve Copy-Up

OverlayFS, “Sandviç Dosya Sistemi”dir. En altta salt okunur imaj (Image), en üstte yazılabilir katman (Container Layer) vardır.

  • Copy-Up: Alt kattaki bir dosyayı değiştirmek isterseniz, Kernel önce o dosyayı üst kata kopyalar, sonra değişikliği oraya yapar.
  • Analoji: Asetat kağıdı ile çizim yapmak gibidir. Mona Lisa tablosunun (Image) üzerine şeffaf kağıt koyup bıyık çizersiniz (Container). Tablo bozulmaz ama bakan kişi bıyıklı görür.
  • Risk: Bu kopyalama sırasında dosyanın Inode numarası değişir!
graph TD
    subgraph Merged["Görünen Dosya Sistemi"]
        F1["Dosya A (Upper)"]
        F2["Dosya B (Lower)"]
    end
    
    subgraph Upper["Upper Dir (Yazılabilir)"]
        F1_Up["Dosya A (Değişmiş)"]
        F3_Up["Dosya C (Yeni)"]
    end
    
    subgraph Lower["Lower Dir (Salt Okunur)"]
        F1_Low["Dosya A (Orijinal)"]
        F2_Low["Dosya B"]
    end
    
    Merged --> Upper
    Merged --> Lower
    
    F1_Up -.->|"Copy-Up (Inode Değişir!)"| F1_Low
    
    style Upper stroke:#01579b
    style Lower stroke:#e65100
    style Merged stroke:#1b5e20

Bunun Sonuçları Nedir?

  1. Log Rotation Bozulur: logrotate eski inode’u yeniden adlandırıp yeni dosya açarken, uygulama hala eski (silinmiş) inode’a yazmaya devam edebilir. Disk dolsa bile dosyayı bulamazsınız (lsof +L1 ile yakalanır).
  2. Config Hot-Reload Çalışmaz: Nginx veya Viper kullanan Go uygulamaları, dosya değişimini inotify ile izler. Inode değiştiğinde, izleme (watch) kopar ve uygulamanız güncellemeden habersiz kalır.
  3. Çözüm: Uygulamanızı restart etmek veya dosya değişimini klasör seviyesinde izlemektir.

⚡ 5. İleri Seviye: “Race Condition” ve TOCTOU

Kıdemli yazılımcı mülakatlarının vazgeçilmez sorusu: TOCTOU (Time-of-Check to Time-of-Use).

Diyelim ki C dilinde (veya mantık olarak herhangi bir dilde) şöyle güvenli bir kod yazdığınızı sanıyorsunuz:

1
2
3
4
5
6
7
8
9
10
11
// GÜVENSİZ KOD ÖRNEĞİ
char *dosya = "/tmp/gecici_dosya";

// 1. ADIM: Dosyaya yazma yetkim var mı? (Time of Check)
if (access(dosya, W_OK) == 0) {
    // -- KRİTİK ZAMAN ARALIĞI --
    
    // 2. ADIM: Dosyayı aç ve yaz (Time of Use)
    f = open(dosya, O_WRONLY);
    write(f, "Gizli Veri", 10);
}

Mantık doğru gibi, değil mi? Önce kontrol et, sonra yap. Ama bu bir tuzaktır.

Kernel seviyesinde access çağrısı ile open çağrısı arasında, mikrosaniyeler bile olsa bir zaman farkı vardır. Çok kullanıcılı bir sistemde, kötü niyetli bir saldırgan tam o aralıkta /tmp/gecici_dosya‘yı sili, yerine /etc/passwd dosyasına giden bir sembolik link (symlink) koyabilir.

Programınız access kontrolünü geçer (çünkü ilk başta dosya normaldi). Sonra open çağrısı yapar. Ama artık dosya /etc/passwd olmuştur. Ve programınız root yetkisiyle çalışıyorsa, saldırgan sistemin şifre dosyasının üzerine “Gizli Veri” yazdırarak sistemi çökertebilir veya değiştirebilir.

Kernel Seviyesinde Doğrusu Nedir?

Felsefe şudur: “İzin isteme, özür dile.” (EAFP - Easier to Ask for Forgiveness than Permission).

Kontrol etmeyin. Direkt açmayı deneyin (open). Kernel bu işlemi Atomik (bölünemez, tek parça) olarak yapar. Araya kimse giremez. Ya hep ya hiç. Eğer yetkiniz yoksa open başarısız olur ve EACCES hatası döner. Siz de bu hatayı yakalayıp (try-catch veya errno kontrolü) ona göre davranırsınız. Bu yöntem %100 güvenlidir.

⚡ 6. Hayalet Dosyalar: “Sildim Ama Yer Açılmadı!”

Production sistemlerinde SysAdmin’leri çıldırtan efsanevi bir senaryo daha vardır.

Senaryo: Disk doluluğu alarmı çalar (%98). Log dizinine girersiniz ve 20GB büyüklüğünde devasa bir access.log görürsünüz. “Hah, buldum seni!” diyerek rm access.log komutunu yapıştırırsınız. Sonra gururla df -h yazarsınız… ama o da ne? Disk hala %98 dolu!

Dosya yok oldu (ls ile görünmüyor) ama kapladığı alan serbest kalmadı. Nasıl olur?

Arka Planda Ne Oldu? (Reference Counting)

Linux’ta bir dosyanın diskten silinmesi için iki koşulun sağlanması gerekir:

  1. Link Count = 0: Dosyayı işaret eden dosya adı sayısı (Directory Entry) sıfır olmalı. (rm bunu yapar)
  2. Open File Handle Count = 0: Dosyayı açık tutan süreç sayısı sıfır olmalı.

Siz rm diyerek sadece dosya adını (etiketi) sildiniz. Ama arka planda Nginx veya Java uygulaması o dosyayı hala tutuyor (Open File Descriptor). Kernel, “Bu dosya hala kullanımda, verisini silemem” der. Dosya artık isimsiz bir hayalet olarak diskte yaşamaya devam eder.

graph TD
    subgraph Kutuphane["Dosya Sistemi"]
        Inode((("Inode (Veri)")))
    end
    
    subgraph Rehber["Dizin (Directory)"]
        Ad["Dosya Adı: access.log"]
    end
    
    subgraph App["Uygulama (Process)"]
        FD["File Desc: 3"]
    end
    
    Ad -->|Link| Inode
    FD -.->|Open Handle| Inode
    
    style Inode stroke:#e65100,stroke-width:2px
    style Ad stroke:#fbc02d,stroke-dasharray: 5 5

Nasıl Tespit Edilir?

Bu hayaletleri avlamanın tek yolu lsof‘a “Link count’u 0 olan ama açık olan dosyaları göster” demektir:

1
2
3
lsof +L1
# COMMAND   PID  USER   FD   TYPE DEVICE SIZE/OFF NLINK  NAME
# nginx     123  root    3u   REG  253,1    20G     0    /var/log/access.log (deleted)

Çözüm:

  1. Süreci Restart Etmek: systemctl restart nginx. Süreç kapanınca File Descriptor kapanır, Reference count 0 olur, Kernel veriyi siler.
  2. Truncate (Budama): Dosyayı silmek yerine içini boşaltın.
    1
    2
    
    # Dosya adını silmek yerine içeriğini sıfırla
    > /var/log/access.log
    

    Bu işlem Inode’u değiştirmez, sadece dosya boyutunu 0 yapar. Uygulama yazmaya devam eder ama dosya baştan başlar.

🛠️ 7. Debugging Kontrol Listeniz (Cheat Sheet)

Bir dahaki sefere programınız “Dosya yok” dediğinde veya “Access Denied” yediğinde panik yapmayın. Bu reçeteyi uygulayın:

  1. Yol Kontrolü (Path Check):
    • Dosya yolu Absolute mu Relative mi?
    • Komut readlink -f config.conf ile tam yolu görün.
  2. Kernel Gerçeği (strace):
    • strace -e trace=open,openat,access,stat <komut>
    • Program gerçekten hangi yola bakıyor? ENOENT mi alıyor EACCES mi?
  3. Kullanıcı Kontrolü:
    • Program hangi User/Group ile çalışıyor? (ps aux | grep app)
    • Dosyanın sahibi kim? (ls -n ile ID’leri görün).
  4. MAC Kalkanı:
    • dmesg | tail komutunda “AppArmor” veya “AVC” (SELinux) reddi var mı?
  5. Dondurulmuş Dosya:
    • lsattr <dosya> ile i (immutable) bayrağı var mı?
  6. Namespace Matriksi:
    • Dosya /tmp altında mı? Systemd PrivateTmp açık olabilir. nsenter ile içeri girip bakın.
  7. Inode Takibi:
    • Dosyayı editlediniz ama loglar değişmiyor mu? ls -i dosya ile inode numarasını not edin. lsof | grep dosya ile uygulamanın hangi inode’u tuttuğuna bakın. Farklılarsa restart zamanı gelmiştir.

💡 İpucu: lsof vs strace

  • lsof: Şu an HALİHAZIRDA açık olan dosyaları listeler. “Hangi dosya açık?” sorusunu cevaplar.
  • strace: Dosyanın NEDEN açılamadığını gösterir. “Program dosyayı açarken hangi hatayı alıyor?” sorusunu cevaplar. Önce lsof ile bakın, dosya yoksa veya açılamıyorsa strace ile nedenine inin.

🔮 Bir Sonraki Adım: “Ağ Var Ama Bağlantı Yok!”

Dosyalar, izinler, inode’lar… Dosya sisteminin bürokrasisini aştınız. Sırada daha büyük bir kaos var.

Ping atıyorum cevap var, ama uygulama sunucuya bağlanamıyor. Neden?

Connection Timeout ile Connection Refused arasındaki o ince çizgiyi, DNS sorgularının sessiz çığlıklarını ve Kernel’ın ağ trafiğini nasıl yönettiğini merak ediyor musunuz?

Bir sonraki yazıda, kabloların (socket’lerin) dünyasına iniyoruz.

🚀 Serinin Devam Yazısı: Linux’ta Network Sorunlarını Syscall Perspektifiyle Okumak

This post is licensed under CC BY 4.0 by the author.