diff --git a/projects/tensorflow-serving/Dockerfile b/projects/tensorflow-serving/Dockerfile index 724a242aa781..59f7e70e9590 100644 --- a/projects/tensorflow-serving/Dockerfile +++ b/projects/tensorflow-serving/Dockerfile @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,29 +11,35 @@ # 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. -# -################################################################################ -FROM gcr.io/oss-fuzz-base/base-builder@sha256:19782f7fe8092843368894dbc471ce9b30dd6a2813946071a36e8b05f5b1e27e -# ! This project was pinned after a clang bump. Please remove the pin, Try to fix any build warnings and errors, as well as runtime errors -RUN apt-get update && apt-get install -y make autoconf automake libtool default-jdk bison m4 \ - build-essential\ +FROM gcr.io/oss-fuzz-base/base-builder + +# Install Bazel (TF Serving's build system) +ENV BAZEL_VERSION=6.4.0 +RUN apt-get update && apt-get install -y \ curl \ - doxygen \ - flex \ - libffi-dev \ - libncurses5-dev \ - libtool \ - libsqlite3-dev \ - mcpp \ - sqlite \ - uuid-runtime \ - zlib1g-dev -RUN python3 -m pip install numpy -RUN wget https://github.com/bazelbuild/buildtools/releases/download/4.2.5/buildifier-linux-amd64 \ - -O /usr/local/bin/buildifier && chmod a+x /usr/local/bin/buildifier + git \ + python3 \ + python3-pip \ + unzip \ + wget \ + zip \ + && rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL \ + "https://github.com/bazelbuild/bazel/releases/download/${BAZEL_VERSION}/bazel-${BAZEL_VERSION}-installer-linux-x86_64.sh" \ + -o /tmp/bazel_installer.sh \ + && chmod +x /tmp/bazel_installer.sh \ + && /tmp/bazel_installer.sh \ + && rm /tmp/bazel_installer.sh + +# Clone tensorflow/serving at latest master +RUN git clone --depth=1 https://github.com/tensorflow/serving.git /src/tensorflow-serving + +WORKDIR /src/tensorflow-serving -RUN git clone https://github.com/tensorflow/serving serving +# Copy the fuzz target and build script +COPY json_tensor_fuzzer.cc tensorflow_serving/util/json_tensor_fuzzer.cc +COPY build.sh $SRC/ -COPY build.sh *.diff $SRC/ -WORKDIR $SRC/serving +# Seed corpus: valid predict/classify/regress JSON payloads diff --git a/projects/tensorflow-serving/build.sh b/projects/tensorflow-serving/build.sh index 6e898aa4f6c4..18d5f8b1ab58 100755 --- a/projects/tensorflow-serving/build.sh +++ b/projects/tensorflow-serving/build.sh @@ -1,5 +1,5 @@ #!/bin/bash -eu -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,64 +13,116 @@ # See the License for the specific language governing permissions and # limitations under the License. # -################################################################################ - -synchronize_coverage_directories() { - # For coverage, we need to remap source files to correspond to the Bazel build - # paths. We also need to resolve all symlinks that Bazel creates. - if [ "$SANITIZER" = "coverage" ] - then - declare -r RSYNC_CMD="rsync -aLkR" - declare -r REMAP_PATH=${OUT}/proc/self/cwd/ - mkdir -p ${REMAP_PATH} - - # Synchronize the folder bazel-BAZEL_OUT_PROJECT. - declare -r RSYNC_FILTER_ARGS=("--include" "*.h" "--include" "*.cc" "--include" \ - "*.hpp" "--include" "*.cpp" "--include" "*.c" "--include" "*/" "--include" "*.inc" \ - "--exclude" "*") - - # Sync existing code. - ${RSYNC_CMD} "${RSYNC_FILTER_ARGS[@]}" tensorflow_serving/ ${REMAP_PATH} - - # Sync generated proto files. - if [ -d "./bazel-out/k8-opt/bin/tensorflow/" ] - then - ${RSYNC_CMD} "${RSYNC_FILTER_ARGS[@]}" ./bazel-out/k8-opt/bin/tensorflow_serving/ ${REMAP_PATH} - fi - if [ -d "./bazel-out/k8-opt/bin/external" ] - then - ${RSYNC_CMD} "${RSYNC_FILTER_ARGS[@]}" ./bazel-out/k8-opt/bin/external/ ${REMAP_PATH} - fi - if [ -d "./bazel-out/k8-opt/bin/third_party" ] - then - ${RSYNC_CMD} "${RSYNC_FILTER_ARGS[@]}" ./bazel-out/k8-opt/bin/third_party/ ${REMAP_PATH} - fi - - # Sync external dependencies. We don't need to include `bazel-tensorflow`. - # Also, remove `external/org_tensorflow` which is a copy of the entire source - # code that Bazel creates. Not removing this would cause `rsync` to expand a - # symlink that ends up pointing to itself! - pushd bazel-serving - [[ -e external/org_tensorflow ]] && unlink external/org_tensorflow - ${RSYNC_CMD} external/ ${REMAP_PATH} - popd - fi -} - -git apply --ignore-space-change --ignore-whitespace $SRC/tensorflow-serving.diff - -bazel run @com_google_fuzztest//bazel:setup_configs >> /etc/bazel.bazelrc -bazel build --config=oss-fuzz --subcommands --spawn_strategy=sandboxed //tensorflow_serving/util:json_tensor_test_fuzz - -cp bazel-bin/tensorflow_serving/util/json_tensor_test_fuzz $OUT/json_tensor_test_fuzz - -TARGET_FUZZER="json_tensor_test_fuzz@JsonFuzzTest.TheF" -echo "#!/bin/sh -# LLVMFuzzerTestOneInput for fuzzer detection. -this_dir=\$(dirname \"\$0\") -chmod +x \$this_dir/json_tensor_test_fuzz -\$this_dir/json_tensor_test_fuzz --fuzz=JsonFuzzTest.TheF -- \$@" > $OUT/${TARGET_FUZZER} -chmod +x $OUT/${TARGET_FUZZER} - -# Synchronize coverage folders -synchronize_coverage_directories +# Build script for OSS-Fuzz: compiles the TF Serving JSON tensor fuzzer. + +cd /src/tensorflow-serving + +# ------------------------------------------------------------------------- +# Translate OSS-Fuzz sanitizer flags into Bazel config flags. +# ------------------------------------------------------------------------- +EXTRA_BAZEL_FLAGS="" + +case "$SANITIZER" in + address) + EXTRA_BAZEL_FLAGS="--copt=-fsanitize=address \ + --copt=-fsanitize-address-use-after-scope \ + --linkopt=-fsanitize=address" + ;; + memory) + EXTRA_BAZEL_FLAGS="--copt=-fsanitize=memory \ + --linkopt=-fsanitize=memory" + ;; + undefined) + EXTRA_BAZEL_FLAGS="--copt=-fsanitize=undefined \ + --linkopt=-fsanitize=undefined" + ;; + coverage) + EXTRA_BAZEL_FLAGS="--copt=-fprofile-instr-generate \ + --copt=-fcoverage-mapping \ + --linkopt=-fprofile-instr-generate" + ;; +esac + +# libFuzzer link flag — required for all sanitizer builds. +EXTRA_BAZEL_FLAGS="${EXTRA_BAZEL_FLAGS} \ + --copt=-fsanitize=fuzzer-no-link \ + --linkopt=-fsanitize=fuzzer \ + --copt=-g \ + --strip=never" + +# ------------------------------------------------------------------------- +# Build the fuzzer target using Bazel. +# ------------------------------------------------------------------------- +bazel build \ + --spawn_strategy=standalone \ + --genrule_strategy=standalone \ + --compilation_mode=opt \ + --copt=-O1 \ + --jobs="$(nproc)" \ + --noshow_progress \ + --show_result=0 \ + ${EXTRA_BAZEL_FLAGS} \ + //tensorflow_serving/util:json_tensor_fuzzer + +# ------------------------------------------------------------------------- +# Copy the fuzzer binary to $OUT (required by OSS-Fuzz). +# ------------------------------------------------------------------------- +cp bazel-bin/tensorflow_serving/util/json_tensor_fuzzer \ + "${OUT}/json_tensor_fuzzer" + +# ------------------------------------------------------------------------- +# Package the seed corpus. +# Corpus files are valid JSON payloads representative of each API endpoint. +# ------------------------------------------------------------------------- +mkdir -p /tmp/json_tensor_fuzzer_corpus + +# Predict endpoint — "inputs" (columnar) format +cat > /tmp/json_tensor_fuzzer_corpus/predict_inputs_float.json << 'EOF' +{"inputs": [[1.0, 2.0], [3.0, 4.0]]} +EOF + +# Predict endpoint — "instances" (row) format +cat > /tmp/json_tensor_fuzzer_corpus/predict_instances_float.json << 'EOF' +{"instances": [1.0, 2.0, 3.0]} +EOF + +# Predict endpoint — nested array (triggers GetDenseTensorShape recursion path) +cat > /tmp/json_tensor_fuzzer_corpus/predict_nested.json << 'EOF' +{"inputs": [[[1.0, 2.0], [3.0, 4.0]], [[5.0, 6.0], [7.0, 8.0]]]} +EOF + +# Predict endpoint — base64 bytes +cat > /tmp/json_tensor_fuzzer_corpus/predict_b64.json << 'EOF' +{"inputs": [{"b64": "aGVsbG8="}]} +EOF + +# Predict endpoint — with signature_name +cat > /tmp/json_tensor_fuzzer_corpus/predict_with_sig.json << 'EOF' +{"signature_name": "serving_default", "inputs": [1.0]} +EOF + +# Classify endpoint +cat > /tmp/json_tensor_fuzzer_corpus/classify.json << 'EOF' +{"examples": [{"feature_a": 1.0, "feature_b": [2.0, 3.0]}]} +EOF + +# Classify endpoint — with context +cat > /tmp/json_tensor_fuzzer_corpus/classify_context.json << 'EOF' +{"context": {"global": "foo"}, "examples": [{"feature": 1.0}]} +EOF + +# Regress endpoint +cat > /tmp/json_tensor_fuzzer_corpus/regress.json << 'EOF' +{"examples": [{"x": 1.0}, {"x": 2.0}]} +EOF + +# Empty / edge cases +echo '{}' > /tmp/json_tensor_fuzzer_corpus/empty_object.json +echo '{"inputs": []}' > /tmp/json_tensor_fuzzer_corpus/empty_inputs.json +echo '{"instances": []}' > /tmp/json_tensor_fuzzer_corpus/empty_instances.json + +zip -j "${OUT}/json_tensor_fuzzer_seed_corpus.zip" \ + /tmp/json_tensor_fuzzer_corpus/*.json + +echo "Build complete. Fuzzer: ${OUT}/json_tensor_fuzzer" +echo "Corpus: ${OUT}/json_tensor_fuzzer_seed_corpus.zip" diff --git a/projects/tensorflow-serving/json_tensor_fuzzer.cc b/projects/tensorflow-serving/json_tensor_fuzzer.cc new file mode 100644 index 000000000000..2345504fd6cf --- /dev/null +++ b/projects/tensorflow-serving/json_tensor_fuzzer.cc @@ -0,0 +1,133 @@ +// Copyright 2026 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 +// +// https://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. + +// OSS-Fuzz harness for the TensorFlow Serving JSON tensor parser. +// +// Targets the three HTTP REST API parsing entry points in json_tensor.cc: +// - FillClassificationRequestFromJson (/v1/models/*:classify) +// - FillRegressionRequestFromJson (/v1/models/*:regress) +// - FillPredictRequestFromJson (/v1/models/*:predict) +// +// These functions process untrusted user-supplied HTTP request bodies and are +// the primary attack surface for the TF Serving REST API. CVE-2025-0649 +// (unbounded recursion in GetDenseTensorShape / FillTensorProto) was found in +// this path. This harness ensures continuous regression coverage. + +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "google/protobuf/map.h" +#include "tensorflow/core/framework/tensor_shape.pb.h" +#include "tensorflow/core/framework/types.pb.h" +#include "tensorflow/core/lib/core/status.h" +#include "tensorflow/core/protobuf/meta_graph.pb.h" +#include "tensorflow_serving/apis/classification.pb.h" +#include "tensorflow_serving/apis/predict.pb.h" +#include "tensorflow_serving/apis/regression.pb.h" +#include "tensorflow_serving/util/json_tensor.h" + +namespace { + +// Provides a minimal single-input float tensorinfo_map for the predict path. +// Using DT_FLOAT covers the most common numeric tensor type and exercises the +// full GetDenseTensorShape + FillTensorProto recursion path. +tensorflow::Status MockGetTensorInfoMap( + const std::string& /*signature_name*/, + google::protobuf::Map* map) { + tensorflow::TensorInfo info; + info.set_dtype(tensorflow::DT_FLOAT); + // Unbounded shape — allows any input shape from the fuzz corpus. + info.mutable_tensor_shape()->set_unknown_rank(true); + (*map)["input"] = info; + return tensorflow::OkStatus(); +} + +// Same as above but for DT_STRING inputs (exercises the bytes/b64 path). +tensorflow::Status MockGetStringTensorInfoMap( + const std::string& /*signature_name*/, + google::protobuf::Map* map) { + tensorflow::TensorInfo info; + info.set_dtype(tensorflow::DT_STRING); + info.mutable_tensor_shape()->set_unknown_rank(true); + (*map)["input"] = info; + return tensorflow::OkStatus(); +} + +// Same as above but for DT_INT64 inputs. +tensorflow::Status MockGetInt64TensorInfoMap( + const std::string& /*signature_name*/, + google::protobuf::Map* map) { + tensorflow::TensorInfo info; + info.set_dtype(tensorflow::DT_INT64); + info.mutable_tensor_shape()->set_unknown_rank(true); + (*map)["input"] = info; + return tensorflow::OkStatus(); +} + +} // namespace + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + const absl::string_view json(reinterpret_cast(data), size); + + // --- Classify endpoint --- + // POST /v1/models/{model}:classify + // Exercises MakeExampleFromJsonObject -> AddValueToFeature + { + tensorflow::serving::ClassificationRequest req; + (void)tensorflow::serving::FillClassificationRequestFromJson(json, &req); + } + + // --- Regress endpoint --- + // POST /v1/models/{model}:regress + // Same parsing path as classify. + { + tensorflow::serving::RegressionRequest req; + (void)tensorflow::serving::FillRegressionRequestFromJson(json, &req); + } + + // --- Predict endpoint: float input --- + // POST /v1/models/{model}:predict + // Exercises GetDenseTensorShape + FillTensorProto (the CVE-2025-0649 path). + { + tensorflow::serving::PredictRequest req; + tensorflow::serving::JsonPredictRequestFormat format = + tensorflow::serving::JsonPredictRequestFormat::kInvalid; + (void)tensorflow::serving::FillPredictRequestFromJson( + json, MockGetTensorInfoMap, &req, &format); + } + + // --- Predict endpoint: string/bytes input --- + // Exercises the base64 decode path (JsonDecodeBase64Object). + { + tensorflow::serving::PredictRequest req; + tensorflow::serving::JsonPredictRequestFormat format = + tensorflow::serving::JsonPredictRequestFormat::kInvalid; + (void)tensorflow::serving::FillPredictRequestFromJson( + json, MockGetStringTensorInfoMap, &req, &format); + } + + // --- Predict endpoint: int64 input --- + // Exercises integer parsing and type-checking paths. + { + tensorflow::serving::PredictRequest req; + tensorflow::serving::JsonPredictRequestFormat format = + tensorflow::serving::JsonPredictRequestFormat::kInvalid; + (void)tensorflow::serving::FillPredictRequestFromJson( + json, MockGetInt64TensorInfoMap, &req, &format); + } + + return 0; +} diff --git a/projects/tensorflow-serving/project.yaml b/projects/tensorflow-serving/project.yaml index 57642e96cd85..251ef199f874 100644 --- a/projects/tensorflow-serving/project.yaml +++ b/projects/tensorflow-serving/project.yaml @@ -1,9 +1,22 @@ homepage: "https://github.com/tensorflow/serving" -primary_contact: "david@adalogics.com" -main_repo: "https://github.com/tensorflow/serving" language: c++ +primary_contact: "tensorflow-serving-dev@googlegroups.com" +auto_ccs: + - "tensorflow-security@google.com" +main_repo: "https://github.com/tensorflow/serving.git" + +# TF Serving handles untrusted user input via its HTTP REST API. +# The JSON tensor parser (json_tensor.cc) is the primary attack surface — +# it processes POST request bodies for /predict, /classify, and /regress. +# CVE-2025-0649 (uncontrolled recursion → stack overflow DoS) was found here. + +fuzzing_engines: + - libfuzzer + sanitizers: - address + - memory - undefined -fuzzing_engines: - - libfuzzer + +architectures: + - x86_64