diff --git a/Makefile b/Makefile index 2b3a093f38..8fc5ef9785 100644 --- a/Makefile +++ b/Makefile @@ -97,7 +97,7 @@ _test_plugins_fail/%.so: _test_plugins_fail/%.go .PHONY: fuzz fuzz: ## run all fuzz tests - for p in $(PACKAGES); do go test -run=NONE -fuzz=Fuzz -fuzztime 30s $$p; done + $(MAKE) -C fuzz $(MAKECMDGOALS) .PHONY: lint lint: build staticcheck ## run all linters diff --git a/fuzz/Dockerfile b/fuzz/Dockerfile new file mode 100644 index 0000000000..648e1ba9d8 --- /dev/null +++ b/fuzz/Dockerfile @@ -0,0 +1,21 @@ +FROM amazonlinux:2023 + +WORKDIR /workspace + +ENV PATH="/root/go/bin:$PATH" + +COPY . . + +RUN dnf install -q -y make clang golang && \ + update-alternatives --install /usr/bin/cc cc /usr/bin/clang 20 && \ + go install github.com/mdempsky/go114-fuzz-build@latest && \ + go mod init fuzz && \ + go mod tidy && \ + make -s -j $(nproc) && \ + dnf autoremove -q -y && \ + dnf clean all -q && \ + go clean -cache -testcache -modcache -fuzzcache + +ENTRYPOINT [ "/usr/bin/make", "fuzz" ] + +CMD ["FUZZ_TARGETS=FuzzParseEskip"] diff --git a/fuzz/Makefile b/fuzz/Makefile new file mode 100644 index 0000000000..08f8662d76 --- /dev/null +++ b/fuzz/Makefile @@ -0,0 +1,64 @@ +MKDIR = mkdir -p +GO_FUZZ_BUILD ?= go114-fuzz-build + +PREFIX ?= $(CURDIR) +ARTIFACTS_PATH = $(PREFIX)/artifacts +DICTIONARY_PATH = $(PREFIX)/dictionaries +CORPUS_PATH = $(PREFIX)/corpus +FUZZ_TARGETS_PATH = $(PREFIX)/fuzz_targets + +FUZZ_TARGETS ?= FuzzParseCIDRs FuzzParseEskip FuzzParseFilters \ + FuzzParseIngressV1JSON FuzzParseIPCIDRs \ + FuzzParseJwt FuzzParsePredicates \ + FuzzParseRouteGroupsJSON FuzzServer + +FUZZ_SANITIZER ?= address +FUZZ_FORKS ?= 2 +FUZZ_MAX_TOTAL_TIME ?= 600 +FUZZ_RUNS ?= -1 +FUZZER_ARGS ?= -artifact_prefix=$(ARTIFACTS_PATH) \ + -rss_limit_mb=2560 -timeout=25 \ + -max_total_time=$(FUZZ_MAX_TOTAL_TIME) \ + -len_control=0 -detect_leaks=0 -max_len=4096 \ + -fork=$(FUZZ_FORKS) -runs=$(FUZZ_RUNS) + +ifeq ($(FUZZ_SANITIZER), address) + FUZZ_BUILD_ARGS = -fsanitize=fuzzer -fsanitize=address -fsanitize-address-use-after-scope +else ifeq ($(FUZZ_SANITIZER), undefined) + FUZZ_BUILD_ARGS = -fsanitize=fuzzer -fsanitize=array-bounds,bool,builtin,enum,float-divide-by-zero,function,integer-divide-by-zero,null,object-size,return,returns-nonnull-attribute,shift,signed-integer-overflow,unsigned-integer-overflow,unreachable,vla-bound,vptr -fno-sanitize-recover=array-bounds,bool,builtin,enum,float-divide-by-zero,function,integer-divide-by-zero,null,object-size,return,returns-nonnull-attribute,shift,signed-integer-overflow,unreachable,vla-bound,vptr +else ifeq ($(FUZZ_SANITIZER), none) + FUZZ_BUILD_ARGS = -fsanitize=fuzzer +else +$(error $(FUZZ_SANITIZER) is invalid sanitizer, available options are address, undefined, none) +endif + +export ASAN_OPTIONS = alloc_dealloc_mismatch=0:allocator_may_return_null=1:allocator_release_to_os_interval_ms=500:check_malloc_usable_size=0:detect_container_overflow=1:detect_odr_violation=0:detect_leaks=1:detect_stack_use_after_return=1:fast_unwind_on_fatal=0:handle_abort=1:handle_segv=1:handle_sigill=1:max_uar_stack_size_log=16:print_scariness=1:quarantine_size_mb=10:strict_memcmp=1:symbolize=1:use_sigaltstack=1:dedup_token_length=3 +export UBSAN_OPTIONS = print_stacktrace=1:print_summary=1:silence_unsigned_overflow=1:symbolize=1:dedup_token_length=3 + +.PHONY: help all fuzz clean +default: all + +help: ## display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +all: $(FUZZ_TARGETS:=.out) ## build all fuzz binaries + +%.a: + $(GO_FUZZ_BUILD) -func $(@:.a=) -o $@ $(FUZZ_TARGETS_PATH) + +gosigfuzz/gosigfuzz.o: + $(CC) -c $(@:.o=.c) -o $@ + +%.out: %.a gosigfuzz/gosigfuzz.o + $(CC) $(FUZZ_BUILD_ARGS) -lresolv $^ -o $@ + +fuzz: $(FUZZ_TARGETS:=.out) ## run all fuzz tests + for TARGET in $(FUZZ_TARGETS); do \ + $(MKDIR) $(CORPUS_PATH)/$$TARGET $(ARTIFACTS_PATH); \ + ARGS="$(FUZZER_ARGS)"; \ + [ -f "$(DICTIONARY_PATH)/$$TARGET.dict" ] && ARGS="$$ARGS -dict=$(DICTIONARY_PATH)/$$TARGET.dict"; \ + $(PREFIX)/$$TARGET.out $$ARGS $(CORPUS_PATH)/$$TARGET; \ + done + +clean: ## clean temporary files and directories + $(RM) $(PREFIX)/*.out $(PREFIX)/*.a $(PREFIX)/*.h $(PREFIX)/main.*.go gosigfuzz/gosigfuzz.o diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 0000000000..d88a5a99b3 --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,3 @@ +# Fuzzing + +This directory contains a set of dictionaries and fuzz targets along with other tooling to fuzz skipper. diff --git a/fuzz/dictionaries/FuzzParseEskip.dict b/fuzz/dictionaries/FuzzParseEskip.dict new file mode 100644 index 0000000000..d03c7a31a3 --- /dev/null +++ b/fuzz/dictionaries/FuzzParseEskip.dict @@ -0,0 +1,188 @@ +"&&" +"*" +"->" +")" +":" +"," +"(" +";" +"" +"" +"" +"<" +">" +"backendIsProxy" +"modRequestHeader" +"setRequestHeader" +"appendRequestHeader" +"dropRequestHeader" +"modResponseHeader" +"setResponseHeader" +"appendResponseHeader" +"dropResponseHeader" +"setContextRequestHeader" +"appendContextRequestHeader" +"setContextResponseHeader" +"appendContextResponseHeader" +"copyRequestHeader" +"copyResponseHeader" +"modPath" +"setPath" +"redirectTo" +"redirectToLower" +"static" +"stripQuery" +"preserveHost" +"status" +"compress" +"decompress" +"setQuery" +"dropQuery" +"inlineContent" +"inlineContentIfStatus" +"flowId" +"xforward" +"xforwardFirst" +"randomContent" +"repeatContent" +"repeatContentHex" +"wrapContent" +"wrapContentHex" +"backendTimeout" +"readTimeout" +"writeTimeout" +"blockContent" +"blockContentHex" +"latency" +"bandwidth" +"chunks" +"backendLatency" +"backendBandwidth" +"backendChunks" +"absorb" +"absorbSilent" +"uniformRequestLatency" +"uniformResponseLatency" +"normalRequestLatency" +"normalResponseLatency" +"histogramRequestLatency" +"histogramResponseLatency" +"logHeader" +"tee" +"teenf" +"teeLoopback" +"sed" +"sedDelim" +"sedRequest" +"sedRequestDelim" +"basicAuth" +"webhook" +"oauthTokeninfoAnyScope" +"oauthTokeninfoAllScope" +"oauthTokeninfoAnyKV" +"oauthTokeninfoAllKV" +"oauthTokenintrospectionAnyClaims" +"oauthTokenintrospectionAllClaims" +"oauthTokenintrospectionAnyKV" +"oauthTokenintrospectionAllKV" +"secureOauthTokenintrospectionAnyClaims" +"secureOauthTokenintrospectionAllClaims" +"secureOauthTokenintrospectionAnyKV" +"secureOauthTokenintrospectionAllKV" +"forwardToken" +"forwardTokenField" +"oauthGrant" +"grantCallback" +"grantLogout" +"grantClaimsQuery" +"jwtValidation" +"oauthOidcUserInfo" +"oauthOidcAnyClaims" +"oauthOidcAllClaims" +"oidcClaimsQuery" +"dropRequestCookie" +"dropResponseCookie" +"requestCookie" +"responseCookie" +"jsCookie" +"consecutiveBreaker" +"rateBreaker" +"disableBreaker" +"admissionControl" +"clientRatelimit" +"ratelimit" +"clusterClientRatelimit" +"clusterRatelimit" +"clusterLeakyBucketRatelimit" +"backendRatelimit" +"ratelimitFailClosed" +"lua" +"corsOrigin" +"headerToQuery" +"queryToHeader" +"disableAccessLog" +"enableAccessLog" +"auditLog" +"unverifiedAuditLog" +"setDynamicBackendHostFromHeader" +"setDynamicBackendSchemeFromHeader" +"setDynamicBackendUrlFromHeader" +"setDynamicBackendHost" +"setDynamicBackendScheme" +"setDynamicBackendUrl" +"apiUsageMonitoring" +"fifo" +"lifo" +"lifoGroup" +"rfcPath" +"rfcHost" +"bearerinjector" +"tracingBaggageToTag" +"stateBagToTag" +"tracingTag" +"tracingTagFromResponse" +"tracingSpanName" +"originMarker" +"fadeIn" +"endpointCreated" +"consistentHashKey" +"consistentHashBalanceFactor" +"opaAuthorizeRequest" +"opaServeResponse" +"healthcheck" +"setFastCgiFilename" +"disableRatelimit" +"unknownRatelimit" +"Path" +"PathSubtree" +"PathRegexp" +"Host" +"HostAny" +"ForwardedHost" +"ForwardedProtocol" +"Weight" +"True" +"False" +"Shutdown" +"Method" +"Methods" +"Header" +"HeaderRegexp" +"Cookie" +"JWTPayloadAnyKV" +"JWTPayloadAllKV" +"JWTPayloadAnyKVRegexp" +"JWTPayloadAllKVRegexp" +"HeaderSHA256" +"After" +"Before" +"Between" +"Cron" +"QueryParam" +"Source" +"SourceFromLast" +"ClientIP" +"Tee" +"Traffic" +"TrafficSegment" +"ContentLengthBetween" \ No newline at end of file diff --git a/fuzz/dictionaries/FuzzServer.dict b/fuzz/dictionaries/FuzzServer.dict new file mode 100644 index 0000000000..203226c193 --- /dev/null +++ b/fuzz/dictionaries/FuzzServer.dict @@ -0,0 +1,217 @@ +# Sources: https://github.com/google/fuzzing/blob/master/dictionaries/http.dict +" *" +" -" +" \"" +"../" +"/.." +"/./" +"//" +"///" +": " +":/" +"; " +"" +"" +"html" +"http" +"HTTP/1.0" +"HTTP/1.1" +"HTTP2-Settings" +"https" +"If-Match" +"If-Modified-Since" +"If-None-Match" +"If-Range" +"If-Unmodified-Since" +"IM" +"keep-alive" +"LABEL" +"Last-Modified" +"Link" +"LINK" +"Location" +"LOCK" +"lockinfo" +"lockscope" +"locktype" +"Lynx" +"Max-Forwards" +"me" +"MERGE" +"mi" +"MKACTIVITY" +"MKCALENDAR" +"MKCOL" +"MKREDIRECTREF" +"MKWORKSPACE" +"MOVE" +"Negotiate" +"nokeepalive" +"only-if-cached" +"OPTIONS" +"ORDERPATCH" +"Origin" +"owner" +"P3P " +"P3P" +"pa" +"PATCH" +"POST" +"Pragma" +"PRI" +"private area" +"PROPFIND" +"PROPPATCH" +"Proxy-Authenticate" +"Proxy-Authorization" +"Proxy-Connection" +"Public-Key-Pins" +"PUT" +"q=0.000" +"Range" +"re" +"REBIND" +"Referer" +"Refresh" +"REPORT" +"Retry-After" +"Save-Data" +"SEARCH" +"secret" +"Server" +"session=" +"Set-Cookie" +"shared" +"Status" +"Strict-Transport-Security" +"TE" +"test" +"Timing-Allow-Origin" +"Tk" +"TRACE" +"Trailer" +"trans" +"Transfer-Encoding" +"UNBIND" +"UNCHECKOUT" +"UNLINK" +"UNLOCK" +"UPDATE" +"UPDATEREDIRECTREF" +"Upgrade" +"Upgrade-Insecure-Requests" +"us" +"User-Agent" +"Vary" +"VERSION-CONTROL" +"Via" +"Warning" +"write" +"WWW-Authenticate" +"*\x00" +"\x0D\x0A" +"\x1F\x8B" +"X-ATT-DeviceId" +"X-Content-Duration" +"X-Content-Security-Policy" +"X-Content-Type-Options" +"X-Correlation-ID" +"X-Csrf-Token" +"X-Forwarded-For" +"X-Forwarded-Host" +"X-Forwarded-Proto" +"X-Frame-Options" +"x-gzip" +"X-Http-Method-Override" +" status(200) -> inlineContent("ok") -> ` + cfg.ApplicationLogLevel = logrus.PanicLevel + cfg.AccessLogDisabled = true + cfg.ApplicationLog = "/dev/null" + cfg.Address = addr + cfg.SupportListener = ":0" + + go func() { + log.Fatal(skipper.Run(cfg.ToOptions())) + }() + + address = cfg.Address +} + +func FuzzServer(data []byte) int { + + if !initialized { + run_server() + initialized = true + } + + conn, err := connect(address) + + if err != nil { + log.Printf("failed to dial: %v\n", err) + return -1 + } + + conn.Write(data) + conn.Close() + + return 1 +} diff --git a/fuzz/gosigfuzz/gosigfuzz.c b/fuzz/gosigfuzz/gosigfuzz.c new file mode 100644 index 0000000000..23f2769a55 --- /dev/null +++ b/fuzz/gosigfuzz/gosigfuzz.c @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Google LLC + + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +// Source: https://raw.githubusercontent.com/google/oss-fuzz/master/infra/base-images/base-builder-go/gosigfuzz.c + +#include +#include + +static void fixSignalHandler(int signum) { + struct sigaction new_action; + struct sigaction old_action; + sigemptyset (&new_action.sa_mask); + sigaction (signum, NULL, &old_action); + new_action.sa_flags = old_action.sa_flags | SA_ONSTACK; + new_action.sa_sigaction = old_action.sa_sigaction; + new_action.sa_handler = old_action.sa_handler; + sigaction (signum, &new_action, NULL); +} + +static void FixStackSignalHandler() { + fixSignalHandler(SIGSEGV); + fixSignalHandler(SIGABRT); + fixSignalHandler(SIGALRM); + fixSignalHandler(SIGINT); + fixSignalHandler(SIGTERM); + fixSignalHandler(SIGBUS); + fixSignalHandler(SIGFPE); + fixSignalHandler(SIGXFSZ); + fixSignalHandler(SIGUSR1); + fixSignalHandler(SIGUSR2); +} + +int LLVMFuzzerInitialize(int *argc, char ***argv) { + FixStackSignalHandler(); + return 0; +}