Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 29 additions & 23 deletions projects/tensorflow-serving/Dockerfile
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
176 changes: 114 additions & 62 deletions projects/tensorflow-serving/build.sh
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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"
133 changes: 133 additions & 0 deletions projects/tensorflow-serving/json_tensor_fuzzer.cc
Original file line number Diff line number Diff line change
@@ -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 <cstddef>
#include <cstdint>
#include <string>

#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<std::string, tensorflow::TensorInfo>* 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<std::string, tensorflow::TensorInfo>* 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<std::string, tensorflow::TensorInfo>* 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<const char*>(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;
}
Loading