Repository: near/borsh-rs
Pull request #310 (2024-09-23 09:57:36)

chore: prepare 0.10.4 with backported fix
- **feat: Forbid Vectors of Zero-sized types from de-/serialization to resolve the RUSTSEC-2023-0033 (#145)**
- **chore: update MSRV and Cargo workspace syntax**
- **chore: additional prepare for release**


dj8yfo: @race-of-sloths ping
race-of-sloths: @dj8yfo Thank you for your contribution! Your pull request is now a part of the [Race of Sloths](https://race-of-sloths.com)!
Weekly streak is on the road, smart strategy! Secure your streak with another PR!

[<picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://badge.race-of-sloths.com/dj8yfo?type=bot&pr=near/borsh-rs/310&theme=dark">
    <source media="(prefers-color-scheme: light)" srcset="https://badge.race-of-sloths.com/dj8yfo?type=bot&pr=near/borsh-rs/310&theme=light">
    <img alt="Shows profile picture for the author of the PR" src="https://badge.race-of-sloths.com/dj8yfo?type=bot&pr=near/borsh-rs/310&theme=white">
</picture>](https://race-of-sloths.com/profile/dj8yfo)


<details><summary>Current status: <i>waiting for scoring</i></summary>

We're waiting for maintainer to score this pull request with `@race-of-sloths score [0,1,2,3,5,8,13]` command. Alternatively, autoscoring [1,2] will be applied for this pull request

</details><details><summary>What is the Race of Sloths</summary>
<p>
Race of Sloths is a friendly competition where you can participate in challenges and compete with other open-source contributors within your normal workflow

For contributors:

- Tag @race-of-sloths inside your pull requests
- Wait for the maintainer to review and score your pull request
- Check out your position in the [Leaderboard](https://race-of-sloths.com/leaderboard)
- Keep weekly and monthly streaks to reach higher positions
- Boast your contributions with a dynamic picture of your [Profile](https://race-of-sloths.com/profile/dj8yfo)

For maintainers:
- Score pull requests that participate in the Race of Sloths
- Engage contributors with fair scoring and fast responses so they keep their streaks
- Promote the Race to the point where the Race starts promoting you
- Grow the community of your contributors

Feel free to check [our website](https://race-of-sloths.com) for additional details!
</p>
</details> 

<details>
<summary>Bot commands</summary>

- For contributors
  - **Include a PR:** `@race-of-sloths include` to enter the Race with your PR
- For maintainers:
  - **Invite contributor** `@race-of-sloths invite` to invite the contributor to participate in a race or include it, if it's already a runner.
  - **Assign points:** `@race-of-sloths score [1/2/3/5/8/13]` to award points based on your assessment.
  - **Reject this PR:** `@race-of-sloths exclude` to send this PR back to the drawing board.
  - **Exclude repo:** `@race-of-sloths pause` to stop bot activity in this repo until `@race-of-sloths unpause` command is called

</details>

frol: @race-of-sloths score 8

Patch:
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index f5a5c4c4..ae963cb2 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -2,13 +2,13 @@ name: Rust
 
 on:
   push:
-    branches: [ master ]
+    branches: [ 0.10.x ]
   pull_request:
-    branches: [ master ]
+    branches: [ 0.10.x ]
 
 env:
   CARGO_TERM_COLOR: always
-  RUSTFLAGS: '-D warnings'
+  # RUSTFLAGS: '-D warnings'
   CARGO_INCREMENTAL: 0
   RUST_BACKTRACE: short
 
@@ -46,71 +46,21 @@ jobs:
     - name: Run cargo fmt
       run: cargo fmt --check
 
-  publish:
-    runs-on: ubuntu-20.04
+  release-plz:
+    runs-on: ubuntu-latest
     needs: [tests, clippy, cargo-fmt]
-    if: github.ref == 'refs/heads/master'
-
+    if: github.ref == 'refs/heads/0.10.x'
     steps:
-    - uses: actions/checkout@v2
-      with:
-        # fetch tags for cargo ws publish
-        # might be a simple `fetch-tags: true` option soon, see https://github.com/actions/checkout/pull/579
-        fetch-depth: 0
-
-    - name: Setup
-      run: |
-        git config user.name github-actions
-        git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
-        cargo install --git https://github.com/miraclx/cargo-workspaces --rev b2d49b9e575e29fd2395352e4d0df47def025039 cargo-workspaces
-        export GIT_PREVIOUS_TAG=$(git describe --tags --abbrev=0)
-        echo "GIT_PREVIOUS_TAG=${GIT_PREVIOUS_TAG}" >> $GITHUB_ENV
-        echo "[ pre run] current latest git tag is \"${GIT_PREVIOUS_TAG}\""
-
-    - name: Publish to crates.io and tag the commit
-      id: tag-and-publish
-      env:
-        CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
-      run: |
-        cargo ws publish --all --yes --exact --force '*' \
-          --skip-published --no-git-commit --allow-dirty \
-          --tag-existing --tag-prefix 'v' \
-          --tag-msg 'crates.io snapshot' --tag-msg $'%{\n - %n: https://crates.io/crates/%n/%v}' \
-          --no-individual-tags --no-git-push
-        export GIT_LATEST_TAG=$(git describe --tags --abbrev=0)
-        echo "GIT_LATEST_TAG=${GIT_LATEST_TAG}" >> $GITHUB_ENV
-        echo "[post run] current latest git tag is \"${GIT_LATEST_TAG}\""
-        echo "::set-output name=tagged::$( [[ "$GIT_LATEST_TAG" == "$GIT_PREVIOUS_TAG" ]] && echo 0 || echo 1 )"
-
-        # returning multi-line outputs gets truncated to include only the first line
-        # we have to escape the newline chars, runner auto unescapes them later
-        # https://github.community/t/set-output-truncates-multiline-strings/16852/3
-        GIT_TAG_MESSAGE="$(git tag -l --format='%(body)' ${GIT_LATEST_TAG})"
-        GIT_TAG_MESSAGE="${GIT_TAG_MESSAGE//'%'/'%25'}"
-        GIT_TAG_MESSAGE="${GIT_TAG_MESSAGE//$'\n'/'%0A'}"
-        GIT_TAG_MESSAGE="${GIT_TAG_MESSAGE//$'\r'/'%0D'}"
-        echo "::set-output name=git_tag_message::${GIT_TAG_MESSAGE}"
-
-    - name: Push tags to GitHub (if any)
-      if: steps.tag-and-publish.outputs.tagged == 1
-      run: git push --tags
-
-    - name: Extract release notes
-      if: steps.tag-and-publish.outputs.tagged == 1
-      id: extract-release-notes
-      uses: ffurrer2/extract-release-notes@c24866884b7a0d2fd2095be2e406b6f260479da8
-
-    - name: Create release
-      if: steps.tag-and-publish.outputs.tagged == 1
-      uses: actions/create-release@v1
-      env:
-        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      with:
-        tag_name: ${{ env.GIT_LATEST_TAG }}
-        release_name: ${{ env.GIT_LATEST_TAG }}
-        body: |
-          ${{ steps.extract-release-notes.outputs.release_notes }}
-
-          #### Crate Links
-
-          ${{ steps.tag-and-publish.outputs.git_tag_message }}
+      - name: Checkout repository
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+          token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
+      - name: Install Rust toolchain
+        uses: dtolnay/rust-toolchain@stable
+      - name: Run release-plz
+        uses: MarcoIeni/release-plz-action@v0.5
+        env:
+          # https://marcoieni.github.io/release-plz/github-action.html#triggering-further-workflow-runs
+          GITHUB_TOKEN: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
+          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
diff --git a/Cargo.toml b/Cargo.toml
index d35274f3..4714b6ec 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,11 +4,10 @@ members = [
     "borsh-derive",
     "borsh-derive-internal",
     "borsh-schema-derive-internal",
-    "fuzz/fuzz-run",
-    "benchmarks",
 ]
+exclude = [ "fuzz/fuzz-run", "benchmarks" ]
 
-[workspace.metadata.workspaces]
+[workspace.package]
 # shared version of all public crates in the workspace
 version = "0.10.3"
-exclude = [ "fuzz/*", "benchmarks" ]
+rust-version = "1.66.0"
diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml
index 450bcda2..f48bc623 100644
--- a/benchmarks/Cargo.toml
+++ b/benchmarks/Cargo.toml
@@ -13,7 +13,9 @@ bench = false
 rand_xorshift = "0.2.0"
 rand = "0.7.0"
 borsh = { path = "../borsh", default-features = false }
-serde = { version = "1.0", features = ["derive"] }
+# Building with serde 1.0.204 breaks due to the use of ‘diagnostic’
+# attribute.  Remove once MSRV is updated.
+serde = { version = "1.0, <1.0.204", features = ["derive"] }
 speedy-derive = "0.5"
 speedy = "0.5"
 
diff --git a/borsh-derive-internal/Cargo.toml b/borsh-derive-internal/Cargo.toml
index 53b14e52..7670a8a7 100644
--- a/borsh-derive-internal/Cargo.toml
+++ b/borsh-derive-internal/Cargo.toml
@@ -1,6 +1,7 @@
 [package]
 name = "borsh-derive-internal"
-version = "0.0.0"
+version.workspace = true
+rust-version.workspace = true
 authors = ["Near Inc <hello@nearprotocol.com>"]
 edition = "2018"
 license = "Apache-2.0"
diff --git a/borsh-derive/Cargo.toml b/borsh-derive/Cargo.toml
index 923059f8..7a04e1b5 100644
--- a/borsh-derive/Cargo.toml
+++ b/borsh-derive/Cargo.toml
@@ -1,6 +1,7 @@
 [package]
 name = "borsh-derive"
-version = "0.0.0"
+version.workspace = true
+rust-version.workspace = true
 authors = ["Near Inc <hello@nearprotocol.com>"]
 edition = "2018"
 license = "Apache-2.0"
@@ -16,8 +17,8 @@ Binary Object Representation Serializer for Hashing
 proc-macro = true
 
 [dependencies]
-borsh-derive-internal = { path = "../borsh-derive-internal" }
-borsh-schema-derive-internal = { path = "../borsh-schema-derive-internal" }
+borsh-derive-internal = { version = "0.10.3", path = "../borsh-derive-internal" }
+borsh-schema-derive-internal = { version = "0.10.3", path = "../borsh-schema-derive-internal" }
 syn = {version = "1", features = ["full", "fold"] }
 proc-macro-crate = "0.1.5"
 proc-macro2 = "1"
diff --git a/borsh-schema-derive-internal/Cargo.toml b/borsh-schema-derive-internal/Cargo.toml
index 9e3ce7c2..9fcf7420 100644
--- a/borsh-schema-derive-internal/Cargo.toml
+++ b/borsh-schema-derive-internal/Cargo.toml
@@ -1,6 +1,7 @@
 [package]
 name = "borsh-schema-derive-internal"
-version = "0.0.0"
+version.workspace = true
+rust-version.workspace = true
 authors = ["Near Inc <hello@nearprotocol.com>"]
 edition = "2018"
 license = "Apache-2.0"
diff --git a/borsh/Cargo.toml b/borsh/Cargo.toml
index 141609ad..fdbc4ef2 100644
--- a/borsh/Cargo.toml
+++ b/borsh/Cargo.toml
@@ -1,6 +1,7 @@
 [package]
 name = "borsh"
-version = "0.0.0"
+version.workspace = true
+rust-version.workspace = true
 authors = ["Near Inc <hello@near.org>"]
 edition = "2018"
 license = "MIT OR Apache-2.0"
@@ -21,7 +22,7 @@ name = "generate_schema_schema"
 path = "src/generate_schema_schema.rs"
 
 [dependencies]
-borsh-derive = { path = "../borsh-derive" }
+borsh-derive = { version = "0.10.3", path = "../borsh-derive" }
 hashbrown = ">=0.11,<0.14"
 bytes = { version = "1", optional = true }
 
diff --git a/borsh/src/de/mod.rs b/borsh/src/de/mod.rs
index 862b1632..9e667c30 100644
--- a/borsh/src/de/mod.rs
+++ b/borsh/src/de/mod.rs
@@ -3,7 +3,7 @@ use core::mem::MaybeUninit;
 use core::{
     convert::{TryFrom, TryInto},
     hash::{BuildHasher, Hash},
-    mem::{forget, size_of},
+    mem::size_of,
 };
 
 #[cfg(any(test, feature = "bytes"))]
@@ -377,21 +377,18 @@ where
 {
     #[inline]
     fn deserialize_reader<R: Read>(reader: &mut R) -> Result<Self> {
+        if size_of::<T>() == 0 {
+            return Err(Error::new(
+                ErrorKind::InvalidInput,
+                "Vectors of zero-sized types are not allowed due to deny-of-service concerns on deserialization.",
+            ));
+        }
+
         let len = u32::deserialize_reader(reader)?;
         if len == 0 {
             Ok(Vec::new())
         } else if let Some(vec_bytes) = T::vec_from_reader(len, reader)? {
             Ok(vec_bytes)
-        } else if size_of::<T>() == 0 {
-            let mut result = vec![T::deserialize_reader(reader)?];
-
-            let p = result.as_mut_ptr();
-            unsafe {
-                forget(result);
-                let len = len.try_into().map_err(|_| ErrorKind::InvalidInput)?;
-                let result = Vec::from_raw_parts(p, len, len);
-                Ok(result)
-            }
         } else {
             // TODO(16): return capacity allocation when we can safely do that.
             let mut result = Vec::with_capacity(hint::cautious::<T>(len));
@@ -770,6 +767,6 @@ where
 
 impl<T: ?Sized> BorshDeserialize for PhantomData<T> {
     fn deserialize_reader<R: Read>(_: &mut R) -> Result<Self> {
-        Ok(Self::default())
+        Ok(PhantomData)
     }
 }
diff --git a/borsh/src/ser/mod.rs b/borsh/src/ser/mod.rs
index 56eb628c..56697fbf 100644
--- a/borsh/src/ser/mod.rs
+++ b/borsh/src/ser/mod.rs
@@ -1,12 +1,13 @@
 use core::convert::TryFrom;
 use core::hash::BuildHasher;
 use core::marker::PhantomData;
+use core::mem::size_of;
 
 use crate::maybestd::{
     borrow::{Cow, ToOwned},
     boxed::Box,
     collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, LinkedList, VecDeque},
-    io::{ErrorKind, Result, Write},
+    io::{Error, ErrorKind, Result, Write},
     string::String,
     vec::Vec,
 };
@@ -267,6 +268,12 @@ where
 {
     #[inline]
     fn serialize<W: Write>(&self, writer: &mut W) -> Result<()> {
+        if size_of::<T>() == 0 {
+            return Err(Error::new(
+                ErrorKind::InvalidInput,
+                "Vectors of zero-sized types are not allowed due to deny-of-service concerns on deserialization.",
+            ));
+        }
         self.as_slice().serialize(writer)
     }
 }
diff --git a/fuzz/fuzz-run/Cargo.toml b/fuzz/fuzz-run/Cargo.toml
index c926c432..285ce25b 100644
--- a/fuzz/fuzz-run/Cargo.toml
+++ b/fuzz/fuzz-run/Cargo.toml
@@ -12,3 +12,8 @@ path = "src/main.rs"
 [dependencies]
 honggfuzz = "0.5"
 borsh = { path = "../../borsh" }
+# This is transitive dependency specified here only to limit the
+# version.  We need to limit the version because building
+# serde 1.0.204 breaks due to the use of ‘diagnostic’ attribute.  Drop
+# this dependency once MSRV is updated.
+serde = "1.0, <1.0.204"
diff --git a/release-plz.toml b/release-plz.toml
new file mode 100644
index 00000000..ed4ab1dc
--- /dev/null
+++ b/release-plz.toml
@@ -0,0 +1,16 @@
+[workspace]
+# Use `borsh` crate CHANGELOG as top-level one
+changelog_update = false
+
+[[package]]
+name = "borsh"
+changelog_update = true
+changelog_path = "./CHANGELOG.md"
+
+[[package]]
+name = "borsh-fuzz"
+publish = false
+
+[[package]]
+name = "benchmarks"
+publish = false


Test patch:
diff --git a/borsh/tests/test_zero_size.rs b/borsh/tests/test_zero_size.rs
index b47943d4..ca56b007 100644
--- a/borsh/tests/test_zero_size.rs
+++ b/borsh/tests/test_zero_size.rs
@@ -1,11 +1,36 @@
+use borsh::to_vec;
 use borsh::BorshDeserialize;
+use borsh::BorshSerialize;
 
-#[derive(BorshDeserialize, PartialEq, Debug)]
-struct A;
+#[derive(BorshDeserialize, BorshSerialize, PartialEq, Debug)]
+struct A();
 
 #[test]
-fn test_deserialize_vector_to_many_zero_size_struct() {
+fn test_deserialize_zero_size() {
     let v = [0u8, 0u8, 0u8, 64u8];
-    let a = Vec::<A>::try_from_slice(&v).unwrap();
-    assert_eq!(A {}, a[usize::pow(2, 30) - 1])
+    let res = Vec::<A>::try_from_slice(&v);
+    assert!(res.is_err());
+}
+
+#[test]
+fn test_serialize_zero_size() {
+    let v = vec![A()];
+    let res = to_vec(&v);
+    assert!(res.is_err());
+}
+
+#[derive(BorshDeserialize, BorshSerialize, PartialEq, Debug)]
+struct B(u32);
+#[test]
+fn test_deserialize_non_zero_size() {
+    let v = [1, 0, 0, 0, 64, 0, 0, 0];
+    let res = Vec::<B>::try_from_slice(&v);
+    assert!(res.is_ok());
+}
+
+#[test]
+fn test_serialize_non_zero_size() {
+    let v = vec![B(1)];
+    let res = to_vec(&v);
+    assert!(res.is_ok());
 }