সরাসরি পর্বে যান
পর্ব ৪৭ · ৮০-এর মধ্যে · মডিউল ৭

Memory safety — overflow-এর ধারণা, mitigation আর নিরাপদ ভাষা

Memory Safety — Overflow Concepts, Mitigations & Safe Languages

Taught in Bangla, this lesson explains memory safety as limits of space and time, using simulations on a toy memory strip. It covers the main bug families, how to read published figures carefully, what mitigations such as canaries, ASLR, NX, CFI and shadow stacks do and do not stop, ASan, UBSan, Valgrind and fuzzing, safe C and C++ habits, and migrating towards memory-safe languages.

এই পর্বে যা শিখবেন

  • Spatial আর temporal memory ভুল চেনা, আর ছয়টি প্রধান বাগের ফল বলতে পারা
  • প্রকাশিত সংখ্যা ঠিকভাবে পড়া: কে, কী গুনেছে, আর কোথায় সেটি খাটে না
  • প্রতিটি mitigation কিসের খরচ বাড়ায়, আর কী থামায় না
  • নিজের কোডে flag, ASan, UBSan, Valgrind আর fuzzing ব্যবহারের জায়গা
  • Rust, Go, Java আর Python-এর সুরক্ষা ও দাম তুলনা করা
  • পুরোনো C বা C++ কোডের জন্য বাস্তব migration পরিকল্পনা বানানো

১ · Memory safety কী — ব্যাংকের লকার আর দুটি নিয়ম

ব্যাংকের লকার-কক্ষের কথা ভাবুন। লম্বা সারিতে ছোট ছোট লকার, প্রতিটি একজন গ্রাহকের। এখানে দুটি নিয়ম। প্রথম, আপনি শুধু নিজের লকার খুলবেন, পাশেরটি নয়। দ্বিতীয়, লকারের মেয়াদ শেষে চাবি জমা দেবেন। পরে সেই লকার অন্য গ্রাহক পেলে পুরোনো চাবি আর চলবে না।

কম্পিউটারের memory-ও পাশাপাশি রাখা ঘরের সারি। প্রোগ্রামের একটি অংশ যখন একটি pointer দিয়ে কোনো ঘরে হাত দেয়, তখন একই দুটি প্রশ্ন ওঠে। ঘরটি কি তার সীমার ভেতরে? ঘরটি কি এখনো তার? Memory safety মানে প্রোগ্রাম এই দুই প্রশ্নের উত্তর সবসময় ঠিক রাখে। প্রথম নিয়মকে বলে spatial safety, অর্থাৎ জায়গার নিরাপত্তা। দ্বিতীয়টি temporal safety, অর্থাৎ সময়ের নিরাপত্তা।

Python-এ ব্যাংকের একজন কর্মী প্রতিবার সঙ্গে গিয়ে চাবি মিলিয়ে দেখেন। C-তে কর্মী নেই। কক্ষে ঢোকার পর আপনি কোন লকারে হাত দিচ্ছেন, তা কেউ দেখে না। C এই স্বাধীনতার বিনিময়ে গতি আর নিয়ন্ত্রণ দেয়। আর চুক্তি মানার পুরো দায় থাকে লেখকের ওপর।

Memory-র দুই সীমা: জায়গা আর সময়

Spatial and temporal safety, shown on a toy memory strip

Spatial আর temporal memory ভুল জায়গার ভুল (spatial) cart[0] cart[1] cart[2] cart[3] price সীমা পেরিয়ে পড়া বা লেখা সময়ের ভুল (temporal) বরাদ্দ order-A free ঘর খালি একই ঘরে session-B পুরোনো pointer, নতুন মালিকের ডেটা রক্ষা: সীমা যাচাই, ownership, নিরাপদ ভাষা
ছবি ৪৭.১ — ওপরে পাশের ঘরে ঢুকে পড়া, নিচে মেয়াদ শেষের পরও পুরোনো চাবি ব্যবহার। ছবিটি সরল, আসল memory-র বিন্যাস এমন গোছানো নাও হতে পারে।

সংজ্ঞা নিয়ে একটু মতভেদ আছে। কেউ memory safety বলতে শুধু এই দুই সীমা বোঝান। কেউ এর সঙ্গে uninitialized memory পড়া, ভুল type হিসেবে memory পড়া, এমনকি কিছু data race-ও ধরেন। প্রকাশিত হিসাব পড়ার সময় তাই দেখে নেবেন, কে কোন সংজ্ঞায় গুনেছেন।

C-তে এসব ভুলের ফল undefined behavior: ভাষার নিয়ম কোনো ফলের কথাই বলে না। Stack আর heap-এর বিন্যাস, UB-র খুঁটিনাটি আর integer overflow আমরা আবার করব না, এগুলো আছে C কোর্সের Undefined behavior আর নিরাপদ কোড পর্বে। এই পর্বের প্রশ্ন আলাদা: একজন রক্ষক এই ঝুঁকি কীভাবে মাপবেন, কমাবেন আর ধীরে ধীরে সরাবেন।

সীমা স্পষ্ট: এই পর্ব ধারণা আর প্রতিরক্ষার। এখানে কোনো exploit বানানোর কৌশল নেই, থাকবেও না। সব কোড পাতার ভেতরের খেলনা তালিকায় চলে। অন্যের software বা সিস্টেমে memory-র ত্রুটি খোঁজা বা পরীক্ষা করতে লিখিত অনুমতি লাগে, অনুমতি ছাড়া ঢোকা অপরাধ। বিস্তারিত পর্ব ৬-এ। এটি আইনি পরামর্শ নয়।

২ · বাগের পরিবার — ছয়টি ভুল আর সীমা যাচাই

প্রথমে পুরো পরিবারটি এক টেবিলে দেখে নিন। প্রতিটি সারিতে আছে ভুলটি কী, সাধারণত কী ক্ষতি হয়, আর কোন জিনিস একে সবচেয়ে ভালো ঠেকায়।

ভুলকী ঘটেসাধারণ ক্ষতিমূল প্রতিরোধ
Buffer overflow
(জায়গা)
Buffer-এর শেষের পরে লেখা হয়পাশের ডেটা নষ্ট, crash, প্রোগ্রামের নিয়ন্ত্রণ হারানোর ঝুঁকিলেখার আগে দৈর্ঘ্য যাচাই, সীমা-জানা API
Out-of-bounds read
(জায়গা)
Buffer-এর বাইরে থেকে পড়া হয়গোপন তথ্য ফাঁস, প্রায়ই কোনো crash ছাড়াইঅন্য পক্ষের দাবি করা দৈর্ঘ্য যাচাই
Use-after-free
(সময়)
Free হওয়া memory আবার ব্যবহারঅন্যের ডেটা পড়া বা নষ্ট করা, নিয়ন্ত্রণ হারানোর ঝুঁকিস্পষ্ট ownership, smart pointer, নিরাপদ ভাষা
Double free
(সময়)
একই memory দুবার freeAllocator-এর হিসাব নষ্ট, পরে অদ্ভুত ভুলএকজন মালিক, hardened allocator
Null dereference
(অবৈধ pointer)
কোনো কিছুর দিকে না তাকানো pointer ব্যবহারসাধারণত crash, মানে availability-র ক্ষতিNull যাচাই, Option ধরনের type
Uninitialized read
(প্রাথমিক মান নেই)
মান বসানোর আগেই memory পড়াআগের ডেটার অবশিষ্টাংশ ফাঁস, অনিশ্চিত আচরণশুরুতেই মান বসানো, compiler warning

টেবিলের ক্ষতি মানে সম্ভাব্য ক্ষতি। প্রতিটি বাগ exploit করা যায় না, আর সব বাগ সমান গুরুতর নয়। তবে রক্ষক হিসেবে “এটি কাজে লাগানো যায় না” ধরে নেওয়া বিপজ্জনক। তাই আগে ঠিক করুন, পরে তর্ক করুন।

এবার সবচেয়ে সরল প্রতিরোধটি দেখি: bounds check, অর্থাৎ প্রতিটি প্রবেশের আগে সীমা যাচাই। খেলনা memory-তে cart-এর চারটি ঘর (mem[0] থেকে mem[3]), আর ঠিক পাশের ঘরে price রাখা আছে।

1const SIZE = 4;  // mem[4] = price2function write(i, v) {3  if (i < 0 || i >= SIZE) return "REFUSED";4  mem[i] = v;5  return "ok";6}
খেলনা memory-র অবস্থা
i
যাচাই
cart
price
  1. শুরু। cart-এর চার ঘর 0, পাশের ঘরে price = 120. SIZE = 4 মানে বৈধ সূচক 0 থেকে 3 পর্যন্ত।
  2. write(1, 5) এলো। লাইন ৩ দেখে 1 সীমার ভেতরে, তাই কাজ এগোয়।
  3. লেখা হলো। cart[1] = 5. ফেরত মান ok, price অপরিবর্তিত।
  4. write(4, 9) এলো। এবার 4 ≥ 4, অর্থাৎ সীমার বাইরে। লাইন ৩ শর্ত ধরে ফেলল।
  5. লেখা বাতিল। ফেরত মান REFUSED, memory অপরিবর্তিত, price এখনো 120. কলকারী ভুলটি জানতে পারল।
  6. যাচাই বাদ দিলে। লাইন ৩ না থাকলে একই write(4, 9) সরাসরি লাইন ৪-এ যায়। ফেরত মান ok, অথচ price হয়ে গেল 9. কেউ কিছু জানল না, এটাই সবচেয়ে খারাপ দিক।

একই ঘটনাটি নিজে চালিয়ে দেখুন। এটি একটি simulation: সাধারণ JavaScript array-কে memory ধরে নেওয়া হয়েছে, আসল memory-তে কিছুই ঘটছে না।

// SIMULATION: a plain JS array pretends to be memory. No real memory is touched.
const SIZE = 4;                     // cart has 4 cells: mem[0]..mem[3]
function newMemory() {
  return [0, 0, 0, 0, 120];         // mem[4] is a neighbouring toy variable: price
}
const show = m => `cart=[${m.slice(0, SIZE)}] price=${m[SIZE]}`;

function checkedWrite(m, i, v) {
  if (!Number.isInteger(i) || i < 0 || i >= SIZE) return 'REFUSED';
  m[i] = v;
  return 'ok';
}
function uncheckedWrite(m, i, v) {
  m[i] = v;                         // nobody asks whether i belongs to cart
  return 'ok';
}

const writes = [[1, 5], [3, 2], [4, 9]];   // the last index is outside cart
for (const [label, fn] of [['checked', checkedWrite], ['unchecked', uncheckedWrite]]) {
  const m = newMemory();
  console.log(`--- ${label} build ---`);
  for (const [i, v] of writes) {
    console.log(`write cart[${i}] = ${v} -> ${fn(m, i, v)}`);
  }
  console.log(show(m));
  console.log(`total for 5 items = ${5 * m[SIZE]}`);
}

দুটি build-এ একই তিনটি লেখা হয়েছে। Checked build-এ শেষ সারি বলে write cart[4] = 9 -> REFUSED, আর ফল price=120, total for 5 items = 600. Unchecked build-এ একই লেখায় আসে -> ok, কিন্তু price=9 আর total for 5 items = 45. কোনো error নেই, শুধু বিল ভুল। লক্ষ করুন, cart=[0,5,0,2] দুই build-এ একই, ক্ষতি হয়েছে অন্য ভ্যারিয়েবলে। তাই এমন বাগ সাধারণ test-এ প্রায়ই চোখে পড়ে না।

৩ · সময়ের ভুল — use-after-free ধরার ধারণা

হোটেলের রুম-কার্ডের কথা ভাবুন। অতিথি check-out করলেন, কিন্তু কার্ড পকেটে থেকে গেল। ভালো হোটেলে কার্ডে একটি ক্রমিক সংখ্যা থাকে। নতুন অতিথি এলে দরজা নতুন সংখ্যা মানে, পুরোনো কার্ড আর খোলে না। কার্ড যদি এমন যাচাই ছাড়া কাজ করত, পুরোনো অতিথি নতুন অতিথির ঘরে ঢুকে পড়তেন।

Use-after-free ঠিক তা-ই। Memory free হলে heap-এর সেই জায়গা আবার কাজে লাগে। পুরোনো pointer জানে না যে মালিক বদলে গেছে। নিচে একটি খেলনা tracker প্রতিটি ঘরে “generation” নামের ক্রমিক সংখ্যা রাখে, ঠিক কার্ডের সংখ্যার মতো।

একটি মাত্র ঘর (slot), খেলনা হিসাব
ঘরের ডেটা
ঘরের gen
a-র gen
b-র gen
tracker-এর রায়
  1. বরাদ্দ। a = alloc("order-A"). ঘরের gen 1, তাই হাতল a-তেও লেখা থাকল 1.
  2. পড়া। a-র gen 1 আর ঘরের gen 1 মিলে গেল, ঘরটি বরাদ্দ করা আছে। ফল order-A.
  3. Free. ডেটা মুছে গেল, ঘরের gen বেড়ে হলো 2. a-তে এখনো 1 লেখা, তাই সে পুরোনো হয়ে গেল।
  4. আবার free. a ও ঘরের gen মেলে না, ঘরটিও খালি। Tracker একে double free হিসেবে আটকাল।
  5. একই ঘরে নতুন মালিক। b = alloc("session-B"). ঘরের gen 2, তাই b-তে 2. জায়গাটি আবার ব্যবহার হলো।
  6. পুরোনো হাতল দিয়ে পড়া। যাচাই না থাকলে a দিয়ে session-B-এর ডেটা পাওয়া যেত। এখানে gen 1 আর 2 মেলে না, তাই tracker STALE বলে আটকাল।
// SIMULATION: a toy allocator with generation numbers. Not a real heap.
const slots = [{ gen: 1, data: null, live: false }];   // one slot, so reuse is certain
const log = (s, r) => console.log(s.padEnd(24) + r);

function alloc(data) {
  const s = slots[0];
  s.data = data; s.live = true;
  return { slot: 0, gen: s.gen };          // a handle remembers the generation
}
function check(h) {
  const s = slots[h.slot];
  if (h.gen !== s.gen) return s.live ? 'STALE' : 'FREED';
  return s.live ? 'OK' : 'FREED';
}
function read(h, tracked) {
  if (tracked && check(h) !== 'OK') return `refused (${check(h)})`;
  return `"${slots[h.slot].data}"`;
}
function free(h) {
  if (check(h) !== 'OK') return `refused double free (${check(h)})`;
  const s = slots[h.slot];
  s.live = false; s.data = null; s.gen += 1;  // every old handle is now stale
  return 'freed';
}

const a = alloc('order-A');
log('read a', read(a, true));
log('free a', free(a));
log('read a after free', read(a, true));
log('free a again', free(a));
const b = alloc('session-B');              // same slot is reused
log('read b', read(b, true));
log('read a, no tracker', read(a, false)); // wrong owner's data
log('read a, tracker', read(a, true));

read a after free আর free a again সারিতে tracker বলছে refused (FREED) ও refused double free (FREED). আসল শিক্ষা শেষ দুই সারিতে: read a, no tracker ফেরত দিল "session-B", মানে অন্য মালিকের ডেটা, আর read a, tracker বলল refused (STALE). বাস্তবের Rust-এ compiler এই কাজটি চালানোর আগেই করে, আর ASan চালানোর সময় ধরে। Generational handle নামের কৌশলও কিছু C++ কোড আর game engine-এ ব্যবহৃত হয়। এই ছোট কোড শুধু ধারণার জন্য, এটি কোনো real allocator নয়।

লক্ষ করুন, temporal ভুল ধরতে সীমা যাচাই যথেষ্ট নয়। a যে জায়গা দেখছে, সেটি বৈধ memory-র ভেতরেই। ভুল হলো সময়ে। এজন্যই রক্ষকের কাছে দুটি প্রশ্ন আলাদা: “এই ঘরটি কি সীমার ভেতরে?” আর “এই ঘরের মালিক কি এখনো আমি?”

৪ · কেন এত গুরুত্ব — প্রকাশিত সংখ্যা আর Heartbleed

Memory ভুল শুধু একটি বাগের ধরন নয়, এটি একটি বড় অংশের vulnerability-র উৎস। কিন্তু সংখ্যা পড়তে হয় সাবধানে। নিচের টেবিলে দেখুন, প্রতিটি হিসাব কী গুনেছে।

উৎসকী গোনা হয়েছেপ্রকাশিত ফল
Microsoft MSRC (২০১৯)প্রতি বছর যেসব ত্রুটিতে CVE দেওয়া হয়েছেপ্রায় ৭০% memory safety সমস্যা
Chromium প্রকল্প২০১৫ থেকে ৯১২টি high বা critical security bug, Stable channel-এ প্রভাব ফেলেছে এমনপ্রায় ৭০% memory unsafety সমস্যা
Google, Android (২০২৪)Android-এর vulnerability-র মোট সংখ্যায় ভাগ২০১৯-এ ৭৬%, ২০২৪-এ ২৪%
Google, Android (নভেম্বর ২০২৫)একই ভাগ, ২০২৫-এর প্রায় চূড়ান্ত হিসাবপ্রথমবার ২০%-এর নিচে

দুটি সতর্কতা। প্রথমত, প্রথম দুটি সংখ্যা বড়, পুরোনো C ও C++ কোডের। একটি Python ওয়েবসাইটের ঝুঁকির ছবি এমন নয়, সেখানে injection বা ভুল অনুমতি অনেক বেশি প্রাসঙ্গিক। দ্বিতীয়ত, সংখ্যাগুলো প্রতিষ্ঠানের নিজের প্রকাশ করা হিসাব, মাপার নিয়মও এক নয়। তবু আলাদা প্রতিষ্ঠানের হিসাব একই দিকে দেখায়, তাই ধারণাটি উপেক্ষা করা যায় না।

তাহলে ওয়েব বা ব্যবসায়িক software লেখক কেন ভাববেন? কারণ আপনার কোড একা চলে না। ছবি resize করার library, TLS library, database engine, browser, OS kernel, ফোনের media parser, এসবের অনেকটাই C বা C++-এ লেখা। আপনার attack surface-এর ভেতরে এই কোডও আছে। এজন্য পর্ব ৩২-এর patch ব্যবস্থা এখানে সরাসরি কাজে লাগে।

২০১৪ — Heartbleed. এপ্রিল ২০১৪-তে OpenSSL প্রকল্প CVE-2014-0160 প্রকাশ করে। Advisory-র ভাষায় TLS heartbeat extension-এ একটি bounds check বাদ পড়েছিল, ফলে সংযুক্ত client বা server-এর সামনে memory-র 64 KB পর্যন্ত প্রকাশ পেতে পারত। আক্রান্ত ছিল 1.0.1 শাখা (1.0.1f পর্যন্ত) আর 1.0.2-beta, সমাধান ছিল 1.0.1g. এটি একটি out-of-bounds read: কিছুই নষ্ট হয়নি, কেবল বাড়তি তথ্য বেরিয়ে গেছে। সাড়া হিসেবে শুধু patch যথেষ্ট ছিল না। Key বদল, certificate বাতিল আর password নতুন করার দরকার পড়েছিল, কারণ আগে কী ফাঁস হয়েছে তা বোঝা কঠিন ছিল। TLS-এর দিক থেকে আলোচনা আছে পর্ব ১২-এ।

রক্ষকের শিক্ষা তিনটি। প্রতিরোধ: অন্য পক্ষের দাবি করা দৈর্ঘ্য কখনো বিশ্বাস নয়, আসল ডেটার সঙ্গে মেলানো। Detection: এমন ফাঁস স্বাভাবিক log-এ প্রায় চিহ্ন রাখে না, তাই release-এর আগে খোঁজাই ভালো। ২০১৫ সালে Hanno Böck লেখেন, afl fuzzer দিয়ে ছয় ঘণ্টার মধ্যে ত্রুটিটি আবার পাওয়া গিয়েছিল। সাড়া: secret ফাঁস হয়ে থাকতে পারে ধরে নিয়ে সব বদলানো। C-র দিক থেকে একই ঘটনা আছে C কোর্সের সেই পর্বে।

৫ · Mitigation — বাগ সারায় না, খরচ বাড়ায়

বাড়ির দেয়ালে একটি ফাটল আছে। জানালায় গ্রিল, গেটে দারোয়ান আর CCTV চোরের কাজ কঠিন করে, কিন্তু ফাটলটি জোড়া লাগে না। Mitigation-ও তেমনি। সে বাগটি সরায় না, বাগ থেকে বড় ক্ষতিতে পৌঁছানোর পথ কঠিন করে। পর্ব ৩-এর defense in depth এখানে ঠিক খাটে।

Mitigationকী করেকিসের খরচ বাড়ায়কী থামায় না
Stack canaryফাংশনের জায়গায় একটি গোপন মান রেখে ফেরার আগে মিলিয়ে দেখেStack-এ টানা overflow দিয়ে ফাংশনের হিসাব নষ্ট করাHeap-এর বাগ, out-of-bounds read, canary-র আগের ভ্যারিয়েবল নষ্ট হওয়া
ASLRপ্রতিবার খোলার সময় memory-র ঠিকানা এলোমেলো করেকোন জিনিস কোথায় আছে আগে থেকে জানাঠিকানা ফাঁস হলে সুরক্ষা অনেক কমে; এলোমেলো করার জায়গা কম হলেও দুর্বল
DEP / NXডেটার memory-কে “চালানো যাবে না” চিহ্ন দেয়ডেটা হিসেবে আসা বাইটকে কোড হিসেবে চালানোডেটা নষ্ট বা ফাঁস, প্রোগ্রামের নিজের কোডের ভুল ব্যবহার
CFIপরোক্ষ লাফ বা ডাকের গন্তব্য অনুমোদিত তালিকায় আছে কি না দেখেপ্রোগ্রামের চলার পথ বদলে দেওয়াপথ না বদলে শুধু ডেটা বদলানো; তালিকা বড় হলে সুরক্ষা ঢিলা
Shadow stackফেরার ঠিকানার আলাদা সুরক্ষিত কপি রেখে মিলিয়ে দেখেফাংশন থেকে ফেরার পথ বদলানোসামনের দিকের পরোক্ষ ডাক (এখানে CFI লাগে), ডেটার ক্ষতি
Hardened allocatorHeap-এর হিসাব আলাদা রাখে, যাচাই করে, পুনর্ব্যবহারে দেরি বা এলোমেলো করেHeap-এর হিসাব নষ্ট করা, double free, free-র পরের ঘর আগেভাগে পাওয়াএকই block-এর ভেতরের ভুল, সব temporal ভুল; গতি ও memory-র কিছু খরচ আছে

ঠিক এই কারণেই Heartbleed-এর সময় canary বা NX কিছু করতে পারেনি: সেখানে কোড বা চলার পথ বদলায়নি, শুধু memory পড়া হয়েছিল। Mitigation কাজ করে নির্দিষ্ট পথে, বাগের পুরো পরিবারে নয়।

ছোট হিসাব: ASLR-এর entropy

ASLR-এর শক্তি মাপা হয় entropy-তে, মানে কত bit এলোমেলো। ধরুন একটি memory অঞ্চলে ২৮ bit entropy আছে। তাহলে সম্ভাব্য অবস্থান 228 = 268,435,456টি, আর এক চেষ্টায় মিলে যাওয়ার সম্ভাবনা 1/228 ≈ 3.7 × 10−9. এবার ধরুন একটি পুরোনো ৩২-bit সিস্টেমে মাত্র ৮ bit: অবস্থান 256টি, সম্ভাবনা 1/256 ≈ 0.39%. দুটির ব্যবধান 220 = 1,048,576 গুণ। সংখ্যাগুলো ধরে নেওয়া, আসল মান OS, CPU আর memory অঞ্চল অনুযায়ী আলাদা। রক্ষকের জন্য ব্যবহারিক কথা হলো: একটি সেবা বারবার crash করে নিজে থেকে আবার চালু হতে থাকলে সেটিকে স্বাভাবিক ধরবেন না, এটি তদন্তের সংকেত।

এবার স্তরগুলো একসঙ্গে দেখুন। ধরুন একটি কাল্পনিক অনলাইন বাজারের ছবি-প্রক্রিয়ার C কোডে heap-এ একটি out-of-bounds লেখার ভুল আছে। লক্ষ রাখুন, শেষ ধাপের আগে প্রথম সারির উত্তর একবারও বদলায় না।

Heap-এ out-of-bounds লেখা, কাল্পনিক parser
বাগ কোডে আছে?
স্তর
ফল
  1. বাগ। Parser ফাইলের দাবি করা মাপ না মিলিয়েই heap-এ লেখে। ফল কোনো যাচাই নেই, বাগ আছে।
  2. Test. ASan আর fuzzing এই পথে পৌঁছালে ধরত, কিন্তু ধরুন সেই ইনপুট তৈরি হয়নি। Test শুধু চালানো পথটুকুই দেখে।
  3. Stack canary. ভুলটি stack-এ নয়, heap-এ। তাই canary কিছুই দেখতে পায় না।
  4. Hardened allocator. কিছু ক্ষেত্রে পাশের block-এর ক্ষতি ধরে process থামায়। নীরব ক্ষতির বদলে নিয়ন্ত্রিত crash, তবে সবসময় নয়।
  5. ASLR, NX, CFI. বাগ থেকে প্রোগ্রামের নিয়ন্ত্রণে পৌঁছানো অনেক কঠিন ও অনিশ্চিত হয়। কিন্তু ডেটা নষ্ট হওয়া এরা ঠেকায় না।
  6. Sandbox. Parser আলাদা, কম অনুমতির process-এ চলে, যা least privilege নীতির প্রয়োগ। সবচেয়ে খারাপ হলেও ক্ষতি ওই ছোট ঘরেই আটকে থাকার কথা।
  7. সংশোধন। Crash report থেকে triage, মূল কারণ বের করা, দৈর্ঘ্য যাচাই যোগ করা আর regression test. শুধু এই ধাপেই প্রথম সারিতে “না” আসে।

এবার একটি খেলনা coverage টেবিল চালান। এটি পরিমাপ নয়, শুধু শেখার মডেল: চারটি কাল্পনিক binary আর সরল করে ধরা বিচার।

// Teaching model, not a measurement. Fictional binaries, simplified judgements.
const bugs = ['stack-write', 'heap-write', 'oob-read', 'uaf', 'double-free', 'uninit'];
// P = prevented by design, D = often detected or stopped at run time, C = cost raised
const covers = {
  canary:    { 'stack-write': 'D' },
  fortify:   { 'stack-write': 'D', 'heap-write': 'D', 'oob-read': 'D' },
  aslr_nx:   { 'stack-write': 'C', 'heap-write': 'C', uaf: 'C', 'double-free': 'C' },
  cfi:       { 'stack-write': 'C', 'heap-write': 'C', uaf: 'C' },
  allocator: { 'heap-write': 'C', uaf: 'C', 'double-free': 'D' },
  safe_lang: Object.fromEntries(bugs.map(b => [b, 'P'])),
};
const binaries = {
  'legacy-daemon': [],
  'billing-hardened': ['canary', 'fortify', 'aslr_nx'],
  'gateway-layered': ['canary', 'fortify', 'aslr_nx', 'cfi', 'allocator'],
  'parser-rust': ['safe_lang', 'aslr_nx'],
};
const rank = { '-': 0, C: 1, D: 2, P: 3 };
const best = (list, bug) => list.reduce((b, m) => {
  const v = covers[m][bug] || '-';
  return rank[v] > rank[b] ? v : b;
}, '-');

console.log('binary'.padEnd(18) + bugs.map(b => b.padEnd(12)).join(''));
for (const [name, list] of Object.entries(binaries)) {
  const row = bugs.map(b => best(list, b));
  const open = row.filter(v => v === '-').length;
  console.log(name.padEnd(18) + row.map(v => v.padEnd(12)).join('') + `gaps=${open}`);
}

সাঙ্কেতিক অক্ষর: P নকশাগতভাবে ঠেকায়, D চলার সময় প্রায়ই ধরে, C শুধু খরচ বাড়ায়, - কিছুই নয়। legacy-daemon-এ gaps=6. billing-hardened আর gateway-layered-এ gaps=1, কারণ uninit কোনো mitigation ধরেনি। তবে দেখুন, দুটির uaf ঘর কেবল C, মানে বাগ রয়ে গেছে। parser-rust-এর সারিতে সব P, gaps=0, কিন্তু এটি মডেলের সরলতা: unsafe অংশ আর C library-র সঙ্গে জোড় এখানে গোনা হয়নি (অংশ ৮)। “Gap শূন্য” কখনো “বাগ শূন্য” নয়।

৬ · নিজের কোডে — flag, sanitizer, Valgrind আর fuzzing

দুই ধরনের কাজ আলাদা রাখুন। খোঁজা হয় test-এ, যেখানে ধীর হলেও চলে। শক্ত করা হয় release build-এ, যেখানে খরচ কম হতে হয়। Flag-গুলোর পূর্ণ ব্যাখ্যা আছে C কোর্সের পর্বে, আর sanitizer ও Valgrind-এর হাতে-কলমে ব্যবহার আছে C debugging পর্বে। এখানে শুধু নিরাপত্তার মানচিত্র।

টুলকখনকী ধরে বা করেসীমা
-Wall -WextraCompileসন্দেহজনক কোড, কিছু uninitialized ব্যবহারচালানোর সময়ের ইনপুট জানে না
-fstack-protector-strongReleaseArray বা local ঠিকানা আছে এমন ফাংশনে canaryশুধু stack; ভুল হলে process থামে
-D_FORTIFY_SOURCE=3Releaseকিছু library ডাকে আকার যাচাই (FORTIFY)Optimization লাগে; আকার জানা না থাকলে যাচাই হয় না
AddressSanitizerTestOut-of-bounds, use-after-free, double freeশুধু চালানো পথ; ধীর, Clang-এর docs-এ সাধারণত প্রায় 2 গুণ
UBSanTestInteger overflow, ভুল shift, null ব্যবহারসহ UBশুধু চালানো পথ; memory-র সব ভুল ধরে না
Valgrind MemcheckTestHeap ভুল, memory leak, uninitialized মান ব্যবহারনতুন করে build লাগে না, তবে অনেক ধীর (docs-এ প্রায় ২০ থেকে ৩০ গুণ)
FuzzingTest ও CIঅসংখ্য অদ্ভুত ইনপুট বানিয়ে sanitizer-এর সামনে পথ খুলে দেয়নিজে শুধু খুঁজে দেয়; বিস্তারিত পর্ব ৪৯ · নিরাপত্তা পরীক্ষা
# উদাহরণ: test build, শুধু নিজের কোডে
gcc -g -O1 -Wall -Wextra -fsanitize=address,undefined \
    -fno-omit-frame-pointer app.c -o app_test

# উদাহরণ: release build, শুধু নিজের কোডে
gcc -O2 -Wall -Wextra -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 \
    -fstack-protector-strong -fPIE -pie app.c -o app

# নিজের build সত্যিই hardened কি না দেখা
readelf -h app | grep 'Type:'        # DYN মানে সাধারণত PIE
readelf -lW app | grep GNU_STACK     # শেষে RW, RWE নয়: stack চালানো যায় না
readelf -sW app | grep stack_chk     # canary-র চিহ্ন, থাকলে দেখা যাবে

এই কমান্ডগুলো এই পাতায় চালানো হয়নি, তাই আউটপুট দেখানো হলো না। =3 নতুন GCC ও glibc-তে কাজ করে, পুরোনো toolchain-এ =2 ব্যবহার করুন। ASan release build-এ রাখার মতো hardening নয়, এটি বাগ খোঁজার টুল।

ASan-এর report পড়া শেখা দরকারি। নিচে use-after-free-এর একটি সরল নমুনা। এটি চালানো কোনো প্রোগ্রামের হুবহু আউটপুট নয়, শুধু আকার বোঝানোর জন্য বানানো।

নমুনা আউটপুট — version-ভেদে আলাদা হতে পারে

ERROR: AddressSanitizer: heap-use-after-free
READ of size 4
    #0 in show_order order.c:21
freed by thread T0 here:
    #0 in free
    #1 in close_order order.c:14
previously allocated by thread T0 here:
    #0 in malloc
    #1 in open_order order.c:8

তিনটি অংশ তিনটি প্রশ্নের উত্তর দেয়: কোথায় ভুল ব্যবহার হলো (order.c:21), কোথায় free হয়েছিল (order.c:14), আর কোথায় বরাদ্দ হয়েছিল (order.c:8). সমাধান শুরু হয় একটি প্রশ্ন দিয়ে: এই memory-র মালিক আসলে কে, আর সে কখন ছেড়ে দেবে?

৭ · C আর C++-এ নিরাপদ অভ্যাস

পুরোনো কোড রাতারাতি বদলায় না, তাই C আর C++-এর ভেতরেই ঝুঁকি কমাতে হয়। মূল নীতি একটি: দৈর্ঘ্য আর মালিকানা যেন ডেটার সঙ্গেই চলে, মানুষের মাথায় নয়।

/* উদাহরণ: দৈর্ঘ্য বিশ্বাস করার আগে যাচাই */
int copy_name(char *dst, size_t cap, const char *src, size_t len) {
    if (dst == NULL || src == NULL) return -1;
    if (cap == 0 || len >= cap) return -1;   /* '\0'-এর জন্য এক ঘর লাগে */
    memcpy(dst, src, len);
    dst[len] = '\0';
    return 0;                                /* কলকারী ফল দেখবেন */
}
// উদাহরণ: সীমা আর মালিকানা type-এর ভেতরে
#include <memory>
#include <span>
#include <vector>

struct Order { int id; };

void clear_cart(std::span<int> cart) {    // pointer আর size একসঙ্গে
    for (int &item : cart) item = 0;      // সীমার বাইরে যাওয়ার সূচক নেই
}

int main() {
    std::vector<int> cart(4);
    clear_cart(cart);
    cart.at(2) = 5;                       // ভুল সূচকে exception, নীরব ক্ষতি নয়
    auto order = std::make_unique<Order>();  // একজন মালিক, আপনাআপনি মুক্ত
    order->id = 47;
}
ঝুঁকির অভ্যাসভালো বিকল্পসতর্কতা
gets, strcpy, sprintffgets, snprintf আর ফল যাচাইলেখা কেটে যাওয়াও একটি ফল, সেটি সামলান
Pointer আর দৈর্ঘ্য আলাদা পাঠানোstd::span, std::string_viewspan-এর [] সাধারণত সীমা যাচাই করে না; এগুলো মালিকও নয়
Raw new আর deleteSmart pointer: unique_ptr, প্রয়োজনে shared_ptrকাঁচা pointer বের করে রাখলে temporal ভুল আবার ফেরে
[] দিয়ে যাচাইহীন সূচক.at(), library-র hardened modeMode চালু করতে হয়, নিয়ম library ভেদে আলাদা
strcpy_s-কে সর্বরোগের ওষুধ ভাবাAnnex K ঐচ্ছিক, সব platform-এ নেই; আগে যাচাই করুনPortable কোডে ধরে নেবেন না
Free-র পরে pointer ফেলে রাখাScope ছোট রাখা, একজন মালিক, free-র পর NULLঅন্য কপি থাকলে NULL সবাইকে বাঁচায় না

Smart pointer-এর বিস্তারিত আছে C++ কোর্সের RAII পর্বে। এগুলো ভালো অভ্যাস, তবে নিশ্চয়তা নয়। Vector বড় হলে পুরোনো reference নষ্ট হতে পারে, compiler সবসময় তা ধরে না। আর SEI CERT C-র মতো coding standard আর SAST টুল এই নিয়মগুলো দলে ধরে রাখতে সাহায্য করে। সাধারণ secure coding-এর নিয়ম আছে পর্ব ৪৬-এ।

৮ · Memory-safe ভাষা — চারটি পথ আর তার দাম

Memory-safe language সুরক্ষাকে লেখকের মনোযোগ থেকে সরিয়ে ভাষার নিয়মে নিয়ে আসে। পথ প্রধানত দুটি। একটি garbage collector: কোনো কিছু আর ব্যবহার হচ্ছে না, তা runtime নিজে দেখে memory ফেরত নেয়। অন্যটি Rust-এর পথ: compile-এর সময় মালিকানা যাচাই।

Ownership হলো জমির দলিলের মতো। একটি জমির একজনই মালিক। দলিল হস্তান্তর করলে আগের মালিকের আর অধিকার নেই। ধার নেওয়া (borrow) মানে নির্দিষ্ট সময়ের জন্য ব্যবহার, মালিকানা নয়। Rust-এর borrow checker অফিসের রেজিস্টার খাতার মতো হিসাব রাখে, আর নিয়ম ভাঙা হলে প্রোগ্রাম বানাতেই দেয় না।

// উদাহরণ: এই কোড ইচ্ছা করেই compile হয় না
fn main() {
    let order = String::from("order-A");
    let moved = order;          // মালিকানা moved-এর কাছে গেল
    println!("{order}");        // error[E0382]: borrow of moved value
    println!("{moved}");
}

এখানে কোনো ভুল চালানোর সময় ঘটে না, প্রোগ্রাম তৈরিই হয় না। এটিই compile-time নিশ্চয়তা। তবে চার ভাষার সুরক্ষা আর দাম এক নয়:

ভাষাকীভাবে রক্ষা করেযা রয়ে যায়দাম
RustOwnership, borrow check, চলার সময় সীমা যাচাই; GC নেইunsafe অংশ আর C-র সঙ্গে জোড়; ভুল সূচকে panicশেখা কঠিন, compile সময় বেশি, দলে নতুন দক্ষতা লাগে
GoGC, সীমা যাচাই, nil-এ panicData race-এ memory-র নিশ্চয়তা ভাঙতে পারে; unsafe আর cgoGC-র memory ও সময়ের খরচ
JavaGC, সীমা যাচাই, type যাচাইNullPointerException; JNI-র native কোড; deserialization ঝুঁকিRuntime-এর memory আর শুরু হওয়ার সময়
PythonReference counting আর GC, সীমা যাচাইC extension-এর ভেতরের ভুল; interpreter নিজেই C-তে লেখাগতি কম, কড়া latency-র কাজে কঠিন

Python-এর সুরক্ষা নিজে দেখুন। লক্ষ করবেন, memory নিরাপদ থাকলেও যুক্তির ভুল আপনাকেই ধরতে হয়।

# Python keeps memory safe, but it does not check your logic for you.
cart = [10, 20, 30, 40]
price = 120                       # a separate object, not a neighbouring cell

try:
    cart[4] = 9
except IndexError as e:
    print("write refused:", e)
try:
    print(cart[4])
except IndexError as e:
    print("read refused:", e)
print("cart:", cart, "price:", price)

# A toy record: the sender CLAIMS a length, and the real payload is shorter.
def naive_echo(payload, claimed):
    return payload[:claimed]      # a slice stops at the end, no exception

def safe_echo(payload, claimed):
    if claimed < 0 or claimed > len(payload):
        return None               # discard: the claim does not match the data
    return payload[:claimed]

payload = b"hello"
for claimed in (5, 300):
    print(f"claimed={claimed}:", "naive", naive_echo(payload, claimed),
          "| safe", safe_echo(payload, claimed))
print("negative index:", cart[-1])  # valid in Python, but is it what you meant?

প্রথম দুটি সারিতে Python বলছে list assignment index out of range আর list index out of range, আর cart: [10, 20, 30, 40] price: 120 অপরিবর্তিত। অংশ ২-এর ক্ষতি এখানে সম্ভবই নয়। এবার দেখুন claimed=300: naive b'hello' | safe None. Slice শেষে থেমে গেছে, তাই memory ফাঁস হয়নি, কিন্তু naive কোড মিথ্যা দৈর্ঘ্যের বার্তাটিও কোনো প্রশ্ন ছাড়া গ্রহণ করল। Safe কোড সেটি বাদ দিল, যেমনটি TLS heartbeat-এর RFC 6520 বলে। আর negative index: 40 বৈধ Python, কিন্তু সূচক ভুল হিসাব থেকে এলে সেটি নীরব যুক্তির ভুল। Memory-safe মানে ভুল-মুক্ত নয়।

৯ · পুরোনো কোডের পথ — নির্দেশিকা, Android-এর ফল আর migration

“নতুন করে সব লিখে ফেলি” শুনতে সহজ, কিন্তু বাস্তবে এটি বিরল পথ। আগে দেখি সরকারি নির্দেশিকা আর একটি বড় প্রকল্পের অভিজ্ঞতা কী বলে।

সময়নথিমূল কথা
ডিসেম্বর ২০২৩The Case for Memory Safe Roadmaps (CISA, NSA, FBI আর আন্তর্জাতিক অংশীদার)নির্মাতারা memory safe roadmap লিখে প্রকাশ করুন
অক্টোবর ২০২৪, পরিমার্জন জানুয়ারি ২০২৫Product Security Bad Practices (CISA, FBI)গুরুত্বপূর্ণ অবকাঠামোর নতুন পণ্য নিরাপদ বিকল্প থাকতেও unsafe ভাষায় শুরু করা খারাপ চর্চা; বিদ্যমান পণ্যে ১ জানুয়ারি ২০২৬-এর মধ্যে roadmap প্রকাশ। নথিটি নিজেই বলে, এটি বাধ্যতামূলক নয়
জুন ২০২৫Memory Safe Languages: Reducing Vulnerabilities in Modern Software Development (NSA, CISA)Memory-safe ভাষা গ্রহণে উৎসাহ, একে সর্বরোগের ওষুধ না ভেবে

এগুলো যুক্তরাষ্ট্রের সংস্থার পরামর্শ, বাংলাদেশের কোনো আইন নয়। তবে দিকটি বোঝার জন্য কাজে লাগে।

Android-এর প্রকাশিত ফল

Google জানিয়েছে, Android-এ memory safety ত্রুটির ভাগ ২০১৯-এর ৭৬% থেকে ২০২৪-এ ২৪%-এ নামে, আর ২০২৫-এ প্রথমবার ২০%-এর নিচে। উল্লেখযোগ্য দিক হলো, কৌশলটি পুরোনো কোড নতুন করে লেখা নয়, বরং নতুন কোড memory-safe ভাষায় লেখা। Google-এর ব্যাখ্যায় ত্রুটি কোডের বয়সের সঙ্গে দ্রুত কমে, তাদের কথায় এর একটি half-life আছে (vulnerability half-life), তাই ঝুঁকির বড় অংশ নতুন কোডে।

হিসাব করে দেখুন

Google-এর ২০২৫ সালের লেখায় C ও C++-এর ঘনত্ব ১০ লাখ লাইনে (MLOC) প্রায় 1,000টি memory safety ত্রুটি, আর Rust-এর অনুমান 0.2টি। সরাসরি ভাগ করলে 1000 ÷ 0.2 = 5,000 গুণ। Google নিজে আরও সাবধানী “1000x” কথাটি লেখে। প্রায় ৫০ লাখ লাইন Rust-এ অনুমানটি দাঁড়িয়ে আছে একটিমাত্র সম্ভাব্য ত্রুটির ওপর, তাই এটি নিয়ে সতর্ক থাকুন। এটি একটি প্রতিষ্ঠানের নিজের প্রকাশিত অনুমান, স্বাধীনভাবে যাচাই করা সংখ্যা নয়।

সেই একটি ঘটনাও শিক্ষণীয়। Google জানায়, CrabbyAVIF নামের Rust-এর একটি অংশের unsafe কোডে linear buffer overflow পাওয়া গিয়েছিল। Android-এর Scudo hardened allocator সেটিকে কাজে লাগানোর অযোগ্য করে দেয়, আর ত্রুটিটি প্রকাশের আগেই সারানো হয়। অর্থাৎ নিরাপদ ভাষাতেও ফাঁক থাকে, আর mitigation-এর স্তর তখনও কাজে আসে।

একটি বাস্তব migration পরিকল্পনা

  1. তালিকা: কোন C ও C++ অংশ আছে, কারা অবিশ্বস্ত ইনপুট নেয়, কার legacy মালিক নেই, তা লিখে ফেলুন।
  2. অগ্রাধিকার: ঝুঁকি মাপুন। আগে network-মুখী parser, ছবি ও ফাইলের format, cryptography-র সীমানা। পর্ব ৪-এর threat model এখানে কাজে লাগে।
  3. নতুন ঝুঁকি থামান: নতুন অংশ memory-safe ভাষায় লেখার নিয়ম করুন। ব্যতিক্রম লিখিত থাকুক।
  4. বর্তমানকে শক্ত করুন: Hardening flag, CI-তে sanitizer আর fuzzing, ঝুঁকির অংশ sandbox-এ।
  5. সীমানা বানান: নতুন আর পুরোনো কোডের মাঝে ছোট, পরিষ্কার FFI সেতু। সেতুটিও নিজে একটি ঝুঁকি, তাই সরল রাখুন।
  6. বেছে বেছে সরান: সবচেয়ে ঝুঁকির ছোট অংশগুলো একে একে বদলান, আর পুরোনো অংশ বন্ধ করুন।
  7. মাপুন ও প্রকাশ করুন: নতুন কোডে memory-safe ভাষার ভাগ, memory ত্রুটির সংখ্যা, fuzz coverage. Roadmap-এ অগ্রগতি লিখে রাখুন।

আরও গভীরে: Szekeres ও সহকর্মীদের “SoK: Eternal War in Memory” (IEEE S&P, ২০১৩) mitigation-গুলোর খরচ আর সীমার একটি ক্লাসিক মানচিত্র। Hardware দিকে আছে ARM-এর Memory Tagging Extension (প্রতিটি অংশে tag, pointer-এর সঙ্গে মিলিয়ে দেখা), Intel CET, আর CHERI গবেষণা, যেখানে pointer নিজেই সীমা বহন করে। Rust-এর নিরাপত্তার গাণিতিক প্রমাণের কাজ RustBelt (POPL, ২০১৮)-এ পাবেন, যা unsafe-এর দায়িত্বও স্পষ্ট করে। AddressSanitizer-এর মূল লেখা Serebryany ও সহকর্মীদের (USENIX ATC, ২০১২), আর CFI-র সূচনা Abadi ও সহকর্মীদের লেখায় (CCS, ২০০৫)।

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এমন প্রশ্ন আসতে পারে:

  • Spatial আর temporal safety-র পার্থক্য? Spatial হলো সীমার ভেতরে থাকা, temporal হলো বস্তুর জীবনকালের ভেতরে থাকা।
  • Heartbleed কোন ধরনের বাগ? OpenSSL-এ বাদ পড়া bounds check থেকে out-of-bounds read, যাতে memory ফাঁস হতে পারত।
  • ASLR আর NX কী থেকে রক্ষা করে? ASLR ঠিকানা জানা কঠিন করে, NX ডেটা-memory-তে কোড চালানো আটকায়; কোনোটিই বাগ সারায় না।
  • ASan কি production-এ রাখবেন? সাধারণত না; এটি test-এর টুল, ধীর, আর release-এর জন্য hardening flag আলাদা।
  • Rust কি সব ঝুঁকি দূর করে? না; safe Rust memory ভুলের বড় অংশ ঠেকায়, কিন্তু unsafe, যুক্তির ভুল আর injection থেকে যায়।

১০ · AI-এর যুগে — memory safety-তে AI-কে যাচাই করা

AI খুব দ্রুত C কোড লেখে আর Rust-এ অনুবাদও করে। কিন্তু memory ভুল অনেক সময় test পাস করা কোডেই লুকিয়ে থাকে। তাই AI-এর উত্তরকে একজন নতুন সহকর্মীর খসড়া ভাবুন।

AI-এর সাধারণ ভুলবিপদযাচাই
Warning বা sanitizer error থামাতে flag বন্ধ করতে বলেউপসর্গ লুকায়, বাগ থাকেমূল কারণ বের করুন; flag কখনো বন্ধ নয়
C থেকে Rust-এ অনুবাদে সবকিছু unsafe-এ মুড়ে দেয়নিশ্চয়তা হারায়, শুধু ভাষা বদলায়unsafe গুনে দেখুন, প্রতিটির কারণ লিখুন
strncpy-কে পুরো নিরাপদ বলে, বা দৈর্ঘ্যের যোগফলের overflow ভোলেOff-by-one, string শেষ না হওয়াসীমার মানে test: 0, cap−1, cap, cap+1
CVE নম্বর, শতকরা হিসাব বা নির্দেশিকার নাম বানিয়ে দেয়ভুল সিদ্ধান্তVendor advisory, CVE রেকর্ড আর মূল নথি পড়ুন
“এই crash কাজে লাগানো যায় না” বলে দৃঢ়ভাবে রায় দেয়Triage-এ অতি-আত্মবিশ্বাসমানুষ সিদ্ধান্ত নেবে; সন্দেহ থাকলে ঠিক করুন

নিজের কোড review করাতে নিচের কাঠামো ব্যবহার করুন। আগে key, token বা গ্রাহকের ডেটা সরিয়ে নিন।

তুমি একজন সতর্ক secure-code reviewer. নিচের C ফাংশন আমার নিজের প্রকল্পের, কোনো secret নেই। [এখানে নিজের কোড পেস্ট করুন] ১. প্রতিটি buffer-এর আকার কোথা থেকে আসে, আর কোথায় যাচাই হয়, তা তালিকা করো। ২. প্রতিটি allocation-এর মালিক কে, আর কে free করে বলো। Use-after-free বা double free-র সম্ভাবনা দেখাও। ৩. শুধু প্রতিরক্ষার সংশোধন দাও: সবচেয়ে ছোট diff, আর কেন। ৪. সীমার মান দিয়ে কোন unit test আর কোন sanitizer চালাব বলো। ৫. কোনো CVE বা মান উল্লেখ করলে উৎস দাও, নিশ্চিত না হলে "নিশ্চিত নই" লেখো। Exploit বা আক্রমণের ধাপ লিখবে না।

AI-এর পরামর্শ গ্রহণের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১কয়েক দশকের mitigation-এর পরেও memory ভুলের ভাগ এত বড় থাকল কেন?

Stack canary-র ধারণা জনপ্রিয় হয় ১৯৯৮ সালে Crispin Cowan ও সহকর্মীদের StackGuard লেখার মাধ্যমে। এরপর এলো NX, ASLR, CFI আর hardware-এর সুরক্ষা। প্রতিটি সত্যিই কাজে লেগেছে, কারণ একসময় যা সহজ ছিল তা এখন অনেক কঠিন ও ব্যয়বহুল। তবু ২০১৯ সালে Microsoft জানাল, বছরের পর বছর তাদের দেওয়া CVE-র প্রায় ৭০% memory safety সমস্যা। সংখ্যাটি একই জায়গায় থাকার কারণ বুঝতে হলে দুটি জিনিস আলাদা করতে হবে: বাগ তৈরি হওয়ার হার, আর বাগ কাজে লাগানোর খরচ।

Mitigation দ্বিতীয়টি বদলায়, প্রথমটি নয়। C ও C++-এ লেখা প্রতিটি নতুন লাইন আগের মতোই ভুল করতে পারে, কারণ চুক্তি ভাষার ভেতরে নয়, মানুষের মনোযোগে। তাই বাগের ভাগ না কমে, শুধু একটি বাগ দিয়ে বড় ক্ষতি করতে আরও বেশি শ্রম লাগে। আক্রমণকারীও বসে থাকে না; সে এক mitigation-এর সীমা পেরোতে নতুন পথ খোঁজে, আর রক্ষক তখন নতুন স্তর বানান। গবেষকেরা একে “eternal war in memory” নাম দিয়েছেন।

Android-এর প্রকাশিত ফল এই চক্রের বাইরে একটি ভিন্ন দৃশ্য দেখায়। সেখানে ভাগ কমেছে mitigation বাড়িয়ে নয়, নতুন কোডে ভুলের উৎপাদন কমিয়ে। অবশ্যই এটি একটি প্রতিষ্ঠানের অভিজ্ঞতা, আর হিসাবের ধরনও তাদের নিজের। তবু যুক্তিটি সাধারণ: কারখানায় নষ্ট পণ্য বাছাই বাড়ানোর চেয়ে যন্ত্রের ত্রুটি সারানো বেশি টেকসই।

মূল উপলব্ধি: Mitigation আক্রমণের খরচ বাড়ায়, ভাষা ও নকশা বাগের জন্মই কমায়। দুটোই দরকার, কিন্তু দুটিকে এক ভাবা ভুল।

প্র ০২Heartbleed-এর সংশোধন ছিল ছোট, তবু ত্রুটিটি প্রায় দুই বছর ধরা পড়েনি কেন?

Heartbleed-এর কোড ২০১২ সালের 1.0.1 release-এ যায়, আর এপ্রিল ২০১৪-তে প্রকাশ পায়। মূল ভুলটি ছোট: একটি দৈর্ঘ্য যাচাই নেই। OpenSSL খোলা উৎসের কোড, বহু মানুষ চাইলেই পড়তে পারতেন। তাহলে কেন এতদিন লাগল?

প্রথম কারণ প্রযুক্তিগত। এটি একটি out-of-bounds read, তাই crash হয় না, ডেটা নষ্ট হয় না, সাধারণ test-ও পাস করে। সাধারণ log-এ প্রায় চিহ্ন থাকে না। Canary বা NX-এর কাজের পথেই এটি পড়ে না, কারণ এখানে চলার পথ বদলায় না। অর্থাৎ বাগটি প্রতিটি সাধারণ সতর্ক-ব্যবস্থার চোখের আড়ালে ছিল।

দ্বিতীয় কারণ অর্থনৈতিক। ইন্টারনেটের বিপুল অংশ যে library-র ওপর চলত, তার রক্ষণাবেক্ষণের দল ছিল খুব ছোট, অর্থায়নও সীমিত। ঘটনার পর ২০১৪ সালেই Linux Foundation-এর উদ্যোগে Core Infrastructure Initiative শুরু হয়, এমন গুরুত্বপূর্ণ প্রকল্পে অর্থ দিতে। “অনেক চোখ থাকলে সব বাগ ধরা পড়ে” কথাটি তখনই সত্য, যখন চোখগুলো সত্যিই কোডের দিকে তাকায়।

তৃতীয় কারণ টুলের। ২০১৫ সালে Hanno Böck afl fuzzer ব্যবহার করে ত্রুটিটি আবার খুঁজে পাওয়ার কথা লেখেন। এটি দেখায় যে পরে আসা সস্তা, স্বয়ংক্রিয় পদ্ধতি এই শ্রেণির বাগ ধরতে পারে, যদিও সেই সময়ের প্রকল্পে এমন test চালানো সহজ ছিল না। এরপর OSS-Fuzz-এর মতো সেবা গুরুত্বপূর্ণ খোলা প্রকল্পে নিয়মিত fuzzing নিয়ে আসে।

মূল উপলব্ধি: নীরব বাগ ধরতে ইনপুট-চালিত টুল আর টেকসই অর্থায়ন লাগে। গুরুত্বপূর্ণ dependency-কে “কেউ না কেউ দেখছে” ভাবা নিজেই একটি ঝুঁকি।

প্র ০৩তাহলে সব C ও C++ কোড Rust-এ নতুন করে লিখে ফেলা হচ্ছে না কেন?

প্রথম বাধা পরিমাণ। Kernel, database, browser, codec, firmware মিলিয়ে কয়েক শত কোটি লাইন কোড চলছে, যার অনেকটি কয়েক দশকের বাগ-সংশোধনের ফল। এই কোডে লুকানো ব্যবসায়িক নিয়ম, পুরোনো যন্ত্রের সঙ্গে মানিয়ে চলার কৌশল আর ছোট আচরণ সব লিখিত নেই। নতুন করে লিখলে memory ভুল কমে, কিন্তু যুক্তির নতুন বাগ আসতে পারে। ব্যবসার কাছে নতুন কোনো সুবিধা ছাড়াই বছরের পর বছরের খরচ, এই হিসাব পাস করা কঠিন।

দ্বিতীয় যুক্তিটি নিরাপত্তারই। ২০২৪ সালে Google লেখে, ত্রুটি কোডের বয়সের সঙ্গে দ্রুত কমে, আর সমস্যা মূলত নতুন কোডে। পুরোনো, স্থিতিশীল কোড নতুন করে লেখার লাভ সময়ের সঙ্গে কমতে থাকে। তাই সবচেয়ে বেশি লাভ নতুন কোড নিরাপদ ভাষায় লিখে, পুরোনোকে শক্ত করে ও ধীরে ধীরে সরিয়ে। তাদের ভাষায়, “interoperability is the new rewrite”: নতুন আর পুরোনো কোডকে নিরাপদে জুড়ে রাখাই বাস্তব পথ।

তৃতীয় বাধা বাস্তুতন্ত্র। ছোট microcontroller, নিরাপত্তা-সনদপ্রাপ্ত যন্ত্র আর কিছু CPU-তে compiler ও টুল C-তেই সবচেয়ে পরিণত। দলের দক্ষতাও রাতারাতি বদলায় না। আবার FFI সেতুতে দুই ভাষার নিয়ম মিলতে হয়, তাই ভুল সেখানেও ঢুকতে পারে।

তবু কিছু জায়গায় পুরো অংশ বদলানো সঠিক: ছোট, অবিশ্বস্ত ইনপুট-নেওয়া parser, যেখানে ঝুঁকি বেশি আর কোড অল্প। মূল উপলব্ধি: প্রশ্ন “C না Rust” নয়। প্রশ্ন হলো, প্রতি টাকা আর প্রতি ঘণ্টায় কোন কাজে সবচেয়ে বেশি ঝুঁকি কমে।

প্র ০৪Memory-safe ভাষায় লিখলেই কি software নিরাপদ? Log4Shell কী শেখায়?

না। ডিসেম্বর ২০২১-এ প্রকাশিত Log4Shell (CVE-2021-44228) ছিল Java-র জনপ্রিয় logging library Log4j-এর ত্রুটি। Java memory-safe ভাষা, তবু ত্রুটিটি বিশ্বজুড়ে জরুরি সাড়া দাবি করেছিল। কারণ সমস্যা memory-তে ছিল না, ছিল নকশায়: log করা ডেটাকে কিছু অবস্থায় lookup-এর নির্দেশ হিসেবে দেখা হতো। ডেটা আর নির্দেশ আলাদা না রাখার সেই পুরোনো ভুল, যা পর্ব ৩৯-এর injection-এর মূলে আছে।

Memory-safe ভাষা একটি শ্রেণির ভুল দূর করে: অন্য সব শ্রেণি থেকে যায়। ভুল অনুমতি, যুক্তির ত্রুটি, SQL injection, দুর্বল cryptography, ঝুঁকিপূর্ণ deserialization, ফাঁস হওয়া secret, আর dependency-র ঝুঁকি। ওয়েব অ্যাপের OWASP Top 10-এর বেশিরভাগ বিষয় ভাষার memory safety-র সঙ্গে সম্পর্কহীন।

আবার নিরাপদ ভাষারও নিচে অনিরাপদ স্তর থাকে। Python-এর C extension, Java-র JNI, Rust-এর unsafe আর Go-র cgo, সব জায়গায় নিশ্চয়তা শেষ হয়ে লেখকের দায়িত্ব শুরু হয়। CrabbyAVIF-এর ঘটনা এর সাম্প্রতিক উদাহরণ।

তাহলে লাভ কী? লাভ হলো ঝুঁকির পরিমাণ আর ধরন বদলানো। যে শ্রেণি গুরুতর ত্রুটির বড় অংশ, সেটি ঠেকলে দলের সময় অন্য ঝুঁকিতে যায়। CISA ও NSA-র ২০২৫ সালের নির্দেশিকাও এই কথাই বলে: এটি সর্বরোগের ওষুধ নয়, তবু বড় পরিসরে এই শ্রেণির ঝুঁকি কমানোর সবচেয়ে কার্যকর পথগুলোর একটি।

মূল উপলব্ধি: নিরাপদ ভাষা একটি দেয়াল, পুরো দুর্গ নয়। ভাষা বাছাইয়ের পরেও threat modeling, review আর patch ব্যবস্থা লাগবে।

প্র ০৫নিরাপত্তায় crash কখনো ভালো খবর হয় কেন? আর কখন হয় না?

Stack canary মিল না পেলে প্রোগ্রাম থেমে যায়। Rust ভুল সূচকে panic করে। Hardened allocator ভুল ধরলে process বন্ধ করে। সবগুলো স্তর একই সিদ্ধান্ত নিয়েছে: অনিশ্চিত অবস্থায় চলতে থাকার চেয়ে থেমে যাওয়া ভালো। এটি পর্ব ৩-এর fail secure নীতির memory রূপ।

কারণটি CIA-র হিসাবে পরিষ্কার। Memory নষ্ট হওয়ার পর প্রোগ্রাম চলতে থাকলে confidentiality আর integrity, দুটোই অজানা ঝুঁকিতে পড়ে: ভুল ডেটা ডেটাবেসে যেতে পারে, গোপন তথ্য বেরিয়ে যেতে পারে। Crash-এ হারায় শুধু availability, আর সেটি দৃশ্যমান, মাপা যায়, সেবাও আবার চালু করা যায়। তা ছাড়া crash একটি সংকেত। Report-এ লাইন নম্বর থাকে, নীরব ক্ষতিতে কিছুই থাকে না।

কিন্তু এই যুক্তি সব জায়গায় খাটে না। হাসপাতালের যন্ত্র, গাড়ির ব্রেক বা শিল্প-নিয়ন্ত্রণে হঠাৎ থেমে যাওয়া নিজেই বিপদ। সেখানে safe state আগে থেকে নকশা করতে হয়, যেমন আলাদা watchdog আর নিরাপদ অবস্থানে ফেরা। আবার অবিশ্বস্ত ইনপুট দিয়ে যদি বারবার crash ঘটানো যায়, তা denial of service-এর পথ হয়ে দাঁড়ায়। তাই সার্ভারে একটি request-এর ভুল পুরো process নামাবে না, এমন বিচ্ছিন্নতা আর crash-এর হার নজরদারিতে রাখা দরকার।

রক্ষক হিসেবে crash report-কে শুধু “প্রোগ্রাম আবার চালু করুন” ভাববেন না। একই জায়গায় বারবার crash হলে triage করুন, কারণ এটি বাগ, ব্যবহার বা আক্রমণের চেষ্টা, যেকোনোটির সংকেত হতে পারে।

মূল উপলব্ধি: নিয়ন্ত্রিত crash হলো নীরব ক্ষতির চেয়ে ভালো, কিন্তু এটি শেষ সমাধান নয়। কোথায় থামা নিরাপদ আর থামার পর কী হবে, সেটিও নকশার অংশ।

অনুশীলন

হিসাব, ফল অনুমান, নকশা-সমালোচনা আর নিজের কোডে একটি যাচাই। শুধু নিজের প্রকল্প বা অনুমতি পাওয়া কোডে করুন।

  1. হিসাব: (ক) একটি কাল্পনিক পণ্যে এক বছরে 1,200টি CVE, যার 70% memory safety সমস্যা। সংখ্যাটি কত? (খ) প্রকাশিত ঘনত্ব C ও C++-এ 1,000/MLOC আর Rust-এ 0.2/MLOC ধরে 2 লাখ নতুন লাইনে দুই ক্ষেত্রে প্রত্যাশিত ত্রুটি কত? (গ) ASLR-এর entropy 12 bit থেকে 28 bit হলে সম্ভাব্য অবস্থান কত গুণ বাড়ে?

    (ক) 1,200 × 0.70 = 840টি.

    (খ) 2 লাখ লাইন = 0.2 MLOC. C ও C++-এ 0.2 × 1,000 = 200টি. Rust-এ 0.2 × 0.2 = 0.04টি. ব্যাখ্যা করতে হবে সাবধানে: এটি একটি প্রতিষ্ঠানের প্রকাশিত ঘনত্ব থেকে সরল হিসাব, আপনার কোডের ভবিষ্যদ্বাণী নয়।

    (গ) 228 ÷ 212 = 216 = 65,536 গুণ.

  2. ফল অনুমান করুন: অংশ ৩-এর এক-ঘরের খেলনা tracker-এ gen শুরু হয় 1 থেকে, আর প্রতি free-তে 1 বাড়ে। ক্রম হলো: a = alloc("x"), free(a), b = alloc("y"), free(a), read(b). শেষে ঘরের gen, দ্বিতীয় free-র ফল আর read(b)-র ফল কী?

    a-র gen 1. free(a)-এর পর ঘরের gen 2. b পায় gen 2. দ্বিতীয় free(a)-এ 1 আর 2 মেলে না, ঘরটি বরাদ্দ করা, তাই ফল refused double free (STALE). ঘর আগের মতোই থাকে, gen 2, ডেটা y. read(b) দেয় "y". Tracker না থাকলে এই free b-র ডেটাই মুছে দিত, আর b নিজেই নিরপরাধ অবস্থায় ক্ষতিগ্রস্ত হতো।

  3. নকশা সমালোচনা: একটি কাল্পনিক অনলাইন বাজারে 2 লাখ লাইনের C-তে লেখা ছবি-প্রক্রিয়ার সেবা আছে, যা বিক্রেতাদের আপলোড নেয়। একজন প্রস্তাব দিলেন: “তিন মাসে পুরোটা Rust-এ লিখে ফেলি, এর মধ্যে আর কোনো কাজ বন্ধ।” প্রস্তাবটির সমালোচনা করুন আর একটি ভালো পরিকল্পনা দিন।

    সমস্যা: সময় অবাস্তব, পরীক্ষিত আচরণ হারানোর ঝুঁকি, যুক্তির নতুন বাগ, দলের দক্ষতা অজানা, আর এই সময়ে পুরোনো কোড অরক্ষিত থাকে।

    ভালো পরিকল্পনা: এখনই hardening flag আর CI-তে ASan ও fuzzing চালু করা। ফাইলের format-parser আলাদা, কম অনুমতির sandbox process-এ, আপলোডের আকার ও ধরন সীমিত। নতুন feature Rust বা অন্য memory-safe ভাষায়। সবচেয়ে ঝুঁকির parser প্রথমে বদলানো, পুরোনোর সঙ্গে একই ইনপুটে ফল মিলিয়ে দেখা। ছোট FFI সেতু, crash হারের নজরদারি, আর ত্রৈমাসিক অগ্রগতির হিসাব।

  4. কমান্ড: নিজের একটি ছোট C প্রকল্পে অংশ ৬-এর test build আর release build বানান। readelf দিয়ে PIE, NX আর canary মিলিয়ে দেখুন। কী পেলে কী বুঝবেন? কোনটিতে ASan থাকবে?

    Type: DYN সাধারণত PIE নির্দেশ করে, যাতে ASLR প্রোগ্রামের কোডকেও সরাতে পারে। GNU_STACK সারিতে RW মানে stack চালানো যায় না, RWE দেখলে তদন্ত করুন। __stack_chk_fail থাকলে অন্তত কিছু ফাংশনে canary আছে; না থাকলে হয়তো কোনো ফাংশনই মানদণ্ডে পড়েনি, তাই একা এটি প্রমাণ নয়। ASan থাকবে শুধু app_test-এ। Linux distribution ভেদে default আলাদা হতে পারে, তাই অনুমান না করে নিজের build মাপুন।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমপুরোনো C কোডবেসের ঝুঁকি কমানোর ধাপগুলো সাজান (▲▼ চেপে সরান)

  1. কোড আর ইনপুটের তালিকা বানিয়ে ঝুঁকি মাপা
  2. নতুন কোড memory-safe ভাষায় লেখার নিয়ম করা
  3. পুরোনো কোডে hardening flag, sanitizer আর fuzzing যোগ করা
  4. ঝুঁকির অংশ sandbox-এ রেখে ছোট FFI সীমানা বানানো
  5. সবচেয়ে ঝুঁকির অংশ একে একে বদলানো আর অগ্রগতি মাপা

কোনটি temporal memory safety-র ভুল?

  • Buffer-এর শেষের পরের ঘরে লেখা
  • Array-র সীমার বাইরে থেকে পড়া
  • Free করা memory আবার ব্যবহার করা
  • SQL query-তে ইনপুট জুড়ে দেওয়া

২০১৪ সালের Heartbleed (CVE-2014-0160) কোন ধরনের ত্রুটি ছিল?

  • Stack-এ buffer overflow লেখা
  • বাদ পড়া bounds check থেকে out-of-bounds read
  • Use-after-free
  • AES algorithm-এর গাণিতিক দুর্বলতা

একটি প্রোগ্রামে heap-এর buffer থেকে out-of-bounds read আছে। শুধু stack canary চালু থাকলে কী হবে?

  • Canary read ধরে process থামাবে
  • Canary heap এনক্রিপ্ট করে ফাঁস ঠেকাবে
  • Canary compile-এর সময় বাগ সারিয়ে দেবে
  • Canary কিছুই বুঝবে না, ফাঁস ঘটতে পারে

AddressSanitizer-সহ test পাস করেছে। এ থেকে সবচেয়ে ঠিক সিদ্ধান্ত কোনটি?

  • যে পথগুলো চলেছে, সেখানে ASan কোনো ধরার মতো ভুল পায়নি; বাকি পথ অজানা
  • প্রোগ্রামে কোনো memory ভুল নেই বলে প্রমাণ হলো
  • এখন release-এ hardening flag লাগবে না
  • প্রোগ্রামে কোনো নিরাপত্তা ত্রুটি নেই

Safe Rust-এ let moved = order; লেখার পর order ব্যবহার করলে (order একটি String) কী হয়?

  • চালানোর সময় ফাঁকা string পাওয়া যায়
  • Compile error হয়, প্রোগ্রাম তৈরিই হয় না
  • Use-after-free ঘটে আর undefined behavior হয়
  • String স্বয়ংক্রিয়ভাবে কপি হয়ে দুটিই চলে

Google-এর প্রকাশিত লেখা অনুযায়ী, Android-এ memory safety ত্রুটির ভাগ কমার প্রধান কৌশল কোনটি?

  • সব পুরোনো C ও C++ কোড Rust-এ নতুন করে লেখা
  • শুধু নতুন hardware mitigation যোগ করা
  • নতুন কোডে memory-safe ভাষাকে অগ্রাধিকার দেওয়া
  • Fuzzing আর sanitizer বন্ধ করা

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Memory safety মানে জায়গা আর সময়, দুই সীমাই মানা।
  2. প্রকাশিত “প্রায় ৭০%” বড় C ও C++ কোডের হিসাব। কে, কী গুনেছে, তা আগে দেখুন।
  3. Mitigation খরচ বাড়ায়, বাগ সারায় না। Heartbleed-এর মতো read-এ অনেকগুলোই অকেজো।
  4. Test-এ ASan, UBSan, Valgrind, fuzzing; release-এ hardening flag, আর নিজের build যাচাই।
  5. নতুন কোড memory-safe ভাষায়, পুরোনোকে শক্ত করে ধীরে সরানো। নিরাপদ ভাষাও অন্য ঝুঁকি দূর করে না।

পরের পর্বে: আপনার কোড নিরাপদ হলেও আপনি যে dependency ব্যবহার করছেন, সেটি কি নিরাপদ? পরের পর্বে দেখব supply chain, SBOM, signing আর SLSA.

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান