Фармальная верыфікацыя на прыкладзе задачы аб ваўку, казе і капусце

На мой погляд, у рускамоўным сектары інтэрнета тэматыка фармальнай верыфікацыі асветлена недастаткова, і асабліва не хапае простых і наглядных прыкладаў.

Я прывяду такі прыклад з замежнай крыніцы, і дапоўню ўласным рашэннем вядомай задачы аб пераправе ваўка, козы і капусты на іншы бок ракі.

Але спачатку сцісла апішу, што з сябе ўяўляе фармальная верыфікацыя і навошта яна патрэбная.

Пад фармальнай верыфікацыяй звычайна разумеюць праверку адной праграмы або алгарытму з дапамогай іншай.

Гэта трэба для таго, каб пераканацца, што паводзіны праграмы адпавядаюць чаканаму, а таксама гарантуюць яе бяспеку.

Фармальная верыфікацыя з'яўляецца самым магутным сродкам пошуку і ўхіленні ўразлівасцяў: яна дазваляе знайсці ўсе існыя дзюры і багі ў праграме, ці ж давесці, што іх няма.
Варта заўважыць, што ў некаторых выпадках гэта бывае немагчыма, як напрыклад, у задачы аб 8 ферзях з шырынёй дошкі 1000 клетак: усё ўпіраецца ў алгарытмічную складанасць або праблему прыпынку.

Аднак у любым выпадку будзе атрыманы адзін з трох адказаў: праграма карэктная, некарэктная, ці ж - вылічыць адказ не ўдалося.

У выпадку немагчымасці знаходжання адказу, часцяком можна перапрацаваць невыразныя месцы праграмы, паменшыўшы іх алгарытмічную складанасць, для таго каб атрымаць пэўны адказ так ці не.

А прымяняецца фармальная верыфікацыя, напрыклад, у ядры Windows і аперацыйных сістэмах беспілотнікаў Darpa, для забеспячэння максімальнага ўзроўню абароны.

Мы будзем выкарыстоўваць Z3Prover, вельмі магутны інструмент для аўтаматызаванага доказу тэарэм і вырашэння ўраўненні.

Прычым Z3 менавіта вырашае ўраўненні, а не падбірае іх значэння грубым брутфорсам.
Гэта азначае, што ён здольны знаходзіць адказ, нават у выпадках калі камбінацый уваходных варыянтаў і 10^100.

А бо гэта ўсяго толькі каля тузіна ўваходных аргументаў тыпу Integer, і падобнае часцяком сустракаецца на практыку.

Задача аб 8 ферзях (Узяты з англамоўнага мануала).

Фармальная верыфікацыя на прыкладзе задачы аб ваўку, казе і капусце

# We know each queen must be in a different row.
# So, we represent each queen by a single integer: the column position
Q = [ Int('Q_%i' % (i + 1)) for i in range(8) ]

# Each queen is in a column {1, ... 8 }
val_c = [ And(1 <= Q[i], Q[i] <= 8) for i in range(8) ]

# At most one queen per column
col_c = [ Distinct(Q) ]

# Diagonal constraint
diag_c = [ If(i == j,
              True,
              And(Q[i] - Q[j] != i - j, Q[i] - Q[j] != j - i))
           for i in range(8) for j in range(i) ]

solve(val_c + col_c + diag_c)

Запусціўшы Z3, мы атрымліваем рашэнне:

[Q_5 = 1,
 Q_8 = 7,
 Q_3 = 8,
 Q_2 = 2,
 Q_6 = 3,
 Q_4 = 6,
 Q_7 = 5,
 Q_1 = 4]

Задача аб ферзя параўнальная з праграмай, якая прымае на ўваход каардынаты 8 ферзя і выводзіць адказ, ці б'юць ферзі адзін аднаго.

Калі б мы вырашалі такую ​​праграму з дапамогай фармальнай верыфікацыі, то ў параўнанні з задачай, нам бы проста спатрэбілася зрабіць яшчэ адзін крок у выглядзе пераўтварэння кода праграмы ў раўнанне: яно б атрымалася па сваёй сутнасці ідэнтычны нашаму (зразумела, калі праграма напісана беспамылкова).

Практычна тое ж самае будзе адбывацца ў выпадку пошуку ўразлівасцяў: мы толькі задаём патрэбныя нам выходныя ўмовы, напрыклад пароль адміна, пераўтворым зыходны ці дэкампіляваны код у сумяшчальныя з верыфікацыяй раўнанні, і затым атрымліваем адказ, якія дадзеныя трэба падаць на ўваход для дасягнення мэты.

На мой погляд, задача аб ваўку, казе і капусце яшчэ цікавей, бо для яе рашэння трэба ўжо шмат (7) крокаў.

Калі задача аб ферзя параўнальная з варыянтам, калі можна пракрасціся на сервер з дапамогай аднаго GET або POST запыту, то воўк, каза і капуста дэманструе прыклад са значна больш складанай і распаўсюджанай катэгорыі, у якой мэты можна дасягнуць толькі некалькімі запытам.

Гэта параўнальна, напрыклад, са сцэнарам, дзе трэба знайсці SQL ін'екцыю, запісаць праз яе файл, пасля падвысіць свае правы і толькі затым атрымаць пароль.

Умовы задачы і яе разгадкаФермеру трэба перавезці праз раку ваўка, казу і капусту. У фермера ёсць лодка, у якой можа змясціцца, акрамя самога селяніна толькі адзін аб'ект. Воўк з'есць казу, а каза з'есць капусту, калі фермер пакіне іх без нагляду.

Разгадка ў тым, што на 4 кроку фермеру трэба будзе завезці казу назад.
Цяпер прыступім да рашэння праграмным спосабам.

Абазначым фермера, ваўка, казу і капусту як 4 зменныя, якія прымаюць значэнне толькі 0 або 1. Нуль азначае што яны на левым беразе, а адзінка- што на правым.

import json
from z3 import *
s = Solver()
Num= 8

Human = [ Int('Human_%i' % (i + 1)) for i in range(Num) ]
Wolf = [ Int('Wolf_%i' % (i + 1)) for i in range(Num) ]
Goat = [ Int('Goat_%i' % (i + 1)) for i in range(Num) ]
Cabbage = [ Int('Cabbage_%i' % (i + 1)) for i in range(Num) ]

# Each creature can be only on left (0) or right side (1) on every state
HumanSide = [ Or(Human[i] == 0, Human[i] == 1) for i in range(Num) ]
WolfSide = [ Or(Wolf[i] == 0, Wolf[i] == 1) for i in range(Num) ]
GoatSide = [ Or(Goat[i] == 0, Goat[i] == 1) for i in range(Num) ]
CabbageSide = [ Or(Cabbage[i] == 0, Cabbage[i] == 1) for i in range(Num) ]
Side = HumanSide+WolfSide+GoatSide+CabbageSide

Num - гэты лік крокаў неабходных для рашэння. Кожны крок уяўляе сабой стан рэчкі, лодкі і ўсіх сутнасцяў.

Пакуль што выберам яго наўздагад і з запасам, возьмем дзесяць.

Кожная сутнасць прадстаўлена ў 10 экзэмплярах - гэта яе значэнне на кожным з 10 крокаў.

Цяпер зададзім умовы для старту і фінішу.

Start = [ Human[0] == 0, Wolf[0] == 0, Goat[0] == 0, Cabbage[0] == 0 ]
Finish = [ Human[9] == 1, Wolf[9] == 1, Goat[9] == 1, Cabbage[9] == 1 ]

Затым задамо ўмовы, дзе воўк з'ядае казу, ці каза капуста, як абмежаванні ў раўнанні.
(У прысутнасці фермера агрэсія немагчымая)

# Wolf cant stand with goat, and goat with cabbage without human. Not 2, not 0 which means that they are one the same side
Safe = [ And( Or(Wolf[i] != Goat[i], Wolf[i] == Human[i]), Or(Goat[i] != Cabbage[i], Goat[i] == Human[i])) for i in range(Num) ]

І нарэшце, зададзім усе магчымыя дзеянні фермера пры пераправе туды ці зваротна.
Ён можа як узяць з сабой ваўка, казу ці капусту, ці ж нікога не браць, ці ж наогул нікуды не плыць.

Зразумела, што без фермера ніхто пераправіцца не можа.

Гэта будзе выяўленае тым, што кожны наступны стан рэчкі, лодкі і сутнасцяў можа адрознівацца ад папярэдняга толькі строга абмежаванай выявай.

Не больш за на 2 біта, і са мноствам іншых лімітаў, бо фермер можа перавезці за раз толькі адну сутнасць і не ўсіх можна пакінуць разам.

Travel = [ Or(
And(Human[i] == Human[i+1] + 1, Wolf[i] == Wolf[i+1] + 1, Goat[i] == Goat[i+1], Cabbage[i] == Cabbage[i+1]),
And(Human[i] == Human[i+1] + 1, Goat[i] == Goat[i+1] + 1, Wolf[i] == Wolf[i+1], Cabbage[i] == Cabbage[i+1]),
And(Human[i] == Human[i+1] + 1, Cabbage[i] == Cabbage[i+1] + 1, Wolf[i] == Wolf[i+1], Goat[i] == Goat[i+1]),
And(Human[i] == Human[i+1] - 1, Wolf[i] == Wolf[i+1] - 1, Goat[i] == Goat[i+1], Cabbage[i] == Cabbage[i+1]),
And(Human[i] == Human[i+1] - 1, Goat[i] == Goat[i+1] - 1, Wolf[i] == Wolf[i+1], Cabbage[i] == Cabbage[i+1]),
And(Human[i] == Human[i+1] - 1, Cabbage[i] == Cabbage[i+1] - 1, Wolf[i] == Wolf[i+1], Goat[i] == Goat[i+1]),
And(Wolf[i] == Wolf[i+1], Goat[i] == Goat[i+1], Cabbage[i] == Cabbage[i+1])) for i in range(Num-1) ]

Запусцім рашэнне.

solve(Side + Start + Finish + Safe + Travel)

І мы атрымліваем адказ!

Z3 знайшоў несупярэчлівую, і якая задавальняе ўсім умовам сукупнасць станаў.
Гэтакі чатырохмерны злепак прасторы-часу.

Давайце разбяромся, што ж адбылося.

Мы бачым, што ў выніку ўсё пераправіліся, вось толькі спачатку наш фермер вырашыў адпачыць, і нікуды на першых 2 кроках не плыве.

Human_2 = 0
Human_3 = 0

Гэта сведчыць аб тым, што колькасць станаў мы выбралі залішняе, і 8 будзе цалкам дастаткова.

У нашым выпадку фермер паступіў так: старт, адпачынак, адпачынак, пераправа казы, пераправа назад, пераправа капусты, вяртанне з казой, пераправа ваўка, зварот назад у адзіночку, паўторная дастаўка казы.

Але ў выніку задача вырашана.

#Старт.
 Human_1 = 0
 Wolf_1 = 0
 Goat_1 = 0
 Cabbage_1 = 0
 
 #Фермер отдыхает.
 Human_2 = 0
 Wolf_2 = 0
 Goat_2 = 0
 Cabbage_2 = 0
 
 #Фермер отдыхает.
 Human_3 = 0
 Wolf_3 = 0
 Goat_3 = 0
 Cabbage_3 = 0
 
 #Фермер отвозит козу на нужный берег.
 Human_4 = 1
 Wolf_4 = 0
 Goat_4 = 1
 Cabbage_4 = 0
 
 #Фермер возвращается.
 Human_5 = 0
 Wolf_5 = 0
 Goat_5 = 1
 Cabbage_5 = 0
 
 #Фермер отвозит капусту на нужный берег.
 Human_6 = 1
 Wolf_6 = 0
 Cabbage_6 = 1
 Goat_6 = 1
 
 #Ключевая часть операции: фермер возвращает козу обратно.
 Human_7 = 0
 Wolf_7 = 0
 Goat_7 = 0
 Cabbage_7 = 1
 
 #Фермер отвозит волка на другой берег, где он теперь находится вместе с капустой.
 Human_8 = 1
 Wolf_8 = 1
 Goat_8 = 0
 Cabbage_8 = 1
 
 #Фермер возвращается за козой.
 Human_9 = 0
 Wolf_9 = 1
 Goat_9 = 0
 Cabbage_9 = 1
 
 #Фермер повторно доставляет козу на нужный берег и завершают переправу.
 Human_10 = 1
 Wolf_10 = 1
 Goat_10 = 1
 Cabbage_10 = 1

Цяпер паспрабуем памяняць умовы і даказаць, што рашэнняў няма.

Для гэтага мы надзелім нашага ваўка траваеднасцю, і ён захоча з'есці капусту.
Гэта можна параўнаць з выпадкам, у якім наша мэта - абарона прыкладання і мы павінны пераканацца што шчылін няма.

 Safe = [ And( Or(Wolf[i] != Goat[i], Wolf[i] == Human[i]), Or(Goat[i] != Cabbage[i], Goat[i] == Human[i]), Or(Wolf[i] != Cabbage[i], Goat[i] == Human[i])) for i in range(Num) ]

Z3 Выдаў нам наступны адказ:

 no solution

Ён азначае, што рашэньняў сапраўды няма.

Такім чынам мы праграмным спосабам даказалі немагчымасць пераправы са ўсяедным ваўком, без страт для фермера.

Калі аўдыторыя палічыць гэтую тэматыку цікавай, то ў далейшых артыкулах я раскажу, як ператварыць звычайную праграму або функцыю ў сумяшчальнае з фармальнымі метадамі раўнанне, і вырашыць яго, выявіўшы тым самым як усе легітымныя сцэнары, так і ўразлівасці. Спачатку на гэтай жа задачы, але прадстаўленай ужо ў выглядзе праграмы, а затым паступова ўскладняючы і пераходзячы да актуальных прыкладаў са свету распрацоўкі ПЗ.

Наступны артыкул ужо гатовы:
Стварэнне сістэмы фармальнай верыфікацыі з нуля: Пішам знакавую VM на PHP і Python

У ёй я пераходжу ад фармальнай верыфікацыі задач, да праграм, і апісваю,
якім чынам можна канвертаваць іх у сістэмы фармальных правіл аўтаматычна.

Крыніца: habr.com

Дадаць каментар