Foundationপ্রথম নীতি থেকে
LEVEL 4লেসন ২/২৯কঠিন১ ঘণ্টা ১০ মিনিট

Kernel space বনাম user space — দেয়ালটা কে বানায়

Kernel Space and User Space

গত লেসনে বারবার বলেছি isolation 'hardware দিয়ে বলপূর্বক প্রয়োগ করা হয়' — এই লেসনে সেই বলপ্রয়োগের যন্ত্রটা খুলে দেখব। CPU-র ভেতরে দুই bit-এর একটা privilege level, যা প্রতিটা instruction আর প্রতিটা memory access-এ যাচাই হয়। আমরা দেখব একটা privileged instruction চালানোর চেষ্টা করলে ঠিক কী ঘটে, kernel কেন প্রতিটা process-এর address space-এ থাকে, আর ২০১৮ সালের Meltdown কীভাবে সেই ৫০ বছরের পুরনো নকশাটাকে রাতারাতি অনিরাপদ বানিয়ে দিয়েছিল।

এই লেসন শেষে আপনি পারবেন

  • x86-64-এর privilege ring (0–3) আর ARM64-এর exception level (EL0–EL3) ব্যাখ্যা করতে পারবেন, আর বলতে পারবেন কেন বাস্তবে x86-এ মাত্র দুটো ring ব্যবহৃত হয়
  • কেন isolation software দিয়ে নয়, hardware দিয়ে বলপূর্বক প্রয়োগ করতেই হয় — সেই যুক্তিটা নিজের ভাষায় গঠন করতে পারবেন
  • একটা user-mode প্রোগ্রাম ঠিক কোন কোন কাজ করতে পারে না তার তালিকা দিতে পারবেন, আর প্রতিটার জন্য hardware কোন সংকেত দেয় তা বলতে পারবেন
  • `/proc/self/maps` পড়ে user/kernel বিভাজন, `[vdso]`, `[stack]` আর ELF segment গুলো চিহ্নিত করতে পারবেন
  • Kernel কেন প্রতিটা process-এর address space-এ ম্যাপ করা থাকে সেটা ব্যাখ্যা করতে পারবেন, আর Meltdown কীভাবে সেই নকশাকে অনিরাপদ করে দিয়েছিল
  • KPTI কী করে, কেন এটা syscall-কে ধীর করে, আর `/sys/devices/system/cpu/vulnerabilities/` পড়ে নিজের মেশিনের অবস্থা যাচাই করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

গত লেসনে একটা দাবি বারবার করেছি, কিন্তু প্রমাণ করিনি:

OS আপনার প্রোগ্রামকে অন্য প্রোগ্রামের memory পড়তে দেয় না।

প্রশ্নটা করুন: দেয় না মানে কী? OS তো নিজেও একটা প্রোগ্রাম। একটা প্রোগ্রাম আরেকটা প্রোগ্রামকে কীভাবে “না” বলে?

ভাবুন আপনি একটা OS লিখছেন এবং শুধু software দিয়ে এটা করতে চান। আপনি প্রতিটা user program-এর প্রতিটা memory access-এর আগে একটা check বসাবেন। কিন্তু সেই check-টা কে চালাবে? User program নিজেই? তাহলে সে সেটা বাদ দিয়ে দিতে পারে।

এখানেই একটা মৌলিক সত্য: software নিজেকে নিজে রক্ষা করতে পারে না। যে কোড CPU-তে চলছে, সে CPU-র উপর সমান অধিকার রাখে — যদি না CPU নিজে পার্থক্য করে।

তাই সমাধানটা hardware-এ। CPU-র ভেতরে একটা ছোট register আছে যা বলে দেয় “এই মুহূর্তে যে কোড চলছে সে কতটা বিশ্বাসযোগ্য”। সেই মানটার উপর ভিত্তি করে CPU প্রতিটা instruction আর প্রতিটা memory access যাচাই করে।

দুই bit-এর একটা সংখ্যা। এটুকুই পুরো আধুনিক computing-এর নিরাপত্তার ভিত্তি।

মূল ধারণা

Privilege ring — x86-এর মডেল

x86 architecture চারটা privilege level সংজ্ঞায়িত করে, যাদের বলা হয় ring:

        ┌───────────────────────────────┐
        │  Ring 3 — user application    │   ← আপনার প্রোগ্রাম
        │  ┌─────────────────────────┐  │
        │  │ Ring 2 — (অব্যবহৃত)     │  │
        │  │  ┌───────────────────┐  │  │
        │  │  │ Ring 1 — (অব্যবহৃত)│  │  │
        │  │  │  ┌─────────────┐  │  │  │
        │  │  │  │  Ring 0     │  │  │  │   ← kernel
        │  │  │  │  kernel     │  │  │  │
        │  │  │  └─────────────┘  │  │  │
        │  │  └───────────────────┘  │  │
        │  └─────────────────────────┘  │
        └───────────────────────────────┘

        ভেতরে = বেশি ক্ষমতা
x86-এর চারটা ring — কিন্তু বাস্তব OS মাত্র দুটো ব্যবহার করে।

বর্তমান level-টা রাখা থাকে CPL (Current Privilege Level) নামে, যা cs segment register-এর নিচের দুই bit।

কেন মাত্র দুটো ব্যবহৃত হয়: ring 1 আর 2 তৈরি হয়েছিল device driver-কে kernel থেকে আলাদা রাখার জন্য। কিন্তু:

  1. পুরনো ARM, MIPS, RISC-V — কারো চারটা level নেই। Portable OS লিখতে হলে সর্বনিম্ন সাধারণ হর ব্যবহার করতে হয়
  2. Paging-এর protection bit-এ মাত্র একটা bit আছে (user/supervisor) — অর্থাৎ page-স্তরে ring 1 আর 2 কে ring 0 থেকে আলাদা করাই যায় না

তাই Linux, Windows, macOS — সবাই শুধু ring 0 (kernel) আর ring 3 (user) ব্যবহার করে।

ARM64-এর মডেল — exception level

ARM একই ধারণা আরো পরিষ্কারভাবে সাজিয়েছে:

Levelকে চলেউদাহরণ
EL0Applicationআপনার প্রোগ্রাম
EL1OS kernelLinux, XNU
EL2HypervisorKVM, Hyper-V
EL3Secure monitorARM TrustZone, firmware

লক্ষ্য করুন ARM-এ hypervisor-এর জন্য একটা আলাদা level আছে শুরু থেকেই — x86-কে সেটা পরে VT-x দিয়ে যোগ করতে হয়েছে।

EL3 সবচেয়ে আকর্ষণীয়: এটা secure world চালায়, যেখানে আপনার ফোনের fingerprint data আর DRM key থাকে। আপনার OS (EL1) সেই memory ছুঁতেও পারে না।

User mode ঠিক কী পারে না

তিন শ্রেণির নিষেধাজ্ঞা:

১. Privileged instruction

কিছু instruction শুধু ring 0-তে চলে:

Instructionকী করেকেন নিষিদ্ধ
cli / stiinterrupt বন্ধ/চালুinterrupt বন্ধ করে দিলে scheduler আর কখনো চলবে না — একটা প্রোগ্রাম পুরো মেশিন দখল করে নেবে
hltCPU থামানোএকই কারণ
mov cr3, raxpage table বদলানোঅন্য process-এর memory ম্যাপ করে ফেলা যেত
lgdt / lidtGDT/IDT বসানোনিজের interrupt handler বসিয়ে ring 0-তে ঢোকা যেত
wrmsrmodel-specific register লেখাCPU-র মৌলিক আচরণ বদলানো
in / outI/O port accessসরাসরি disk controller-এ কমান্ড পাঠানো যেত

চেষ্টা করলে CPU একটা #GP (General Protection) fault তোলে, যা kernel ধরে আর process-কে SIGSEGV বা SIGILL পাঠায়।

২. Kernel memory access

প্রতিটা page table entry-তে একটা U/S bit আছে (bit 2)। 0 মানে supervisor-only।

CPL 3-এ থেকে supervisor page ছোঁয়ার চেষ্টা করলে page fault। এটা instruction-স্তরের check নয় — এটা প্রতিটা memory access-এ MMU করে, hardware-এ, শূন্য অতিরিক্ত খরচে।

৩. সরাসরি hardware access

Device register গুলো হয় I/O port-এ (in/out — privileged), নয় memory-mapped I/O-তে (supervisor page)। দুইভাবেই বন্ধ।

Address space — kernel কোথায় থাকে

এখানে একটা বিস্ময় আছে যা প্রথমে অযৌক্তিক মনে হয়:

Kernel-এর কোড আর ডেটা প্রতিটা process-এর virtual address space-এ ম্যাপ করা থাকে।

x86-64-এ 48-bit canonical address space দুই ভাগে বিভক্ত:

0xFFFF_FFFF_FFFF_FFFF  ┌────────────────────────┐
                       │   kernel space         │
                       │   (সব process-এ একই)   │  128 TB
0xFFFF_8000_0000_0000  ├────────────────────────┤
                       │                        │
                       │  non-canonical hole    │  ব্যবহার করলে #GP
                       │                        │
0x0000_7FFF_FFFF_FFFF  ├────────────────────────┤
                       │   user space           │
                       │   (প্রতি process আলাদা) │  128 TB
0x0000_0000_0000_0000  └────────────────────────┘
x86-64 address space split — উপরের অর্ধেক kernel, নিচের অর্ধেক user। মাঝের বিশাল অংশ অব্যবহৃত (non-canonical)।

কেন এই নকশা? কারণ প্রতিটা syscall আর প্রতিটা interrupt-এ kernel-এর কোড চালাতে হয়। যদি kernel আলাদা address space-এ থাকত, তাহলে প্রতিবার ঢোকা-বেরোনোর সময় page table বদলাতে হতো — অর্থাৎ cr3 লিখতে হতো, যা পুরো TLB flush করে দেয়।

Kernel একই address space-এ থাকলে syscall শুধু একটা mode switch, কোনো address space switch নয়। TLB অক্ষত থাকে।

নিরাপত্তা আসে page table-এর U/S bit থেকে — kernel page গুলো supervisor-only, তাই ম্যাপ করা থাকলেও user mode ছুঁতে পারে না।

৫০ বছর ধরে এই নকশা নিরাপদ ছিল। ২০১৮-তে সেটা ভেঙে গেল।

ভেতরে কী ঘটছে

Mode switch — দেয়াল পার হওয়ার মুহূর্ত

CPL 3 থেকে CPL 0-তে যাওয়ার মাত্র তিনটা বৈধ পথ আছে:

ring 3 থেকে ring 0-তে ঢোকার তিনটা দরজা
  1. System callস্বেচ্ছায় — syscall / svc instruction
  2. Interruptবাইরে থেকে — timer, keyboard, network card
  3. Exceptionঅনিচ্ছাকৃত — page fault, divide by zero, #GP
  4. → CPU privilege level 0-তে সেট করেhardware, atomically
  5. → kernel stack-এ switch করেTSS.RSP0 থেকে — user stack বিশ্বাসযোগ্য নয়
  6. → IDT/MSR থেকে handler address লোড করেuser নির্ধারণ করতে পারে না
  7. → kernel কোড চলেএখন ring 0

তিনটা পথেই তিনটা জিনিস অপরিবর্তনীয়:

১. কোথায় যাবে সেটা kernel আগেই ঠিক করে রেখেছে। Interrupt-এর জন্য IDT (Interrupt Descriptor Table), syscall-এর জন্য IA32_LSTAR MSR। User program শুধু “যাও” বলতে পারে, “কোথায় যাও” বলতে পারে না।

২. Stack বদলে যায়। Kernel user-এর stack pointer বিশ্বাস করতে পারে না — সেটা invalid, unmapped, বা ইচ্ছাকৃতভাবে kernel memory-র দিকে তাক করা হতে পারে। তাই hardware TSS (Task State Segment)-এর RSP0 থেকে একটা নির্ভরযোগ্য kernel stack লোড করে।

৩. পুরো জিনিসটা atomic। এমন কোনো মধ্যবর্তী অবস্থা নেই যেখানে CPL 0 কিন্তু stack এখনো user-এর।

Meltdown — যখন দেয়ালটা ফাঁকা হয়ে গেল

২০১৮ সালের জানুয়ারিতে প্রকাশিত Meltdown দেখাল যে “kernel page supervisor-only, তাই নিরাপদ” — এই যুক্তিটা speculative execution-এর কারণে ভেঙে পড়ে।

আক্রমণটা তিন ধাপে:

/* ১. একটা kernel address পড়ুন — এটা ব্যর্থ হবেই */
char value = *(char *)kernel_address;

/* ২. কিন্তু ব্যর্থ হওয়ার আগে CPU speculatively এটা চালিয়ে ফেলে */
char dummy = probe_array[value * 4096];

/* ৩. exception হলো, value বাতিল হলো — কিন্তু cache-এ চিহ্ন রয়ে গেল */

মূল অন্তর্দৃষ্টি: CPU out-of-order execution-এ ধাপ ১-এর permission check শেষ হওয়ার আগেই ধাপ ২ চালিয়ে ফেলে। Exception পরে raise হয় আর architectural state (register) পুনরুদ্ধার হয় — কিন্তু cache-এর অবস্থা পুনরুদ্ধার হয় না

তারপর attacker probe_array-এর ২৫৬টা cache line-এর প্রতিটার access time মেপে দেখে কোনটা দ্রুত — সেটাই value

এটা একটা side channel: গোপন তথ্য architectural পথে বের হয়নি, বেরিয়েছে timing-এর মাধ্যমে।

KPTI — প্রতিকার আর তার দাম

সমাধান: Kernel Page Table Isolation। যদি kernel page গুলো user address space-এ ম্যাপই না থাকে, তাহলে speculative execution-ও তাদের ছুঁতে পারবে না।

তাই এখন প্রতিটা process-এর দুইটা page table:

KPTI-র আগে                    KPTI-র পরে

একটা page table:               user mode-এ:        kernel mode-এ:
┌──────────────┐               ┌──────────────┐    ┌──────────────┐
│ kernel (S)   │               │  (খালি)      │    │ kernel (S)   │
├──────────────┤               ├──────────────┤    ├──────────────┤
│ trampoline   │               │ trampoline   │    │ trampoline   │
├──────────────┤               ├──────────────┤    ├──────────────┤
│ user (U)     │               │ user (U)     │    │ user (U)     │
└──────────────┘               └──────────────┘    └──────────────┘

syscall = mode switch         syscall = mode switch + CR3 লেখা + TLB প্রভাব
KPTI-র আগে ও পরে — এখন syscall-এ page table বদলাতেই হয়, যা ঠিক সেই খরচ যা মূল নকশা এড়াতে চেয়েছিল।

একটা ক্ষুদ্র trampoline অংশ দুই table-এই ম্যাপ থাকতে হয় — নাহলে page table বদলানোর কোডটাই unmap হয়ে যেত।

খরচ: প্রতিটা syscall-এ এখন cr3 লিখতে হয়। PCID (Process Context ID) থাকলে সম্পূর্ণ TLB flush এড়ানো যায়, কিন্তু তবু overhead থেকে যায়।

WorkloadKPTI overhead
CPU-bound (syscall কম)~০%
সাধারণ application২–৫%
Syscall-ভারী (database, I/O)৫–৩০%

PostgreSQL আর Redis-এর মতো syscall-ভারী system সবচেয়ে বেশি ক্ষতিগ্রস্ত হয়েছিল।

উদাহরণ

/proc/self/maps — বিভাজনটা নিজের চোখে

একটা সাধারণ প্রোগ্রামের address space:

cat /proc/self/maps
5586c4a00000-5586c4a02000 r--p 00000000 08:02 1835017   /usr/bin/cat
5586c4a02000-5586c4a07000 r-xp 00002000 08:02 1835017   /usr/bin/cat
5586c4a07000-5586c4a0a000 r--p 00007000 08:02 1835017   /usr/bin/cat
5586c4a0a000-5586c4a0b000 r--p 00009000 08:02 1835017   /usr/bin/cat
5586c4a0b000-5586c4a0c000 rw-p 0000a000 08:02 1835017   /usr/bin/cat
5586c5f3f000-5586c5f60000 rw-p 00000000 00:00 0         [heap]
7f2e8c000000-7f2e8c028000 r--p 00000000 08:02 1841234   /usr/lib/libc.so.6
...
7ffd4b7e2000-7ffd4b803000 rw-p 00000000 00:00 0         [stack]
7ffd4b8f1000-7ffd4b8f5000 r--p 00000000 00:00 0         [vvar]
7ffd4b8f5000-7ffd4b8f7000 r-xp 00000000 00:00 0         [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]

তিনটা জিনিস লক্ষ্য করুন:

১. সব address 0x7fff...-এর নিচে — user space-এর সীমা। Kernel-এর কোনো entry নেই, যদিও kernel ম্যাপ করা আছে। Kernel ইচ্ছাকৃতভাবে সেগুলো লুকায় (তথ্য ফাঁস রোধ করতে)।

২. ELF segment গুলো আলাদা permission-এr--p (rodata), r-xp (code), rw-p (data)। Level 3-এর ELF লেসনের section গুলো এখানে runtime segment হয়ে এসেছে। কোনো segment একসাথে w আর x নয় — এটাই W^X নীতি, code injection ঠেকায়।

৩. [vdso] — একটা ছোট রহস্য। এটা kernel-এর দেওয়া একটা shared library যা প্রতিটা process-এ ম্যাপ করা। এতে কিছু function-এর user-mode বাস্তবায়ন আছে (gettimeofday, clock_gettime) যাতে সেগুলোর জন্য syscall করতেই না হয়।

[vsyscall]-টা vDSO-র পুরনো, স্থির-ঠিকানার পূর্বসূরি — নিরাপত্তার কারণে (ROP gadget হিসেবে ব্যবহারযোগ্য ছিল) এখন প্রায় অক্ষম।

Kernel address কেন দেখা যায় না

sudo cat /proc/kallsyms | grep " T sys_open"

Root ছাড়া চালালে সব address 0000000000000000 দেখাবে।

cat /proc/sys/kernel/kptr_restrict

এটা 1 বা 2 হলে kernel pointer লুকানো হয়। কারণ KASLR (Kernel Address Space Layout Randomization) কাজ করে শুধু যদি attacker kernel-এর প্রকৃত address না জানে। একটা ফাঁস হওয়া pointer পুরো randomization বাতিল করে দেয়।

নিজে চালিয়ে দেখুন

EXPERIMENT

দেয়ালটা সত্যিই আছে — একটা privileged instruction চালানোর চেষ্টা

Linux x86-64, gcc· ১৫ মিনিট
#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
#include <string.h>

static sigjmp_buf jump_buffer;
static volatile const char *current_test;

static void handler(int sig, siginfo_t *info, void *ctx) {
    (void)ctx;
    printf("  ✗ %-24s%s (si_code=%d)\n",
           current_test, strsignal(sig), info->si_code);
    siglongjmp(jump_buffer, 1);
}

#define TRY(name, code)                       \
    do {                                      \
        current_test = name;                  \
        if (sigsetjmp(jump_buffer, 1) == 0) { \
            code;                             \
            printf("  ✓ %-24s → সফল!\n", name); \
        }                                     \
    } while (0)

int main(void) {
    struct sigaction sa = {0};
    sa.sa_sigaction = handler;
    sa.sa_flags = SA_SIGINFO;
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGILL,  &sa, NULL);
    sigaction(SIGBUS,  &sa, NULL);

    printf("CPL 3 (user mode) থেকে চেষ্টা করছি:\n\n");

    TRY("সাধারণ যোগ",        { volatile int x = 1 + 1; (void)x; });
    TRY("cli (interrupt off)", { __asm__ volatile("cli"); });
    TRY("hlt (CPU থামানো)",   { __asm__ volatile("hlt"); });
    TRY("cr3 পড়া",            { unsigned long v;
                                 __asm__ volatile("mov %%cr3, %0" : "=r"(v)); });
    TRY("kernel memory পড়া",  { volatile char c =
                                 *(char *)0xffffffff81000000UL; (void)c; });
    TRY("NULL pointer",       { volatile char c = *(char *)0; (void)c; });
    TRY("I/O port পড়া",       { unsigned char v;
                                 __asm__ volatile("inb $0x60, %0" : "=a"(v)); });

    printf("\nএকটাও privileged operation সফল হয়নি — দেয়ালটা কাজ করছে।\n");
    return 0;
}
gcc -O0 -o wall wall.c && ./wall

প্রত্যাশিত output:

CPL 3 (user mode) থেকে চেষ্টা করছি:

  ✓ সাধারণ যোগ              → সফল!
  ✗ cli (interrupt off)     → Segmentation fault (si_code=128)
  ✗ hlt (CPU থামানো)        → Segmentation fault (si_code=128)
  ✗ cr3 পড়া                 → Segmentation fault (si_code=128)
  ✗ kernel memory পড়া       → Segmentation fault (si_code=1)
  ✗ NULL pointer            → Segmentation fault (si_code=1)
  ✗ I/O port পড়া            → Segmentation fault (si_code=128)

একটাও privileged operation সফল হয়নি — দেয়ালটা কাজ করছে।

si_code লক্ষ্য করুন — এটা দুই ধরনের ব্যর্থতা আলাদা করে:

si_codeমানেকোনগুলো
128 (SI_KERNEL)#GP fault — privileged instructioncli, hlt, cr3, inb
1 (SEGV_MAPERR)page fault — অবৈধ addressNULL, kernel memory

দুইটা সম্পূর্ণ ভিন্ন hardware প্রক্রিয়া, একই signal-এ এসে মিলেছে। প্রথমটা instruction decoder ধরেছে, দ্বিতীয়টা MMU।

এটা কী প্রমাণ করে

User mode-এর সীমাবদ্ধতা একটা নীতি নয়, একটা hardware সত্য। একই instruction kernel-এ চলে, user-এ SIGSEGV দেয় — আর সেই পার্থক্যটা CPL-এর দুই bit।

EXPERIMENT

vDSO — যে syscall আসলে syscall নয়

Linux, gcc, strace· ১৫ মিনিট
#define _GNU_SOURCE
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include <sys/syscall.h>

#define N 2000000

static double bench(const char *name, void (*fn)(void)) {
    struct timespec a, b;
    clock_gettime(CLOCK_MONOTONIC, &a);
    for (int i = 0; i < N; i++) fn();
    clock_gettime(CLOCK_MONOTONIC, &b);
    double ns = ((b.tv_sec - a.tv_sec) * 1e9 + (b.tv_nsec - a.tv_nsec)) / N;
    printf("  %-34s %7.1f ns/call\n", name, ns);
    return ns;
}

static void via_vdso(void)  { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); }
static void via_syscall(void){ struct timespec t; syscall(SYS_clock_gettime, CLOCK_MONOTONIC, &t); }
static void real_syscall(void){ syscall(SYS_getpid); }
static void no_syscall(void) { __asm__ volatile("" ::: "memory"); }

int main(void) {
    printf("প্রতিটা %d বার:\n\n", N);
    double none = bench("কিছু না (baseline)", no_syscall);
    double vdso = bench("clock_gettime (vDSO)", via_vdso);
    double sysc = bench("clock_gettime (জোর করে syscall)", via_syscall);
    double gpid = bench("getpid (আসল syscall)", real_syscall);

    printf("\n  mode switch-এর আনুমানিক খরচ: %.0f ns\n", gpid - none);
    printf("  vDSO যা বাঁচায়:              %.1f×\n", sysc / vdso);
    return 0;
}
gcc -O2 -o vdso vdso.c && ./vdso

সাধারণ ফলাফল (KPTI চালু একটা মেশিনে):

প্রতিটা 2000000 বার:

  কিছু না (baseline)                     0.3 ns/call
  clock_gettime (vDSO)                  22.7 ns/call
  clock_gettime (জোর করে syscall)      412.5 ns/call
  getpid (আসল syscall)                 398.1 ns/call

  mode switch-এর আনুমানিক খরচ: 398 ns
  vDSO যা বাঁচায়:              18.2×

এখন প্রমাণ করুন যে vDSO সত্যিই syscall করছে না:

strace -c -e trace=clock_gettime ./vdso

২০ লক্ষ কলের মধ্যে clock_gettime শূন্য বার দেখাবে — কারণ সেগুলো kernel-এ কখনো পৌঁছায়নি।

কীভাবে সম্ভব: kernel একটা shared page-এ ([vvar]) বর্তমান সময় লিখে রাখে আর প্রতি tick-এ আপডেট করে। vDSO-র কোড সেই page থেকে পড়ে — একটা সাধারণ memory read, কোনো mode switch নেই।

# vDSO-র কোড dump করে দেখুন
cp /proc/self/maps /tmp/m
grep vdso /tmp/m
এটা কী প্রমাণ করে

Mode switch-এর খরচ বাস্তব ও পরিমাপযোগ্য। vDSO সেই খরচ এড়াতে kernel-এর কোড user space-এ ম্যাপ করে — আর পার্থক্যটা ১০ গুণেরও বেশি।

নিজে বানান

BUILD IT

Address space explorer — নিজের প্রোগ্রামের মানচিত্র

C · ●●●○○
  1. বিভিন্ন ধরনের object-এর address ছাপুন — global, static, heap, stack, code, libc
  2. /proc/self/maps পড়ে প্রতিটা address কোন region-এ পড়ে খুঁজে বের করুন
  3. প্রতিটা region-এর permission ছাপুন আর W^X নীতি যাচাই করুন
  4. ASLR-এর প্রভাব দেখতে প্রোগ্রামটা কয়েকবার চালান
  5. user/kernel সীমানা কোথায় তা হিসাব করে দেখান
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static int   global_initialised = 42;
static int   global_bss;
static const char rodata_string[] = "read-only";

typedef struct {
    unsigned long start, end;
    char perms[5];
    char path[256];
} region_t;

static region_t regions[512];
static int nregions;

static void load_maps(void) {
    FILE *f = fopen("/proc/self/maps", "r");
    if (!f) { perror("maps"); exit(1); }
    char line[512];
    while (nregions < 512 && fgets(line, sizeof line, f)) {
        region_t *r = &regions[nregions];
        r->path[0] = '\0';
        if (sscanf(line, "%lx-%lx %4s %*s %*s %*s %255[^\n]",
                   &r->start, &r->end, r->perms, r->path) >= 3)
            nregions++;
    }
    fclose(f);
}

static const region_t *find(unsigned long addr) {
    for (int i = 0; i < nregions; i++)
        if (addr >= regions[i].start && addr < regions[i].end)
            return &regions[i];
    return NULL;
}

static void show(const char *label, const void *p) {
    unsigned long a = (unsigned long)p;
    const region_t *r = find(a);
    printf("  %-18s %016lx  %-5s %s\n", label, a,
           r ? r->perms : "?????",
           r ? (r->path[0] ? r->path : "[anonymous]") : "(ম্যাপ করা নেই)");
}

int main(void) {
    load_maps();

    void *heap  = malloc(1024);
    int   local = 7;

    printf("\n═══ এই প্রোগ্রামের address space ═══\n\n");
    show("code (main)",     (void *)main);
    show("rodata",          rodata_string);
    show("data (init)",     &global_initialised);
    show("bss (uninit)",    &global_bss);
    show("heap (malloc)",   heap);
    show("stack (local)",   &local);
    show("libc (printf)",   (void *)printf);
    show("vdso",            (void *)load_maps);   /* কাছাকাছি দেখতে */

    printf("\n═══ সীমানার হিসাব ═══\n\n");
    unsigned long user_max = 0x00007FFFFFFFFFFFUL;
    printf("  user space সর্বোচ্চ    : %016lx  (%.0f TB)\n",
           user_max, (double)user_max / (1UL << 40));
    printf("  kernel space শুরু      : %016lx\n", 0xFFFF800000000000UL);
    printf("  stack এই সীমার নিচে?   : %s\n",
           ((unsigned long)&local < user_max) ? "হ্যাঁ" : "না");

    printf("\n═══ W^X যাচাই ═══\n\n");
    int violations = 0;
    for (int i = 0; i < nregions; i++)
        if (strchr(regions[i].perms, 'w') && strchr(regions[i].perms, 'x')) {
            printf("  ⚠ w+x দুটোই: %016lx-%016lx %s %s\n",
                   regions[i].start, regions[i].end,
                   regions[i].perms, regions[i].path);
            violations++;
        }
    if (!violations)
        printf("  কোনো region একসাথে writable আর executable নয় — W^X মানা হচ্ছে\n");

    printf("\n  মোট region: %d\n\n", nregions);
    free(heap);
    return 0;
}
gcc -O0 -o explore explore.c && ./explore

ASLR দেখতে দুইবার চালান:

./explore | grep stack
./explore | grep stack

Address প্রতিবার আলাদা হবে। এবার ASLR বন্ধ করে দেখুন:

setarch $(uname -m) -R ./explore | grep stack
setarch $(uname -m) -R ./explore | grep stack

এখন একই। -R মানে “randomization বন্ধ” — debugging-এ কাজে লাগে, কিন্তু production-এ কখনো নয়।

নিজে বাড়ান:

  1. /proc/self/smaps পড়ে প্রতিটা region-এর প্রকৃত RSS ছাপুন, আর দেখুন VSZ আর RSS-এর ফারাক কত (lazy allocation — লেসন ১০-এ ফিরবে)
  2. mmap দিয়ে একটা নতুন region বানান বিভিন্ন permission-এ, আর maps-এ সেটা খুঁজে বের করুন
  3. একটা PROT_READ|PROT_WRITE page বানিয়ে তাতে machine code লিখুন, তারপর mprotect দিয়ে PROT_EXEC করে চালান — এটাই JIT compiler যা করে (Level 5-এ ফিরবে)
  4. PROT_WRITE|PROT_EXEC একসাথে চাওয়ার চেষ্টা করুন — কিছু hardened kernel-এ (আর OpenBSD-তে সবসময়) এটা ব্যর্থ হবে
  5. getauxval(AT_SYSINFO_EHDR) দিয়ে vDSO-র base address বের করুন আর তার ELF header parse করে exported symbol গুলো ছাপুন

বাস্তব সিস্টেমে

এই দেয়ালটা যেখানে যেখানে কাজ করছে

প্রতিটা browser tab। Chrome-এর site isolation প্রতিটা origin-কে আলাদা process-এ রাখে, ঠিক এই hardware boundary-র উপর ভরসা করে। একটা ক্ষতিকর সাইট যদি renderer process দখলও করে ফেলে, সে অন্য tab-এর memory পড়তে পারে না। Spectre-এর পর এটা বাধ্যতামূলক হয়েছে।

Cloud multi-tenancy। AWS-এ আপনার VM আর অন্য কারো VM একই physical CPU-তে চলতে পারে। পুরো ব্যবসায়িক মডেলটা এই isolation-এর উপর দাঁড়ানো — আর সেজন্যই Meltdown/Spectre cloud industry-তে আতঙ্ক তৈরি করেছিল। AWS ২০১৮-তে জরুরি ভিত্তিতে পুরো fleet reboot করেছিল।

Container-এর দুর্বলতা। Container একই kernel ভাগ করে (Level 12-এ বিস্তারিত)। তাই container escape মানে শুধু একটা kernel bug — VM escape-এর চেয়ে অনেক সহজ। এই কারণেই gVisor (userspace kernel) আর Firecracker (micro-VM) তৈরি হয়েছে: আরেকটা hardware boundary যোগ করতে।

WebAssembly-র ভিন্ন পথ। WASM hardware ring ব্যবহার করে না — এটা একটা software sandbox যা bytecode verify করে আর memory access bounds-check করে। ধীর, কিন্তু হাজারো sandbox এক process-এ চালানো যায়। Cloudflare Workers এই মডেলে চলে — এই ওয়েবসাইটটা যেখানে host করা।

eBPF — নিয়ন্ত্রিত ব্যতিক্রম। eBPF user-এর লেখা কোড kernel-এ চালায়। কীভাবে নিরাপদ? একটা verifier প্রতিটা program load করার আগে প্রমাণ করে যে এটা থামবে (কোনো unbounded loop নেই) আর শুধু অনুমোদিত memory ছোঁবে। অর্থাৎ hardware boundary-র বদলে formal verification — Level 13-এর ধারণা, production-এ প্রয়োগ। লেসন ৩০-এ ফিরব।

Microkernel-এর দাবি। seL4-এ driver ring 3-এ চলে, তাই একটা buggy driver পুরো system নামাতে পারে না। দাম: প্রতিটা driver call একটা IPC, তাই বেশি context switch। QNX (গাড়িতে) আর seL4 (সামরিক ড্রোনে) সেই দাম দিতে রাজি, কারণ সেখানে crash-এর খরচ অনেক বেশি।

যে ভুলগুলো সবাই করে

“Root বা administrator মানে ring 0-তে চলা।”

সম্পূর্ণ আলাদা দুইটা ধারণা, আর গুলিয়ে ফেলাটা খুব সাধারণ।

Ring 0/3 একটা hardware বৈশিষ্ট্য — CPU-র ভেতরে দুই bit। root/uid 0 একটা software ধারণা — kernel-এর নিজের রাখা একটা সংখ্যা, যা দিয়ে সে permission check করে।

sudo চালানো প্রোগ্রামও ring 3-এ চলে। তার uid 0, তাই kernel তাকে বেশি জিনিস করতে দেয় — কিন্তু সে এখনো cli চালাতে পারে না, এখনো kernel memory সরাসরি পড়তে পারে না।

sudo ./wall     # আগের experiment-টা — একই ভাবে সব ব্যর্থ হবে

Root থেকে ring 0-তে যাওয়ার পথ আছে (kernel module load করা, /dev/mem লেখা) — কিন্তু সেগুলো আলাদা, স্পষ্ট পদক্ষেপ।

আর এই কারণেই আধুনিক Linux-এ root-কেও ভাগ করা হয়েছে capability-তে (CAP_SYS_ADMIN, CAP_NET_RAW…) — যাতে একটা প্রোগ্রামকে “সব ক্ষমতা” না দিয়ে শুধু যেটুকু দরকার সেটুকু দেওয়া যায়।

“Kernel অন্য address space-এ থাকে, তাই user program সেটা দেখতেই পায় না।”

KPTI-র আগে kernel প্রতিটা process-এর page table-এ ম্যাপ করা থাকত। “দেখতে পায় না” এসেছিল page table-এর U/S bit থেকে, আলাদা address space থেকে নয়।

আর সেই পার্থক্যটাই Meltdown-কে সম্ভব করেছিল — speculative execution U/S bit-এর check শেষ হওয়ার আগেই ডেটা cache-এ টেনে আনত।

KPTI-র পরে দাবিটা আংশিকভাবে সত্য হয়েছে: user mode-এ থাকাকালীন kernel সত্যিই ম্যাপ করা থাকে না (একটা ছোট trampoline ছাড়া)।

কিন্তু নতুন CPU-তে hardware fix থাকায় KPTI বন্ধ থাকে, আর তখন আবার পুরনো নকশাই চলে। তাই উত্তরটা “নির্ভর করে” — আর নিজের মেশিনে যাচাই করা যায়:

cat /sys/devices/system/cpu/vulnerabilities/meltdown
dmesg | grep -i "page table isolation"

“Mode switch আর context switch একই জিনিস।”

দুইটা সম্পূর্ণ ভিন্ন খরচের operation, আর গুলিয়ে ফেললে performance নিয়ে ভুল যুক্তি হয়।

Mode switchContext switch
কী বদলায়শুধু privilege levelপুরো process
Page tableঅপরিবর্তিত (KPTI ছাড়া)বদলায় (cr3)
TLBঅক্ষতflush বা PCID
Cacheঅক্ষতসাধারণত ঠান্ডা হয়ে যায়
খরচ~১০০–৫০০ ns~১–১০ μs

একটা syscall মানে দুইটা mode switch (ঢোকা + বেরোনো), কিন্তু কোনো context switch নয় — একই process ফিরে আসে।

Context switch হয় যখন scheduler অন্য process বেছে নেয় — যা syscall-এর সময় ঘটতে পারে (যদি syscall block করে), কিন্তু ঘটতেই হবে এমন নয়।

getpid() একটা syscall কিন্তু কোনো context switch নয়। read() একটা ঠান্ডা ফাইলে দুটোই ঘটাবে।

লেসন ৭-এ আমরা context switch-এর প্রকৃত খরচ মাপব।

“Meltdown আর Spectre একই দুর্বলতা।”

সম্পর্কিত, কিন্তু ভিন্ন — আর প্রতিকারও ভিন্ন।

Meltdown privilege check bypass করে: user code সরাসরি kernel memory পড়ার চেষ্টা করে, আর speculation-এর কারণে সফল হয়। এটা মূলত Intel-এর একটা বাস্তবায়ন ত্রুটি, আর KPTI দিয়ে সম্পূর্ণ প্রতিকারযোগ্য। নতুন CPU-তে hardware-এই ঠিক করা হয়েছে।

Spectre branch predictor-কে বিভ্রান্ত করে একই privilege level-এর ভেতরে। ভুক্তভোগী প্রোগ্রামকে দিয়ে তার নিজের ডেটা ফাঁস করানো হয়। এটা speculative execution-এর মৌলিক ধারণার পরিণতি, তাই প্রায় সব out-of-order CPU আক্রান্ত — Intel, AMD, ARM।

আর Spectre-এর কোনো সম্পূর্ণ প্রতিকার নেই। যা আছে তা আংশিক: retpoline, lfence barrier, IBRS/IBPB, আর compiler-এর সতর্কতা। প্রতিটার performance খরচ আছে।

grep . /sys/devices/system/cpu/vulnerabilities/*

এই একটা কমান্ড আপনার CPU-র সব পরিচিত speculative দুর্বলতা আর তাদের বর্তমান প্রশমন দেখাবে — সাধারণত ১০-১৫টা এন্ট্রি।

Level 11-এ speculative execution, আর Level 10-এ side channel বিস্তারিত দেখব।

বুঝেছেন কি না দেখুন

1

কেন একটা OS শুধু software দিয়ে isolation প্রয়োগ করতে পারে না? যুক্তিটা ধাপে ধাপে গঠন করুন।

যুক্তি

যুক্তিটা একটা regress-এ গিয়ে ভেঙে পড়ে।

ধরুন OS প্রতিটা memory access-এর আগে একটা check বসাতে চায়:

if (address_belongs_to_me(addr)) *addr = value;

সমস্যা ১ — কে check চালাবে? যদি user program নিজের কোডে check রাখে, সে সেটা সরিয়ে দিতে পারে। Compiler বসালেও — attacker নিজের assembler ব্যবহার করবে।

সমস্যা ২ — OS যদি প্রতিটা instruction যাচাই করে? তাহলে প্রতিটা instruction interpret করতে হবে, অর্থাৎ প্রোগ্রাম ১০০ গুণ ধীর হবে। আর সেই interpreter-এর কোড নিজেই কে রক্ষা করবে?

সমস্যা ৩ — OS যদি কোড আগে scan করে? এটাই static verification, আর সাধারণ ক্ষেত্রে এটা অসিদ্ধান্তযোগ্য — “এই প্রোগ্রাম কি কখনো এই address ছোঁবে?” প্রশ্নটা halting problem-এ কমিয়ে আনা যায় (Level 13)।

সমাধান তাই বাইরে থেকে আসতে হবে। এমন একটা সত্তা যা কোড-এর অংশ নয়, আর যাকে কোড প্রভাবিত করতে পারে না — hardware

CPU প্রতিটা access-এ CPL আর U/S bit মেলায়। এটা কোনো instruction নয় যা বাদ দেওয়া যায়; এটা silicon-এর একটা comparator যা সবসময় চলছে, শূন্য অতিরিক্ত সময়ে।

তিনটা ব্যতিক্রম যা নিয়মটা স্পষ্ট করে:

পদ্ধতিকীভাবে software-এ isolation পায়দাম
Interpreter (JVM bytecode verifier)প্রতিটা instruction যাচাইগতি
WASMসীমিত ISA + bounds checkসীমিত ক্ষমতা
eBPFverifier প্রমাণ করে program থামবেসীমিত ভাষা (loop প্রায় নিষিদ্ধ)

তিনটাই কাজ করে কারণ তারা ভাষাটাকে সীমিত করেছে যাতে verification সিদ্ধান্তযোগ্য হয়। অর্থাৎ software isolation সম্ভব — কিন্তু শুধু যদি আপনি সাধারণ-উদ্দেশ্য কোড চালানো ছেড়ে দেন।

2

আপনার প্রোগ্রাম SIGSEGV পেল। si_code দেখে আপনি কীভাবে বলবেন এটা একটা privileged instruction ছিল নাকি একটা অবৈধ memory access?

প্রয়োগ

siginfo_t-এর si_code আর si_addr দুটো ভিন্ন গল্প বলে।

si_codeনামকারণsi_addr
1SEGV_MAPERRaddress ম্যাপ করা নেইসেই address
2SEGV_ACCERRম্যাপ আছে কিন্তু permission নেইসেই address
128SI_KERNEL#GP fault — privileged instructionসাধারণত 0
static void handler(int sig, siginfo_t *info, void *ctx) {
    switch (info->si_code) {
    case SEGV_MAPERR:
        printf("অবৈধ address: %p (ম্যাপ করা নেই)\n", info->si_addr);
        break;
    case SEGV_ACCERR:
        printf("permission নেই: %p (হয়তো W^X বা read-only)\n", info->si_addr);
        break;
    case SI_KERNEL:
        printf("privileged instruction — #GP fault\n");
        break;
    }
}

তিনটা কারণ আলাদা করার ব্যবহারিক মূল্য:

  • SEGV_MAPERR + si_addr == 0 → NULL pointer dereference, সবচেয়ে সাধারণ bug
  • SEGV_ACCERR একটা r--p region-এ → string literal-এ লেখার চেষ্টা (char *s = "abc"; s[0] = 'x';)
  • SEGV_ACCERR executable region-এ লেখার চেষ্টা → JIT-এ mprotect ভুলে যাওয়া
  • SI_KERNEL → হয় inline assembly-তে ভুল, নয় corrupted function pointer এমন জায়গায় লাফিয়েছে যেখানে random byte privileged instruction হিসেবে decode হয়েছে

আরো তথ্য পেতে তৃতীয় argument ucontext_t কাজে লাগান — সেখানে crash-এর মুহূর্তের সব register আছে:

ucontext_t *uc = ctx;
printf("RIP = %llx\n", (unsigned long long)uc->uc_mcontext.gregs[REG_RIP]);

তারপর addr2line বা objdump দিয়ে সেই RIP কোন লাইন তা বের করা যায় — এটাই crash reporter (Sentry, Crashpad) যা করে।

Level 10-এ আমরা দেখব attacker কীভাবে ইচ্ছাকৃতভাবে এই fault গুলো ঘটিয়ে memory layout সম্পর্কে তথ্য বের করে।

3

আপনি একটা plugin system ডিজাইন করছেন যেখানে তৃতীয় পক্ষের কোড আপনার server-এ চলবে। Isolation-এর কোন কৌশল বেছে নেবেন?

ডিজাইন

প্রশ্নটা আসলে: কতটা isolation বনাম কতটা খরচ, আর attacker কতটা সক্ষম ধরে নিচ্ছি

কৌশলBoundaryStartupOverheadEscape-এর জন্য যা লাগে
একই process, ভাষার sandboxকিছুই না~0~0একটা memory bug
WASMsoftware verifier~1 ms10–50%verifier bug
আলাদা processhardware ring~1 msIPC খরচkernel bug
+ seccomp + namespace+ syscall filter~2 msসামান্যkernel bug (কম surface)
gVisoruserspace kernel~100 ms20–50%দুইটা bug
microVM (Firecracker)hardware virt~125 ms5–15%hypervisor bug
আলাদা মেশিনভৌতনেটওয়ার্ককিছুই না

সিদ্ধান্তের প্রশ্নগুলো:

১. কোড কি বিশ্বাসযোগ্য? আপনার নিজের team-এর plugin আর ইন্টারনেট থেকে আসা কোড — সম্পূর্ণ ভিন্ন হুমকি।

২. Plugin কতক্ষণ চলে? ১০ ms-এর একটা transform-এর জন্য ১২৫ ms VM boot অযৌক্তিক। ঘণ্টাব্যাপী job-এর জন্য নগণ্য।

৩. কতগুলো একসাথে? হাজারো concurrent tenant হলে VM-এর memory overhead অসম্ভব হয়ে যায় — তখন WASM-ই একমাত্র বিকল্প।

৪. Plugin-এর কী দরকার? শুধু গণনা, নাকি নেটওয়ার্ক আর filesystem?

আমার সুপারিশ, ক্ষেত্রভেদে:

অবিশ্বস্ত, স্বল্পস্থায়ী, উচ্চ-ঘনত্ব (edge function, user script) → WASM। Cloudflare Workers ঠিক এই কারণে V8 isolate + WASM বেছে নিয়েছে — হাজারো tenant এক process-এ, মাইক্রোসেকেন্ডে startup।

অবিশ্বস্ত, দীর্ঘস্থায়ী, সাধারণ কোড (CI runner, user-submitted container) → microVM। AWS Lambda আর Fargate Firecracker ব্যবহার করে ঠিক এই কারণে।

মাঝারি বিশ্বাস, native গতি দরকারআলাদা process + seccomp + namespace, আর সাথে RLIMIT দিয়ে resource cap।

যা করবেন না: অবিশ্বস্ত native কোড নিজের process-এ dlopen করা। কোনো boundary নেই — একটা bug মানে সম্পূর্ণ compromise।

একটা স্তর সবসময় যোগ করুন: যেকোনো কৌশলের সাথে timeout, memory limit (cgroup), আর syscall filter (seccomp) রাখুন। Defence in depth — একটা boundary ভাঙলেও পরেরটা থাকে।

Level 12-এ আমরা এই প্রতিটা কৌশল হাতে-কলমে বানাব।

4

KPTI syscall-কে ধীর করে। তাহলে কেন এটা চালু রাখা হয়, আর কখন বন্ধ করা যুক্তিসঙ্গত?

যুক্তি

কেন চালু: Meltdown একটা সাধারণ unprivileged প্রোগ্রামকে পুরো physical memory পড়তে দেয় — password, key, অন্য VM-এর ডেটা। ৫–৩০% performance-এর বিনিময়ে সেটা ঠেকানো প্রায় সবসময় সঠিক বিনিময়।

কখন বন্ধ করা যুক্তিসঙ্গত — তিনটা ক্ষেত্র:

১. CPU আক্রান্তই নয়। সব AMD, আর Intel Ice Lake ও পরবর্তী প্রজন্মে hardware-এ ঠিক করা। Kernel নিজেই তখন KPTI বন্ধ রাখে — আপনাকে কিছু করতে হয় না।

cat /sys/devices/system/cpu/vulnerabilities/meltdown
# "Not affected" হলে KPTI এমনিতেই বন্ধ

২. একক-উদ্দেশ্য, বিশ্বাসযোগ্য workload। একটা HPC cluster node যেখানে শুধু আপনার নিজের গণনা চলে, কোনো untrusted কোড নেই, কোনো নেটওয়ার্ক-মুখী সেবা নেই — সেখানে হুমকি মডেলটাই আলাদা।

৩. Embedded / appliance যেখানে arbitrary কোড চালানোর কোনো পথই নেই।

কখন কখনোই বন্ধ করবেন না:

  • Multi-tenant cloud host
  • যেকোনো মেশিন যা untrusted কোড চালায় (CI, plugin, user script)
  • Browser চালানো desktop — একটা ক্ষতিকর ওয়েবপেজই যথেষ্ট (JavaScript থেকে Meltdown প্রদর্শিত হয়েছে)

কীভাবে বন্ধ করবেন (জেনেশুনে):

# kernel command line-এ
nopti
# অথবা সব speculative প্রশমন বন্ধ — খুবই বিপজ্জনক
mitigations=off

আসল পরামর্শ: বন্ধ করার আগে মাপুন। অনেকে ধরে নেয় overhead বেশি, কিন্তু তাদের workload আসলে CPU-bound যেখানে প্রভাব প্রায় শূন্য।

# আপনার নিজের workload-এ syscall হার দেখুন
perf stat -e raw_syscalls:sys_enter ./your_app

সেকেন্ডে কয়েক হাজার syscall হলে KPTI-র প্রভাব নগণ্য। কয়েক লক্ষ হলে তখন কথা বলার মতো — আর তখন প্রথম প্রশ্ন হওয়া উচিত “syscall কমানো যায় কি?” (batching, io_uring — লেসন ২১), “নিরাপত্তা বন্ধ করা যায় কি?” নয়।

5

একটা প্রোগ্রাম mmap দিয়ে memory নিয়ে সেখানে machine code লিখে চালাতে চায় (JIT)। কোন permission লাগবে, আর কেন এক ধাপে PROT_WRITE|PROT_EXEC চাওয়া খারাপ অভ্যাস?

প্রয়োগ

সঠিক ক্রম — দুই ধাপে:

/* ১. লেখার জন্য */
void *mem = mmap(NULL, size, PROT_READ | PROT_WRITE,
                 MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memcpy(mem, machine_code, size);

/* ২. চালানোর জন্য — write সরিয়ে exec দিন */
mprotect(mem, size, PROT_READ | PROT_EXEC);

/* ৩. এখন চালান */
int (*fn)(void) = mem;
int result = fn();

কেন PROT_WRITE|PROT_EXEC একসাথে খারাপ:

এটা W^X (write xor execute) নীতি লঙ্ঘন করে। যদি কোনো page একই সাথে লেখা যায় আর চালানো যায়, তাহলে একটা buffer overflow-এর মাধ্যমে attacker সেখানে নিজের shellcode লিখে সরাসরি চালাতে পারে।

W^X থাকলে attacker-কে অনেক কঠিন পথে যেতে হয় — ROP (Return-Oriented Programming), অর্থাৎ ইতিমধ্যে বিদ্যমান কোডের টুকরো জোড়া লাগানো। সেটা সম্ভব, কিন্তু অনেক কঠিন আর ASLR-এর সাথে মিলে আরো কঠিন।

যেসব জায়গায় W^X জোর করে প্রয়োগ করা হয়:

পরিবেশআচরণ
OpenBSDসবসময়, mmap ব্যর্থ হবে
macOS (Apple Silicon)Hardened runtime-এ নিষিদ্ধ; MAP_JIT + pthread_jit_write_protect_np() লাগে
iOSসম্পূর্ণ নিষিদ্ধ (JIT-এর বিশেষ entitlement ছাড়া)
SELinuxexecmem policy দিয়ে ব্লক করা যায়
Linux (default)অনুমোদিত, কিন্তু নিরুৎসাহিত

আধুনিক JIT-এর কৌশল — dual mapping:

একই physical memory দুইবার ম্যাপ করা হয় — একটা RW, আরেকটা RX:

int fd = memfd_create("jit", MFD_CLOEXEC);
ftruncate(fd, size);
void *w = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
void *x = mmap(NULL, size, PROT_READ|PROT_EXEC,  MAP_SHARED, fd, 0);
/* w দিয়ে লিখুন, x দিয়ে চালান — কোনো একটা pointer-এ দুটো ক্ষমতা নেই */

V8, JavaScriptCore আর LuaJIT এই ধরনের কৌশল ব্যবহার করে।

একটা জরুরি বিবরণ যা সবাই ভোলে — instruction cache:

ARM-এ (আর কিছু অন্য architecture-এ) data cache আর instruction cache আলাদা, আর তারা স্বয়ংক্রিয়ভাবে সমন্বিত হয় না। নতুন লেখা কোড চালানোর আগে cache flush করতে হয়:

__builtin___clear_cache((char *)mem, (char *)mem + size);

x86-এ এটা লাগে না (coherent i-cache), তাই x86-এ লেখা JIT ARM-এ port করলে রহস্যময়ভাবে ভাঙে — একটা কুখ্যাত bug।

Level 5-এ আমরা একটা JIT compiler বানাব আর এই সব বিবরণ হাতে-কলমে দেখব। Level 11-এ i-cache আর d-cache-এর গঠন।

এরপর কী

পরের লেসন — System call

এই লেসনে আমরা দেয়ালটা দেখলাম, আর দেখলাম তাতে তিনটা দরজা আছে: syscall, interrupt, exception।

পরের লেসনে আমরা প্রথম দরজাটা খুলে দেখব — system call, যেটা একমাত্র দরজা যা user program স্বেচ্ছায় ব্যবহার করে।

আমরা দেখব syscall instruction ঠিক কী করে, syscall number কোথায় যায়, argument কোন register-এ (আর কেন সেটা C-র calling convention থেকে আলাদা — rcx কোথায় হারিয়ে গেল?), kernel কীভাবে sys_call_table থেকে সঠিক handler খুঁজে পায়, আর ফেরার পথে errno কীভাবে তৈরি হয়।

আর সবচেয়ে গুরুত্বপূর্ণ — আমরা libc সম্পূর্ণ বাদ দিয়ে একটা write করব, শুধু inline assembly দিয়ে। তারপর strace দিয়ে পাশাপাশি রেখে দেখব libc ঠিক কী কী বাড়তি কাজ করে।

আজকের experiment-এ আমরা মেপেছি একটা syscall-এর খরচ প্রায় ৪০০ ন্যানোসেকেন্ড। পরের লেসনে দেখব সেই ৪০০ ন্যানোসেকেন্ড ঠিক কোথায় খরচ হয়।

আরও পড়ুন