PlusMagi's Blog By Pitt Phunsanit Shell Script เจาะลึกข้อกำหนด POSIX File Permissions และ System Calls

เจาะลึกข้อกำหนด POSIX File Permissions และ System Calls

ข้อกำหนด POSIX File Permissions และ System Calls สำหรับการจัดการไฟล์ คือรากฐานของระบบความปลอดภัยและการเข้าถึงทรัพยากรระดับล่าง (Low-level I/O) บนระบบปฏิบัติการตระกูล Unix-like ทั้งหมด โดยเชื่อมโยงระหว่างบิตข้อมูลใน Inode, การตรวจสอบสิทธิ์ของเคอร์เนล และ API ภาษา C


โครงสร้าง POSIX File Permissions (File Mode Bits)

ในระดับเคอร์เนล POSIX กำหนดให้สิทธิ์และชนิดของไฟล์ถูกเก็บอยู่ในตัวแปรประเภท mode_t (ขนาดอย่างน้อย 16 บิต) ภายในโครงสร้าง struct stat โดยแบ่งบิตออกเป็นส่วนๆ ดังนี้:

Plaintext

 15 14 13 12 | 11 10  9 |  8  7  6 |  5  4  3 |  2  1  0  (Bit index)
[ File Type ] [ Special ] [  Owner  ] [  Group  ] [  Other  ]

1. ค่าสิทธิ์มาตรฐาน (rwx)

  • Read (r / 4)
    อ่านเนื้อหาไฟล์ หรืออ่านรายชื่อไฟล์ใน Directory ผ่าน readdir()
  • Write (w / 2)
    แก้ไข/เขียนทับเนื้อหาไฟล์ หรือสร้าง/ลบ/เปลี่ยนชื่อไฟล์ภายใน Directory
  • Execute (x / 1)
    รันโปรแกรมไบนารี/สคริปต์ หรือสิทธิ์การ “traverse” (เดินข้ามผ่าน) Directory เพื่อเข้าถึงไฟล์ข้างใน

2. Special Permissions (บิตพิเศษ 3 บิต)

บิตพิเศษOctalสัญลักษณ์พฤติกรรมบน Regular Fileพฤติกรรมบน Directory
SetUID (S_ISUID)4000s (Owner)โปรเซสรันด้วยสิทธิ์ Effective UID ของเจ้าของไฟล์ไม่มีผลตามมาตรฐาน POSIX
SetGID (S_ISGID)2000s (Group)โปรเซสรันด้วยสิทธิ์ Effective GID ของกลุ่มไฟล์ไฟล์ใหม่ที่สร้างใต้ Directory นี้จะสืบทอด Group ID จากไดเรกทอรี
Sticky Bit (S_ISVTX)1000t (Other)ไม่มีผลในระบบสมัยใหม่ (อดีตเคยใช้แคช Text segment)ผู้ใช้จะลบหรือเปลี่ยนชื่อไฟล์ได้ก็ต่อเมื่อเป็นเจ้าของไฟล์นั้น หรือเจ้าของ Directory (เช่น /tmp)

อัลกอริทึมการตรวจสอบสิทธิ์ของ Kernel (Permission Check Algorithm)

เมื่อโปรเซสเรียก System Call เพื่อเข้าถึงไฟล์ เคอร์เนลจะตรวจสอบตามลำดับ จากบนลงล่าง และหยุดทันทีที่ตรงเงื่อนไขแรก (First match wins)

  1. Root Override
    หาก Effective UID เป็น 0 (Superuser) จะได้รับอนุญาตผ่านเกือบทุกกรณี (ยกเว้น Execute ที่ต้องการบิต x อย่างน้อย 1 จุด)
  2. Owner Check
    หาก Effective UID ของโปรเซสตรงกับ Owner UID ของไฟล์ -> ตรวจสอบเฉพาะชุดสิทธิ์ Owner เท่านั้น (หาก Owner บล็อกสิทธิ์ แม้ Group จะอนุญาต โปรเซสจะถูกปฏิเสธทันทีด้วยข้อผิดพลาด EACCES)
  3. Group Check
    หากตรงกับ Owner GID หรือตรงกับหนึ่งใน Supplementary Group IDs ของโปรเซส -> ตรวจสอบเฉพาะชุดสิทธิ์ Group
  4. Other Check
    หากไม่เข้าเงื่อนไขข้างต้น -> ตรวจสอบชุดสิทธิ์ Other

ระบบ POSIX System Calls พื้นฐานสำหรับ File I/O

POSIX กำหนด System Calls ผ่าน Header <fcntl.h>, <unistd.h>, และ <sys/stat.h> โดยส่งคืนผลลัพธ์เป็น File Descriptor (FD) ซึ่งเป็นตัวเลขจำนวนเต็มชี้ไปยัง File Table Entry ใน Kernel Space

Plaintext

User Process               Kernel Space
+---------------+         +-----------------------+         +-------------------+
|  FD Table     | ------> |  Open File Table      | ------> |  V-node / Inode   |
|  0: stdin     |         |  - Status flags (R/W) |         |  - File size      |
|  1: stdout    |         |  - Current offset     |         |  - Mode &amp; Owners  |
|  3: myfile.txt|         +-----------------------+         +-------------------+
+---------------+

1. open / openat – การเปิดหรือสร้างไฟล์

C

int open(const char *path, int oflag, ... /* mode_t mode */);

  • Flags หลัก (oflag)
    • โหมดการเข้าถึง (ต้องเลือก 1 อย่าง)
      O_RDONLY, O_WRONLY, O_RDWR
    • Action flags (รวมกันด้วย Bitwise OR)
      O_CREAT (สร้างไฟล์ถ้าไม่มี), O_TRUNC (ล้างข้อมูลเดิมให้เหลือ 0), O_APPEND (เลื่อน offset ไปท้ายไฟล์ก่อนเขียนเสมอ), O_CLOEXEC (ปิด FD อัตโนมัติเมื่อเรียก exec)
  • Mode
    จำเป็นต้องระบุเมื่อมี O_CREAT (เช่น 0644 หรือ S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) โดยค่าสิทธิ์จริงที่ถูกสร้างจะคำนวณผ่าน:$$\text{Permissions} = \text{mode} \ \& \ (\sim\text{umask})$$

2. read – การอ่านข้อมูล

C

ssize_t read(int fildes, void *buf, size_t nbyte);

  • พยายามอ่านข้อมูลจำนวน nbyte จากตำแหน่ง Offset ปัจจุบันเข้าไปยังบัฟเฟอร์ buf
  • Return Value
    • > 0
      จำนวนไบต์ที่อ่านได้จริง (อาจน้อยกว่า nbyte)
    • 0
      ถึงจุดสิ้นสุดของไฟล์ (End of File – EOF)
    • -1
      เกิดข้อผิดพลาด ตรวจสอบสาเหตุผ่านตัวแปร errno (เช่น EINTR, EIO, EBADF)

3. write – การเขียนข้อมูล

C

ssize_t write(int fildes, const void *buf, size_t nbyte);

  • เขียนข้อมูลจำนวน nbyte จาก buf ลงในไฟล์
  • Return Value
    จำนวนไบต์ที่เขียนสำเร็จ หรือ -1 เมื่อเกิด Error (เช่น ENOSPC กรณีดิสก์เต็ม)
  • การเขียนผ่าน write ปกติจะค้างอยู่ใน Page Cache ของ Kernel ยังไม่ลง Disk ทันทีจนกว่าจะเรียก fsync(fildes)

4. lseek – การเปลี่ยนตำแหน่ง Read/Write Offset

C

off_t lseek(int fildes, off_t offset, int whence);

  • whence Directive:
    • SEEK_SET
      เลื่อนไปที่ตำแหน่ง offset โดยนับจากจุดเริ่มต้นของไฟล์
    • SEEK_CUR
      เลื่อนไปจากตำแหน่งปัจจุบันบวกด้วย offset
    • SEEK_END
      เลื่อนไปจากจุดสิ้นสุดของไฟล์บวกด้วย offset (สามารถเลื่อนเลยขนาดไฟล์จริงเพื่อสร้าง Sparse File ได้)

5. close – การคืนทรัพยากร File Descriptor

C

int close(int fildes);

  • ปลดปล่อย File Descriptor คืนสู่ตาราง เพื่อนำกลับไปใช้ใหม่ ป้องกันปัญหา File Descriptor Leak (จำกัดตามขีดจำกัด RLIMIT_NOFILE)

ตัวอย่างการทำงานจริง: การสร้างไฟล์แบบกำหนดสิทธิ์และเขียนข้อมูล (POSIX C)

C

#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

int main(void) {
    const char *path = "secure_output.txt";
    const char *data = "Classified system information\n";
    
    // กำหนดสิทธิ์: เฉพาะ Owner เท่านั้นที่อ่านและเขียนได้ (0600)
    mode_t file_mode = S_IRUSR | S_IWUSR;
    
    // สั่งเปิดไฟล์: ถ้าไม่มีให้สร้าง, ถ้ามีให้เขียนทับ, ป้องกันการรั่วไหลไปยัง Child process
    int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, file_mode);
    if (fd == -1) {
        perror("Error opening file");
        return 1;
    }

    size_t bytes_to_write = strlen(data);
    ssize_t bytes_written = write(fd, data, bytes_to_write);
    
    if (bytes_written == -1) {
        perror("Error writing to file");
        close(fd);
        return 1;
    }

    // บังคับ Flush ข้อมูลและ Metadata จาก Page Cache ลง Disk
    if (fsync(fd) == -1) {
        perror("Error syncing file to disk");
    }

    if (close(fd) == -1) {
        perror("Error closing file");
        return 1;
    }

    printf("File created and written successfully with mode 0600\n");
    return 0;
}


ความแตกต่างระหว่าง Low-level I/O และ C Standard Library

คุณสมบัติPOSIX System Calls (open, read, write)C Standard I/O (fopen, fread, fwrite)
Interface HandleFile Descriptor (จำนวนเต็ม int)Stream Pointer (FILE *)
Bufferingไม่มี Buffer ฝั่ง User space (เรียก Trap สู่ Kernel ทันที)มี User-space Buffer ช่วยลด Overhead ของ System Call
Portabilityเฉพาะระบบที่รองรับมาตรฐาน POSIXทุกระบบที่มี C Runtime (รวมทั้ง Bare-metal และ Windows ทั่วไป)
Use Casesงานระดับระบบ, IPC, Sockets, ควบคุม Permission, Direct I/Oงานประมวลผลข้อความทั่วไป, ไฟล์ Format, งานข้ามแพลตฟอร์ม