
سلسلة أدوات Go موجّهة نحو الأمن، تركّز على قدرات Fuzzing على أحدث مستوى.
gosentry هو فرع (fork) من سلسلة أدوات Go تركّز على الأمان، يدمج العديد من الميزات لحملات التضمين (fuzzing) المتطورة على قواعد أكواد Go. إذا كنت تستخدم go test -fuzz سابقًا، فينبغي أن تستخدم gosentry كبديل.
يأتي هذا مع تحسينات متنوعة للتضمين وكاشفات أخطاء غير موجودة أصلاً في سلسلة أدوات Go. انظر TLDR; أدناه. يمكنك أيضًا قراءة المقالة المرتبطة بالمدونة هنا.
TLDR (الميزات والخيارات):
struct مباشرة (بدون الحاجة إلى محلل مخصص). أضف بذورًا باستخدام f.Add(Input{N: 7, S: "hi"}) ثم f.Fuzz(func(t *testing.T, in Input) { ... }).X + Y - Z يمكن أن تصبح X / U + Z - 14 بدلاً من X + Yè - Zcd src && ./make.bash # Produces ../bin/go. See GOFLAGS below.
> [!TIP]
> وثائق المساهمين: اقرأ `docs/gosentry/index.md` للحصول على خريطة الكود، وحلقة التطوير الموصى بها، ونقاط دخول CI، وسكربتات القياس.
> يستخدم هذا الفرع تطبيق Pull GitHub لفتح ودمج PRs تلقائيًا من `golang/go:master` إلى `master`، مما يضمن ألا نتخلف أبدًا عن أحدث تحديثات سلسلة أدوات Go.
## الميزة 1: الاختبار التلقائي المبني على البنى (fuzz structs كمدخلات)
#### نظرة عامة
الاختبار التلقائي الأصلي في Go (`go test -fuzz=...`) يدعم فقط مجموعة صغيرة من الأنواع العددية كوسائط للاختبار التلقائي (`[]byte`, `string`, أرقام, ...). في gosentry، يمكنك أيضًا اختبار **الأنواع المركبة** المبنية من تلك الأنواع العددية: البنى (structs)، المصفوفات، الشرائح، والمؤشرات.
هذا مفيد عندما يتلقى الكود الخاص بك مدخلات منظمة بشكل طبيعي ولا تريد بناء مُرمِّز/مُفكِّك ترميز مخصص فقط لتزويد المجموعة (corpus) وتغييرها.
انظر `test/gosentry/examples/multiargs` و `test/gosentry/examples/composite` للحصول على أمثلة.
#### مثال بسيط```go
type Input struct {
Data []byte
S string
N int
OK bool
}
func FuzzStructInput(f *testing.F) {
// Seed the initial corpus with a Go struct (gosentry feature).
f.Add(Input{Data: []byte("A"), S: "B", N: 7, OK: true})
f.Fuzz(func(t *testing.T, in Input) {
if in.OK && in.N == 1337 && in.S == "BOOMMOOB" && bytes.Equal(in.Data, []byte("A")) {
t.Fatalf("boom")
}
})
}
f.Add) والتفزيز البنيوي (اللاصق المُصنَّع)لا يمكن للمفزز الأصلي في Go تفزيز قيمة struct مباشرة (فهو يعرف فقط كيفية تحوير قائمة صغيرة من الأنواع العددية). يضيف gosentry طبقة لاصقة صغيرة: عندما يستهدف المفزز أنواعًا مركّبة (مثل Input)، يقوم gosentry بتفزيز []byte واحدة خلف الكواليس. في كل تنفيذ، يقوم بفك ترميز تلك البايتات إلى البنية الخاصة بك (حقلًا بحقل، وبشكل تكراري للمقاطع/المصفوفات/المؤشرات) ثم يستدعي رد اتصال f.Fuzz بالقيمة المفكوكة. يُستخدم نفس الترميز للبذور، لذا فإن f.Add(Input{...}) يصبح إدخال مجموعة بايتات مشفّرة يمكن للمفزز إعادة استخدامها وتحويرها مثل أي بذرة أخرى.
تقوم المفززات (بما فيها LibAFL) بتحوير البايتات الخام، لذلك نريد وحدة فك ترميز يمكنها تحويل أي شريحة بايتات إلى قيمة بنية "ما" والاستمرار. JSON/gob سترفض معظم المدخلات العشوائية (أمر سيء للتغطية)، كما أنها لا تملأ الحقول غير المُصدَّرة، بينما غالبًا ما يستفيد التفزيز من كسر القيود الثابتة. هذا التنسيق المخصص صغير وسريع وحتمي ومتسامح مع البيانات التالفة.
في الكواليس، يستخدم هذا تنسيق gosentry الثنائي البسيط الخاص (ليس gob وليس JSON). الكود موجود في src/testing/libafl.go:
libaflMarshalInputs / هذا العمل مستوحى من الأداة المطورة سابقًا go-panikint. يضيف كشفًا للتجاوز (overflow) والتجاوز السفلي (underflow) في العمليات الحسابية على الأعداد الصحيحة، و(اختياريًا) كشفًا لاقتطاع الأنواع في تحويلات الأعداد الصحيحة. عند اكتشاف تجاوز أو اقتطاع، يُطلق panic برسالة خطأ مفصلة، تشمل نوع العملية المحدد والأنواع الصحيحة المعنية.
العمليات الحسابية: تتعامل مع الجمع + والطرح - والضرب * والقسمة / لكل من الأنواع الصحيحة الموقّعة وغير الموقّعة. للأعداد الموقّعة، تشمل int8 وint16 وint32. للأعداد غير الموقّعة، تشمل uint8 وuint16 وuint32 وuint64. حالة القسمة تكشف تحديدًا حالة التجاوز MIN_INT / -1 للأعداد الموقّعة. int64 وuintptr لا يتم فحصهما في العمليات الحسابية.
كشف اقتطاع النوع: يكشف تحويلات الأنواع الصحيحة التي قد تسبب فقدانًا للبيانات. يشمل جميع الأنواع الصحيحة: int8 وint16 وint32 وint64 وuint8 وuint16 وuint32 وuint64. يستبعد uintptr بسبب استخدامه المعتمد على المنصة. هذا معطّل افتراضيًا.
كشف التجاوز مفعّل افتراضيًا. لتعطيله، أضف GOFLAGS='-gcflags=-overflowdetect=false' قبل تنفيذ ./make.bash. يمكنك أيضًا تفعيل فاحص الاقتطاع بإضافة: -gcflags=-truncationdetect=true
تقوم هذه الميزة بتعديل توليد SSA في المترجم بحيث تحصل العمليات الحسابية على الأعداد الصحيحة وتحويلات الأعداد الصحيحة على فحوصات إضافية في زمن التشغيل تستدعي بيئة التشغيل لإطلاق panic برسالة خطأ مفصلة عند اكتشاف خطأ. تُطبَّق الفحوصات باستخدام تصفية مبنية على موقع المصدر بحيث يتم تزويد كود المستخدم بالفاحصات بينما يتم تخطي ملفات المكتبة القياسية والتبعيات (ذاكرة الوحدات وvendor/).
يمكنك قراءة مقالة المدونة المرتبطة بهذا الموضوع هنا.
أضف علامة على نفس سطر العملية أو السطر الذي يسبقه مباشرة لكبت تقرير محدد:
overflow_false_positivetruncation_false_positiveمثال:```go // overflow_false_positive intentionalOverflow := a + b // truncation_false_positive x := uint8(big) sum2 := a + b // overflow_false_positive x2 := uint8(big) // truncation_false_positive
أحيانًا قد لا يعمل هذا، والسبب أن Go تقوم بتضمين الدالة (inlining). إذا لم يكن `// overflow_false_positive` كافيًا، أضف `//go:noinline` قبل توقيع الدالة الخاصة بك.
## الميزة 3: التسبب في panic عند استدعاء دوال محددة
عند فحص الأهداف باستخدام fuzzing، قد نكون مهتمين بتفعيل panic عند استدعاء دوال معينة. على سبيل المثال، قد تصدر بعض البرامج رسائل `log.error` بدلاً من التسبب في panic، على الرغم من أن هذه الحالات غالبًا ما تشير إلى حالات يريد باحثو الأمان اكتشافها أثناء fuzzing.
ومع ذلك، عادةً ما يتم التعامل مع هذه الأخطاء داخليًا (على سبيل المثال، عبر آليات إعادة المحاولة أو الإيقاف المؤقت، أو عبر طباعة الرسائل في السجلات)، مما يجعلها غير مرئية إلى حد كبير لأدوات fuzzing. الهدف من هذه الميزة هو معالجة هذه المشكلة.
#### طريقة الاستخدام
قم بتجميع gosentry، ثم استخدم الخيار `--panic-on`.```bash
./bin/go test -fuzz=FuzzHarness --use-libafl --focus-on-new-code=false --catch-races=false --catch-leaks=false --panic-on="test_go_panicon.(*Logger).Warning,test_go_panicon.(*Logger).Error"
المثال أعلاه سيُحدث panic عند استدعاء إما (*Logger).Warning أو (*Logger).Error (قائمة مفصولة بفواصل).
يقدم LibAFL أداءً أفضل بكثير من أداة فزّينغ Go التقليدية. عند الفزّينغ (go test -fuzz=...)، يستخدم gosentry LibAFL افتراضيًا (المشغل في golibafl/).
ملاحظة ثبات: في وضع LibAFL، يفرض gosentry GODEBUG=updatemaxprocs=0 (تعطيل تحديثات GOMAXPROCS التلقائية في وقت التشغيل) لتجنب تعطل متقطع في CI الخاص بنظام Linux ("sync: inconsistent mutex state"). التفاصيل في misc/gosentry/USE_LIBAFL.md.
عند استخدام LibAFL (الوضع الافتراضي)، يجب عليك أن تختار صراحةً ما إذا كنت تريد تفعيل الجدولة المدركة لـ git أم لا: --focus-on-new-code=true|false. مزيد من التوثيق في ملف Markdown هذا.
يمكنك أيضًا تمرير ملف إعداد JSONC اختياري لـ LibAFL (بما في ذلك خيارات الفزّينغ القواعدي)، انظر هنا.
مع "stop_all_fuzzers_on_panic": false، يحفظ LibAFL كل تعطل ويعيد تشغيل عميله لمواصلة الفزّينغ.```bash
./bin/go test -fuzz=FuzzHarness --focus-on-new-code=false --catch-races=false --catch-leaks=false --libafl-config=path/to/libafl.jsonc # optional --libafl-config
استخدم `-fuzztime=1m` لإيقاف حملة LibAFL بعد دقيقة واحدة.
توليد تقرير التغطية من مجموعة حملة LibAFL موثق في [الميزة 8](#feature-8-generate-go-coverage-reports-from-fuzzing-campaign).
التضمين القائم على القواعد (Nautilus) موثق في [الميزة 7](#feature-7-grammar-based-fuzzing-nautilus).
<details>
<summary><strong>كيف يتم ربط Go مع LibAFL معًا</strong></summary>```text
┌───────────────────────────────────────────────────────────────────────────┐
│ 1) gosentry `go test` │
│ - captures `testing.F.Fuzz(...)` callback │
│ - generates extra source file: `_libaflmain.go` │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 2) Generated bridge: `_libaflmain.go` │
│ - provides libFuzzer-style C ABI entrypoints: │
│ LLVMFuzzerInitialize │
│ LLVMFuzzerTestOneInput │
│ - adapts bytes -> Go types -> calls the captured fuzz callback │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 3) `libharness.a` (static archive on disk) contains: │
│ - compiled objects for all test package (+ dependencies) │
│ - generated `_testmain.go` + `_libaflmain.go` │
│ - LLVMFuzzerInitialize │
│ - LLVMFuzzerTestOneInput │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 4) `golibafl/` (Rust + LibAFL) │
│ env: HARNESS_LIB=/path/to/libharness.a │
│ fuzz loop: mutate input -> LLVMFuzzerTestOneInput(data) -> observe │
└───────────────────────────────────────────────────────────────────────────┘
في وضع --use-libafl، يقوم gosentry ببناء libharness.a ويقوم مشغّل Rust golibafl بتشغيله داخل العملية عبر نقاط دخول libFuzzer. ملاحظة: يمكن أن يشير HARNESS_LIB إلى أي اسم أرشيف harness (على سبيل المثال libharness_race.a المستخدم من قبل --catch-races).
في وضع --use-libafl، يقوم gosentry بترجمة harness Go مع تفعيل آلية تتبع التغطية. يضيف هذا عدّادات صغيرة إلى الكود تتغير عند تنفيذ أجزاء مختلفة من برنامجك. عندما يبدأ harness داخل golibafl، يكشف وقت تشغيل Go عن هذه العدّادات إلى LibAFL. يقرأ LibAFL هذه العدّادات بعد كل إدخال لمعرفة أي كود تم تنفيذه، ويستخدم تلك التغطية لتوجيه الطفرات التالية.
لنتحدث عن الدافع وراء استخدام LibAFL. إن التضمين باستخدام go test -fuzz متخلف بكثير عن أحدث تقنيات التضمين. مثال جيد على ذلك هو CMPLOG/Redqueen الخاص بـ AFL++. تسمح هذه الميزات لأدوات التضمين بحل قيود معينة. لنفترض المقتطف التالي```go
if input == "IMARANDOMSTRINGJUSTCMPLOGMEMAN" {
panic("this string is illegal")
}
أدوات التضمين المتطورة (SOTA) مثل AFL++ أو LibAFL كانت ستكتشف الانهيار (panic) فورًا في تلك الحالة. لكن أداة التضمين الأصلية للغة Go لن تفعل ذلك. تلك فجوة هائلة تعيق استكشاف التغطية **بكثير**.
المعيار أدناه يُظهر هذه الحدود. لاحظ أن هذه المعايير يمكن **إعادة إنتاجها** وتحسينها عبر [مستودع gosentry-bench-libafl](https://github.com/kevin-valerio/gosentry-bench-libafl/tree/main).
##### المعيار 1:
الرسم البياني أدناه يوضح تطور عدد الأسطر المغطاة أثناء التضمين (Fuzzing) لمكتبة [UUID](https://github.com/google/uuid) الخاصة بجوجل باستخدام LibAFL مقابل أداة التضمين الأصلية للغة Go.

##### المعيار 2:
الرسم البياني أدناه يوضح تطور عدد الأسطر المغطاة أثناء التضمين (Fuzzing) لمشروع [go-ethereum](https://github.com/ethereum/go-ethereum) باستخدام LibAFL مقابل أداة التضمين الأصلية للغة Go.

#### مثال
يمكنك اختباره على بعض أحزمة التضمين (fuzzing harnesses) في المسار `test/gosentry/examples/`.```bash
cd test/gosentry/examples/reverse
../../../../bin/go test -fuzz=FuzzReverse --focus-on-new-code=false --catch-races=false --catch-leaks=false
أوقف حملة fuzz باستخدام Ctrl+C.
يخزّن gosentry حالة حملة LibAFL (corpus، الأعطال، إلخ) تحت جذر ذاكرة التخزين المؤقت لـ fuzz في Go (تقريبًا $(go env GOCACHE)/fuzz)، في دليل حتمي مشتق من نفس الحزمة + نفس هدف fuzz (ونفس جذر المشروع).
هذا يعني أن إيقاف (Ctrl+C) وإعادة تشغيل نفس حملة fuzz سيؤدي، افتراضيًا، إلى المتابعة من corpus queue/ السابق لـ LibAFL.
يُطبع المسار في نهاية التشغيل:```text libafl output dir: /full/path/to/.../fuzz//libafl//
ملاحظات:
- `<harness>` هو اسم هدف الـ fuzz عندما يكون `-fuzz` معرّفًا بسيطًا مثل `FuzzXxx` (أو `^FuzzXxx$`)، وإلا فإنه يكون `pattern-<hash>`.
- توليد التغطية (`--generate-coverage`) يستخدم نفس القاعدة للعثور على مجموعة `queue/` الصحيحة، لذا يجب تشغيله من نفس الحزمة مع نفس `-fuzz=...`.
## الميزة 5: التضمين الموجَّه بـ Git-blame (تجريبي)
#### نظرة عامة
التضمين الموجَّه بالتغطية ممتاز في استكشاف مسارات جديدة، لكنه يعامل جميع الأكواد المغطاة على أنها مثيرة للاهتمام بنفس القدر. عند تضمين قواعد أكواد كبيرة، قد ترغب في تحيز أداة التضمين نحو الأكواد المعدلة مؤخرًا، حيث من المرجح أن تُدخل الانحدارات والعلل. في وضع LibAFL، يمكن لـ gosentry استخدام `git blame` لتفضيل المدخلات التي تنفذ أسطرًا تغيرت مؤخرًا (مع الإبقاء على توجيه التغطية كإشارة أساسية).
يستند هذا العمل إلى عمل سابق من [LibAFL-git-aware](https://github.com/kevin-valerio/LibAFL-git-aware). جميع التفاصيل التقنية المعمقة موثقة هناك.
#### طريقة الاستخدام
فعّل الجدولة الموجَّهة بـ git باستخدام `--focus-on-new-code=true`:```bash
./bin/go test -fuzz=FuzzHarness --use-libafl --focus-on-new-code=true --catch-races=false --catch-leaks=false
يتطلب هذا الوضع git (لتشغيل git blame) وgo tool addr2line لتعيين عدادات التغطية مرة أخرى إلى المصدر file:line.
git_recency_map.bin.go.fuzzcntrs هو قسم الرابط الذي يحمل عدادات التغطية 8-بت بنمط libFuzzer الخاصة بلغة Go (مفعّلة عبر -gcflags=all=-d=libfuzzer)؛ كل بايت يمثل "عدد المرات التي تم فيها الوصول إلى هذه النقطة المُجهزة". عند استخدام --focus-on-new-code=true، يقوم golibafl بإنشاء git_recency_map.bin عبر:
go.o من libharness.a..go.fuzzcntrs للحصول على عدد العدادات N..text التي تشير إلى رموز .go.fuzzcntrs لاستعادة العنوان لكل فهرس عداد.file:line باستخدام .تم التنفيذ باستخدام misc/gosentry/bench_focus_on_new_code_geth.sh --trials 5 --warmup 600 --timeout 200.
git-aware results: trial 1: timeout (200000ms) trial 2: crash (87432ms) trial 3: crash (61733ms) trial 4: crash (157540ms) trial 5: crash (7122ms) git-aware crashes: 4/5 (timeouts=1, errors=0) git-aware median (capped to timeout): 87.432s
</details>
## الميزة 6: اكتشاف حالات السباق، وتسريبات الغوروتين، والتعليق (انتهاء المهلة) أثناء وقت التضمين
##### اكتشاف التعليقات المؤكدة (انتهاء مهلة LibAFL)
عند التضمين باستخدام LibAFL، يمكن أن ينتهي تنفيذ الـ harness **بمهلة** (على سبيل المثال بسبب طريق مسدود / غوروتينات عالقة في الانتظار، أو مسار بطيء للغاية).
لتقليل النتائج الإيجابية الخاطئة، يتعامل gosentry مع انتهاء المهلة كمرشح للتعليق ويؤكده بإعادة تشغيل المدخلات التي انتهت مدتها عدة مرات مع مهلة أكبر. عند تأكيد التعليق، يكتب gosentry المدخلات إلى `<libafl output dir>/hangs/` ويوقف حملة التضمين (يتعامل معها كخلل/انهيار).
قبل الخروج، يحاول `golibafl` تصغير المدخلات المتسببة في الانهيار/التعليق (أفضل جهد؛ يتم تحديد التعليقات بحوالي 60 ثانية إجمالاً).
ملاحظة: يتم أيضًا تشغيل تأكيد التعليق أثناء استيراد/توليد المجموعة الأولية، لذا يمكن اكتشاف الأهداف التي تنتهي مهلتها على كل مدخل بشكل حتمي.
يتم تكوين هذا عبر `--libafl-config`:
- `catch_hangs` (الافتراضي: `true`)
- `hang_timeout_ms` (الافتراضي: `10000`)
- `hang_confirm_runs` (الافتراضي: `3`)
##### اكتشاف حالات السباق (`--catch-races`)
يمكن لـ gosentry تشغيل حلقة إعادة تشغيل منفصلة `-race` تراقب دليل LibAFL `queue/` وتعيد تشغيل البذور المكتشفة حديثًا مع `GORACE=halt_on_error=1`.
تقوم حلقة إعادة التشغيل ببناء أرشيف harness منفصل `-race` لإعادة التشغيل فقط (بدون أدوات قياس تغطية التضمين).
عند اكتشاف حالة سباق أثناء إعادة التشغيل، يطبع gosentry تقرير كاشف السباق الكامل قبل ملخص `catch-races:` وأمر إعادة الإنتاج.
ملاحظة: يكتشف كاشف السباق في Go حالات السباق **داخل تنفيذ harness واحد فقط** (سباقات بين غوروتينات في نفس العملية تصل إلى نفس الذاكرة دون مزامنة صحيحة). سيفوّت `--catch-races` السباقات إذا لم تؤدِ البذرة إلى تحفيز التزامن المتسابق، كما أنه لا يكتشف السباقات عبر العمليات.
<details>
<summary><strong>كيف تعمل وضعية كشف السباق</strong></summary>
يبدأ هذا الوضع مراقبًا صغيرًا داخل `go test` (نفس العملية الأب)، ويعمل طوال حملة التضمين بأكملها.
- متى: قبل بدء عملية التضمين الرئيسية في LibAFL، يبني gosentry أداة إعادة التشغيل + المُشغّل.
- المراقبة: قبل بدء التضمين، تلتقط gosentry لقطة للمحتويات الأولية لدليل `<libafl output dir>/queue/` في مجموعة `seen`. ثم يقوم غوروتين باستطلاع `<libafl output dir>/queue/` كل ~1 ثانية ويعيد فقط تشغيل البذور المنشأة حديثًا (يتخطى ملفات dotfiles و `*.metadata`).```text
Legend: output/... = <libafl output dir>/...
┌───────────────────────────────────────────────────────────────────────────┐
│ 1) Main LibAFL fuzzing run │
│ - `golibafl` writes new seeds to `output/queue/` │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 2) `--catch-races` sidecar setup │
│ - builds replay harness: `libharness_race.a` (`go test -race ...`) │
│ - builds replay runner: `golibafl-race` (linked against race harness) │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 3) Replay loop │
│ - polls `output/queue/` for new seeds │
│ - runs: `GORACE=halt_on_error=1 golibafl-race run --input <seed>` │
│ (2 workers × 3 repeats per seed) │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 4) On "DATA RACE" │
│ - prints the race detector report │
│ - copies seed to `output/races/` │
│ - stops the fuzz campaign (treat as bug/crash) │
└───────────────────────────────────────────────────────────────────────────┘
--catch-leaks)يمكن لـ gosentry أيضًا تشغيل حلقة إعادة تشغيل goleak تراقب مجلد queue/ الخاص بـ LibAFL وتعيد تشغيل البذور المكتشفة حديثًا مع تفعيل go.uber.org/goleak.
عند اكتشاف تسريب goroutine، يطبع gosentry المسار الدقيق للبذرة وينسخه إلى <libafl output dir>/leaks/.
ملاحظة: goleak مخصص لتسريبات الـ goroutine، وليس تسريبات الذاكرة.
تبدأ هذه الوضعية أيضًا مراقبًا صغيرًا داخل go test (نفس العملية الأم)، ويعمل طوال حملة التضميد الكاملة.
<libafl output dir>/queue/ كل ~1 ثانية وتعيد تشغيل كل بذرة جديدة مع GOSENTRY_LIBAFL_CATCH_LEAKS=1 (يفعّل go.uber.org/goleak بعد كل تنفيذ).```text
Legend: output/... = /...┌───────────────────────────────────────────────────────────────────────────┐
│ 1) Main LibAFL fuzzing run │
│ - golibafl writes new seeds to output/queue/ │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 2) --catch-leaks sidecar setup │
│ - builds replay runner: golibafl-leak (linked against the harness) │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 3) Replay loop │
│ - polls output/queue/ for new seeds │
│ - runs: │
│ (enables checks after each execution) │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 4) On "catch-leaks: detected goroutine leak" │
│ - copies seed to │
│ - stops the fuzz campaign (treat as bug/crash) │
└───────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────┐
│ 0) gosentry go test -fuzz=FuzzXxx (LibAFL + --use-grammar) │
│ - captures your testing.F.Fuzz callback + its parameter types │
│ - builds libharness.a (libFuzzer-style entrypoints for LibAFL) │
│ - runs golibafl fuzz ... --use-grammar --grammar ... │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 1) golibafl (Rust + LibAFL) fuzzes the Go harness in-process │
│ - loads libharness.a via HARNESS_LIB=... │
│ - observers: edges + time (+ cmplog for comparisons) │
│ - feedback/objective: coverage/time/crash (and optional hang handling) │
│ - scheduler selects a corpus seed (coverage-guided) │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 2) Nautilus (in-process, per client) │
│ - loads the JSON grammar into a Nautilus context │
│ - fuzz loop stage: parse seed -> mutate tree -> unparse to bytes │
│ - if the seed is not parseable: fall back to generation-from-scratch │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 3) Grammar mode stages │
│ - initial corpus: if input dir empty, call N times │
│ - fuzz loop: corpus seed -> grammar mutate -> exec harness │
│ - new coverage inputs are added to the on-disk corpus () │
└───────────────────────────────────────────────────────────────────────────┘
libaflAppendValuelibaflUnmarshalArgs / libaflDecodeValueقواعد الترميز (بشكل عام):
bool: 1 بايت (0 أو 1)int/uint حجمها 8 بايتات)float32 = 4 بايتات، float64 = 8 بايتات)string: uvarint(len) ثم بايتات السلسلة الخام[]byte: uvarint(len) ثم البايتات الخامuvarint(len) ثم ترميز كل عنصر0 = فارغ، 1 = موجود) ثم القيمة المُشار إليهاgo tool addr2linegit blame --line-porcelain للحصول على committer-time لكل سطر.git_recency_map.bin كـ u64 head_time + u64 N + N * u64 timestamps (بترتيب البايتات الأقل أهمية). الإدخالات غير المعيّنة تستخدم الطابع الزمني 0.GOSENTRY_LIBAFL_CATCH_LEAKS=1 golibafl-leak run --input <seed>go.uber.org/goleakoutput/leaks/</details>
#### كيفية الاستخدام
فعّل اكتشاف تسرب الـ goroutine باستخدام `--catch-leaks=true` أو اكتشاف التسابق باستخدام `--catch-races=true````bash
./bin/go test -fuzz=FuzzHarness --use-libafl --focus-on-new-code=false --catch-races=true --catch-leaks=true
التوليد على مستوى البايت رائع، لكن المحلِّلات (parsers) وتنسيقات الملفات غالبًا ما تحتاج إلى مدخلات منظمة. مع --use-grammar، يستخدم gosentry مولّد القواعد النحوية Nautilus من LibAFL لتوليد وتحوير مدخلات تتوافق مع قواعد نحوية يقدمها المستخدم (بتنسيق JSON)، ويغذيها إلى أداة الاختبار التقليدية لـ Go (testing.F.Fuzz).
في وضع القواعد النحوية، لا يزال LibAFL يشغّل حلقة التوجيه بالتغطية المعتادة (اختيار بذرة من المجموعة → تحوير → تنفيذ → الاحتفاظ بالمدخلات التي تزيد التغطية). يضيف المُشغّل (runner) تحوير Nautilus (بذرة → شجرة قواعد نحوية → تحوير → إلغاء البناء النحوي) بالإضافة إلى مرحلة (افتراضيًا) موجَّهة بـ CMPLOG على غرار I2S تعيد كتابة العُقد الطرفية في Nautilus بناءً على المقارنات أثناء التشغيل. وهذا يُبقي المدخلات صالحة نحويًا (فهو لا يشغّل مراحل الفوضى/الرموز الخام على مستوى البايت في وضع القواعد النحوية). يمكنك تعطيل مرحلة CMPLOG/I2S في --libafl-config عبر nautilus_cmplog_i2s=false (التوليد على مستوى البايت يُبقي CMPLOG/I2S مفعّلًا دائمًا).
[!NOTE] وضع القواعد النحوية عادةً ما يكون أبطأ من التوليد على مستوى البايت. إنها مقايضة: بنية أكثر مقابل عدد أقل من عمليات التنفيذ في الثانية.
للحصول على أفضل النتائج، استخدم دالة اختبار (fuzz callback) بوسيط واحد تأخذ إما شريحة بايت ([]byte) أو سلسلة نصية (string):```go
f.Fuzz(func(t testing.T, data []byte) { / parse data */ })
// or:
f.Fuzz(func(t testing.T, s string) { / parse s */ })
يعمل وضع القواعد بشكل أفضل مع وسيط إدخال واحد (`[]byte` أو `string`). تؤدي استدعاءات fuzz متعددة الوسائط إلى جعل gosentry يفك ترميز المخزن المؤقت للبايتات الأساسي إلى قيم منفصلة، لذا لن يبقى النص الأصلي المُولَّد عبر القواعد سليماً.
> [!NOTE]
> وضع القواعد ما زال يولد **بايتات/سلاسل نصية**. إذا كنت تحتاج إلى مدخلات مهيكلة (أو تقوم بتشويش تفاضلي)، فإن الـ harness هو المكان الذي تحوّل فيه `data` إلى قيم المجال (parse/unmarshal). (خارج وضع القواعد، يمكن لـ gosentry أيضًا تشويش أنواع Go > المركبة عن طريق فك ترميزها من البايتات؛ انظر [الميزة 1](#feature-1-struct-aware-fuzzing-fuzz-structs-as-inputs).)
يمكنك ضبط Nautilus عبر `--libafl-config` (يُستخدم فقط مع `--use-grammar`): `nautilus_max_len` و `nautilus_cmplog_i2s` (انظر `misc/gosentry/libafl.config.jsonc`).
<details>
<summary><strong>قياس الأداء: مرحلة Nautilus grammar CMPLOG/I2S (مفعّلة مقابل معطّلة)</strong></summary>
نُفّذ في 17 فبراير 2026 باستخدام مثال JSON الخاص بالقواعد في المستودع (`test/gosentry/examples/grammar_json`, `FuzzGrammarJSON`, grammar `testdata/JSON.json`).
النتائج (LibAFL `UserStats`):
| الوضع | `nautilus_cmplog_i2s` | مدة التشغيل | عدد التنفيذات | تنفيذ/ثانية | الحواف |
|---|---:|---:|---:|---:|---:|
| مفعّل | `true` | 1m-5s | 103818 | 1.586k | 388/8008 (4%) |
| معطّل | `false` | 1m-0s | 256659 | 4.251k | 388/8008 (4%) |
ملاحظة: `edges` هي حواف خريطة التغطية في LibAFL، وليست أسطر مصدر Go.
</details>
عيّن `GOSENTRY_VERBOSE_AFL=1` لطباعة عدد قليل من المدخلات المولّدة. عيّن `GOSENTRY_VERBOSE_AFL_ALL_INPUTS=1` لطباعة **كل** تنفيذ في وضع القواعد بصيغة `GOLIBAFL_MUTATED_INPUT "..."` (ضجيج كبير جدًا).
#### أدوات مساعدة لتأليف القواعد
إذا كنت بحاجة إلى إنشاء قواعد Nautilus JSON جديدة لتنسيق/بروتوكول هدفك الخاص، فإن gosentry يتضمن:
- موجه جاهز لـ LLM: [misc/gosentry/nautilus/prompt.md](https://github.com/trailofbits/gosentry/blob/HEAD/misc/gosentry/nautilus/prompt.md)
- مجموعة صغيرة من أمثلة القواعد: [misc/gosentry/nautilus/examples/](https://github.com/trailofbits/gosentry/blob/HEAD/misc/gosentry/nautilus/examples/)
<details>
<summary><strong>مثال على harness اختبار Go (JSON)</strong></summary>```go
func FuzzGrammarJSON(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
dec := json.NewDecoder(bytes.NewReader(data))
dec.UseNumber()
var v any
if err := dec.Decode(&v); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if err := dec.Decode(&struct{}{}); err != io.EOF {
t.Fatalf("invalid JSON: trailing data")
}
})
}
مخطط منصة اختبار التشويش التفاضلي (محللان):```go f.Fuzz(func(t *testing.T, data []byte) { gotA, errA := ParseA(data) gotB, errB := ParseB(data) if (errA == nil) != (errB == nil) { t.Fatalf("parser disagreement: A=%v B=%v", errA, errB) } _ = gotA _ = gotB })
</details>
<details>
<summary><strong>مثال: اختبار قواعد نحوية لِـ"لغة إدخال حقيقية" (بدون مُرمِّز مخصص)</strong></summary>
يقوم هذا المثال باختبار مُقيِّم تعبيرات حسابية بسيطة من خلال توليد **تعبيرات صالحة** من قاعدة نحوية. لا يوجد ترميز مُخصص بصيغة "بنية إلى بايتات": المُختبر يُنتج نفس النوع من الإدخال الذي يحلّله الكود الخاص بك عادةً.
كود الاختبار (إدخال `string` بوسيط واحد يعمل بشكل أفضل في وضع القواعد النحوية):```go
func FuzzExprEval(f *testing.F) {
f.Add("1+2")
f.Add("(3*4)-5")
f.Fuzz(func(t *testing.T, expr string) {
// Parse+eval your language/protocol.
// You can be **sure** that `expr` will always be a valid math operation. Just decode/parse/unmarshall it afterwards.
_, _ = Eval(expr)
})
}
مخطط القواعد النحوية (صيغة Nautilus JSON):```json [ ["Expr", "{Term}"], ["Expr", "{Term}+{Expr}"], ["Expr", "{Term}-{Expr}"], ["Term", "{Factor}"], ["Term", "{Factor}*{Term}"], ["Factor", "{Num}"], ["Factor", "({Expr})"], ["Num", "0"], ["Num", "1"], ["Num", "2"], ["Num", "3"] ]
</details>
<details>
<summary><strong>مثال لقواعد Nautilus JSON (مجموعة فرعية صغيرة من JSON)</strong></summary>
هذا هو تنسيق الملف المتوقع بواسطة `--grammar=...`:
- القواعد النحوية هي مصفوفة JSON من القواعد: `["NonTerm", "RHS"]`.
- يجب أن تبدأ أسماء الرموز غير الطرفية بحرف كبير (`Value`, `Object`, ...).
- استخدم `{NonTerm}` في RHS للإشارة إلى قاعدة أخرى.
- الرمزان `{` و `}` محجوزان لمراجع الرموز غير الطرفية؛ لإصدار أقواس حرفية، استخدم `\\{` و `\\}` في سلسلة RHS.```json
[
["Json", "{Value}"],
["Value", "null"],
["Value", "{String}"],
["String", "\"{Chars}\""],
["Chars", ""],
["Chars", "{Char}{Chars}"],
["Char", "a"],
["Char", "b"]
]
generateoutput/queue/</details>
القيود (الغراء الحالي):
- وضع القواعد النحوية يعمل بشكل أفضل مع معامل إدخال واحد؛ أهداف التشويش متعددة المعاملات ستفك ترميز مخزن البايت الأساسي إلى قيم منفصلة.
- لا يوجد إعادة تجميع/تقاطع قواعد بين بذور المجموعة حتى الآن (التحوير أحادي البذرة).
## الميزة 8: إنشاء تقارير تغطية Go من حملة التشويش
بعد (أو أثناء) تشغيل حملة تشويش LibAFL، يمكن لـ gosentry إنشاء تقرير تغطية Go عن طريق إعادة تنفيذ **مجموعة الانتظار** الحالية في LibAFL (بدون تشويش).```bash
# Same package + same fuzz target as your fuzz campaign:
./bin/go test -fuzz=FuzzHarness --generate-coverage .
تقوم هذه الأداة بإعادة تشغيل المدخلات من <libafl output dir>/queue/ وتكتب cover.out و cover.html.
تم اكتشاف هذه الأخطاء من خلال حملة تنقيح تفاضلي (differential-fuzzing) باستخدام ميزة التنقيح النحوي في gosentry.