diff --git a/Cargo.lock b/Cargo.lock
index f50e45a..e731adf 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1043,6 +1043,7 @@ dependencies = [
  "rlimit",
  "tempfile",
  "textwrap",
+ "uu_blockdev",
  "uu_ctrlaltdel",
  "uu_dmesg",
  "uu_fsfreeze",
@@ -1056,6 +1057,17 @@ dependencies = [
  "xattr",
 ]
 
+[[package]]
+name = "uu_blockdev"
+version = "0.0.1"
+dependencies = [
+ "clap",
+ "linux-raw-sys 0.7.0",
+ "regex",
+ "sysinfo",
+ "uucore",
+]
+
 [[package]]
 name = "uu_ctrlaltdel"
 version = "0.0.1"
diff --git a/Cargo.toml b/Cargo.toml
index fd8d36e..406921d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,6 +26,7 @@ default = ["feat_common_core"]
 uudoc = []
 
 feat_common_core = [
+  "blockdev",
   "ctrlaltdel",
   "dmesg",
   "fsfreeze",
@@ -67,6 +68,7 @@ phf = { workspace = true }
 textwrap = { workspace = true }
 uucore = { workspace = true }
 
+blockdev = { optional = true, version = "0.0.1", package = "uu_blockdev", path = "src/uu/blockdev" }
 ctrlaltdel = { optional = true, version = "0.0.1", package = "uu_ctrlaltdel", path = "src/uu/ctrlaltdel" }
 dmesg = { optional = true, version = "0.0.1", package = "uu_dmesg", path = "src/uu/dmesg" }
 fsfreeze = { optional = true, version = "0.0.1", package = "uu_fsfreeze", path = "src/uu/fsfreeze" }
diff --git a/src/uu/blockdev/Cargo.toml b/src/uu/blockdev/Cargo.toml
new file mode 100644
index 0000000..eb81469
--- /dev/null
+++ b/src/uu/blockdev/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "uu_blockdev"
+version = "0.0.1"
+edition = "2021"
+description = "blockdev ~ Get or set various block device attributes."
+
+[lib]
+path = "src/blockdev.rs"
+
+[[bin]]
+name = "blockdev"
+path = "src/main.rs"
+
+[dependencies]
+clap = { workspace = true }
+linux-raw-sys = { workspace = true }
+regex = { workspace = true }
+sysinfo = { workspace = true }
+uucore = { workspace = true }
diff --git a/src/uu/blockdev/blockdev.md b/src/uu/blockdev/blockdev.md
new file mode 100644
index 0000000..a520424
--- /dev/null
+++ b/src/uu/blockdev/blockdev.md
@@ -0,0 +1,8 @@
+# blockdev
+
+```
+blockdev <COMMAND...> <DEVICE...>
+blockdev --report <DEVICE...>
+```
+
+Get or set various block device attributes.
diff --git a/src/uu/blockdev/src/blockdev.rs b/src/uu/blockdev/src/blockdev.rs
new file mode 100644
index 0000000..aabff89
--- /dev/null
+++ b/src/uu/blockdev/src/blockdev.rs
@@ -0,0 +1,432 @@
+// This file is part of the uutils util-linux package.
+//
+// For the full copyright and license information, please view the LICENSE
+// file that was distributed with this source code.
+
+use clap::{crate_version, value_parser, Arg, ArgAction, Command};
+use linux_raw_sys::ioctl::*;
+#[cfg(target_os = "linux")]
+use std::collections::BTreeMap;
+#[cfg(target_os = "linux")]
+use uucore::error::USimpleError;
+use uucore::{error::UResult, format_usage, help_about, help_usage};
+
+const ABOUT: &str = help_about!("blockdev.md");
+const USAGE: &str = help_usage!("blockdev.md");
+
+#[derive(Copy, Clone, Debug)]
+enum IoctlArgType {
+    Short,
+    Int,
+    Long,
+    U64Sectors,
+    U64,
+}
+
+#[allow(dead_code)]
+#[derive(Clone, Debug)]
+enum IoctlCommand {
+    GetAttribute(IoctlArgType),
+    SetAttribute,
+    Operation(u32),
+}
+
+#[allow(dead_code)]
+#[derive(Clone, Debug)]
+enum BlockdevCommand {
+    SetVerbosity(bool),
+    Ioctl(&'static str, u32, IoctlCommand),
+}
+
+const BLOCKDEV_ACTIONS: &[(&str, BlockdevCommand)] = &[
+    ("verbose", BlockdevCommand::SetVerbosity(true)),
+    ("quiet", BlockdevCommand::SetVerbosity(false)),
+    (
+        "flushbufs",
+        BlockdevCommand::Ioctl("flush buffers", BLKFLSBUF, IoctlCommand::Operation(0)),
+    ),
+    (
+        "getalignoff",
+        BlockdevCommand::Ioctl(
+            "get alignment offset in bytes",
+            BLKALIGNOFF,
+            IoctlCommand::GetAttribute(IoctlArgType::Int),
+        ),
+    ),
+    (
+        "getbsz",
+        BlockdevCommand::Ioctl(
+            "get blocksize",
+            BLKBSZGET,
+            IoctlCommand::GetAttribute(IoctlArgType::Int),
+        ),
+    ),
+    (
+        "getdiscardzeroes",
+        BlockdevCommand::Ioctl(
+            "get discard zeroes support status",
+            BLKDISCARDZEROES,
+            IoctlCommand::GetAttribute(IoctlArgType::Int),
+        ),
+    ),
+    (
+        "getfra",
+        BlockdevCommand::Ioctl(
+            "get filesystem readahead",
+            BLKFRAGET,
+            IoctlCommand::GetAttribute(IoctlArgType::Long),
+        ),
+    ),
+    (
+        "getiomin",
+        BlockdevCommand::Ioctl(
+            "get minimum I/O size",
+            BLKIOMIN,
+            IoctlCommand::GetAttribute(IoctlArgType::Int),
+        ),
+    ),
+    (
+        "getioopt",
+        BlockdevCommand::Ioctl(
+            "get optimal I/O size",
+            BLKIOOPT,
+            IoctlCommand::GetAttribute(IoctlArgType::Int),
+        ),
+    ),
+    (
+        "getmaxsect",
+        BlockdevCommand::Ioctl(
+            "get max sectors per request",
+            BLKSECTGET,
+            IoctlCommand::GetAttribute(IoctlArgType::Short),
+        ),
+    ),
+    (
+        "getpbsz",
+        BlockdevCommand::Ioctl(
+            "get physical block (sector) size",
+            BLKPBSZGET,
+            IoctlCommand::GetAttribute(IoctlArgType::Int),
+        ),
+    ),
+    (
+        "getra",
+        BlockdevCommand::Ioctl(
+            "get readahead",
+            BLKRAGET,
+            IoctlCommand::GetAttribute(IoctlArgType::Long),
+        ),
+    ),
+    (
+        "getro",
+        BlockdevCommand::Ioctl(
+            "get read-only",
+            BLKROGET,
+            IoctlCommand::GetAttribute(IoctlArgType::Int),
+        ),
+    ),
+    (
+        "getsize64",
+        BlockdevCommand::Ioctl(
+            "get size in bytes",
+            BLKGETSIZE64,
+            IoctlCommand::GetAttribute(IoctlArgType::U64),
+        ),
+    ),
+    (
+        "getsize",
+        BlockdevCommand::Ioctl(
+            "get 32-bit sector count (deprecated, use --getsz)",
+            BLKGETSIZE,
+            IoctlCommand::GetAttribute(IoctlArgType::Long),
+        ),
+    ),
+    (
+        "getss",
+        BlockdevCommand::Ioctl(
+            "get logical block (sector) size",
+            BLKSSZGET,
+            IoctlCommand::GetAttribute(IoctlArgType::Int),
+        ),
+    ),
+    (
+        "getsz",
+        BlockdevCommand::Ioctl(
+            "get size in 512-byte sectors",
+            BLKGETSIZE64,
+            IoctlCommand::GetAttribute(IoctlArgType::U64Sectors),
+        ),
+    ),
+    (
+        "rereadpt",
+        BlockdevCommand::Ioctl(
+            "reread partition table",
+            BLKRRPART,
+            IoctlCommand::Operation(0),
+        ),
+    ),
+    (
+        "setbsz",
+        BlockdevCommand::Ioctl("set blocksize", BLKBSZSET, IoctlCommand::SetAttribute),
+    ),
+    (
+        "setfra",
+        BlockdevCommand::Ioctl(
+            "set filesystem readahead",
+            BLKFRASET,
+            IoctlCommand::SetAttribute,
+        ),
+    ),
+    (
+        "setra",
+        BlockdevCommand::Ioctl("set readahead", BLKRASET, IoctlCommand::SetAttribute),
+    ),
+    (
+        "setro",
+        BlockdevCommand::Ioctl("set read-only", BLKROSET, IoctlCommand::Operation(1)),
+    ),
+    (
+        "setrw",
+        BlockdevCommand::Ioctl("set read-write", BLKROSET, IoctlCommand::Operation(0)),
+    ),
+];
+
+#[cfg(target_os = "linux")]
+mod linux {
+    use crate::*;
+    use std::{fs::File, io, os::fd::AsRawFd};
+    use std::{io::Read, os::unix::fs::MetadataExt, path::Path};
+    use uucore::{error::UIoError, libc};
+
+    unsafe fn uu_ioctl<T>(device_file: &File, ioctl_code: u32, input: T) -> UResult<()> {
+        if libc::ioctl(device_file.as_raw_fd(), ioctl_code.into(), input) < 0 {
+            Err(Box::new(UIoError::from(io::Error::last_os_error())))
+        } else {
+            Ok(())
+        }
+    }
+
+    unsafe fn get_ioctl_attribute(
+        device_file: &File,
+        ioctl_code: u32,
+        ioctl_type: IoctlArgType,
+    ) -> UResult<u64> {
+        unsafe fn ioctl_get<T: Default + Into<u64>>(
+            device: &File,
+            ioctl_code: u32,
+        ) -> UResult<u64> {
+            let mut retval: T = Default::default();
+            uu_ioctl(device, ioctl_code, &mut retval as *mut T as usize).map(|_| retval.into())
+        }
+
+        match ioctl_type {
+            IoctlArgType::Int => ioctl_get::<libc::c_uint>(device_file, ioctl_code),
+            IoctlArgType::Long => ioctl_get::<libc::c_ulong>(device_file, ioctl_code),
+            IoctlArgType::Short => ioctl_get::<libc::c_ushort>(device_file, ioctl_code),
+            IoctlArgType::U64 => ioctl_get::<u64>(device_file, ioctl_code),
+            IoctlArgType::U64Sectors => Ok(ioctl_get::<u64>(device_file, ioctl_code)? / 512),
+        }
+    }
+
+    fn get_partition_offset(device_file: &File) -> UResult<usize> {
+        let rdev = device_file.metadata()?.rdev();
+        let major = unsafe { libc::major(rdev) };
+        let minor = unsafe { libc::minor(rdev) };
+        if Path::new(&format!("/sys/dev/block/{}:{}/partition", major, minor)).exists() {
+            let mut start_fd = File::open(format!("/sys/dev/block/{}:{}/start", major, minor))?;
+            let mut str = String::new();
+            start_fd.read_to_string(&mut str)?;
+            return str
+                .trim()
+                .parse()
+                .map_err(|_| USimpleError::new(1, "Unable to parse partition start offset"));
+        }
+        Ok(0)
+    }
+
+    pub fn do_report(device_path: &str) -> UResult<()> {
+        let device_file = File::open(device_path)?;
+        let partition_offset = get_partition_offset(&device_file)?;
+        let report_ioctls = &["getro", "getra", "getss", "getbsz", "getsize64"];
+        let ioctl_values = report_ioctls
+            .iter()
+            .map(|flag| {
+                let Some((
+                    _,
+                    BlockdevCommand::Ioctl(_, ioctl_code, IoctlCommand::GetAttribute(ioctl_type)),
+                )) = BLOCKDEV_ACTIONS.iter().find(|(n, _)| flag == n)
+                else {
+                    unreachable!()
+                };
+                unsafe { get_ioctl_attribute(&device_file, *ioctl_code, *ioctl_type) }
+            })
+            .collect::<Result<Vec<u64>, _>>()?;
+        println!(
+            "{} {:5} {:5} {:5} {:15} {:15}   {}",
+            if ioctl_values[0] == 1 { "ro" } else { "rw" },
+            ioctl_values[1],
+            ioctl_values[2],
+            ioctl_values[3],
+            partition_offset,
+            ioctl_values[4],
+            device_path
+        );
+        Ok(())
+    }
+
+    pub fn do_ioctl_command(
+        device: &File,
+        name: &str,
+        ioctl_code: u32,
+        ioctl_action: &IoctlCommand,
+        verbose: bool,
+        arg: usize,
+    ) -> UResult<()> {
+        match ioctl_action {
+            IoctlCommand::GetAttribute(ioctl_type) => {
+                let ret = unsafe { get_ioctl_attribute(device, ioctl_code, *ioctl_type)? };
+                if verbose {
+                    println!("{}: {}", name, ret);
+                } else {
+                    println!("{}", ret);
+                }
+            }
+            IoctlCommand::SetAttribute => {
+                unsafe { uu_ioctl(device, ioctl_code, arg)? };
+                if verbose {
+                    println!("{} succeeded.", name);
+                }
+            }
+            IoctlCommand::Operation(param) => {
+                unsafe { uu_ioctl(device, ioctl_code, param)? };
+                if verbose {
+                    println!("{} succeeded.", name);
+                }
+            }
+        };
+        Ok(())
+    }
+}
+
+#[cfg(target_os = "linux")]
+use linux::*;
+
+#[cfg(target_os = "linux")]
+#[uucore::main]
+pub fn uumain(args: impl uucore::Args) -> UResult<()> {
+    use std::fs::File;
+
+    let matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?;
+    let devices = matches
+        .get_many::<String>("devices")
+        .expect("Required command-line argument");
+
+    if matches.get_flag("report") {
+        println!("RO    RA   SSZ   BSZ        StartSec            Size   Device");
+        for device_path in devices {
+            uucore::show_if_err!(do_report(device_path));
+        }
+        Ok(())
+    } else {
+        // Recover arguments from clap in the same order they were passed
+        // Based on https://docs.rs/clap/latest/clap/_cookbook/find/index.html
+        let mut operations = BTreeMap::new();
+        for (id, op) in BLOCKDEV_ACTIONS {
+            if matches.value_source(id) != Some(clap::parser::ValueSource::CommandLine) {
+                continue;
+            }
+            let indices = matches.indices_of(id).unwrap();
+            let values = matches.get_many::<usize>(id).unwrap();
+            for (index, value) in indices.zip(values) {
+                operations.insert(index, (op.clone(), *value));
+            }
+        }
+
+        for device_path in devices {
+            let mut verbose = false;
+            let device_file = File::open(device_path)?;
+            for (operation, value) in operations.values() {
+                match operation {
+                    BlockdevCommand::SetVerbosity(true) => verbose = true,
+                    BlockdevCommand::SetVerbosity(false) => verbose = false,
+                    BlockdevCommand::Ioctl(description, ioctl_code, ioctl_action) => {
+                        if let Err(e) = do_ioctl_command(
+                            &device_file,
+                            description,
+                            *ioctl_code,
+                            ioctl_action,
+                            verbose,
+                            *value,
+                        ) {
+                            if verbose {
+                                println!("{} failed.", description);
+                            }
+                            return Err(e);
+                        }
+                    }
+                }
+            }
+        }
+        Ok(())
+    }
+}
+
+pub fn uu_app() -> Command {
+    let mut cmd = Command::new(uucore::util_name())
+        .version(crate_version!())
+        .about(ABOUT)
+        .override_usage(format_usage(USAGE))
+        .infer_long_args(true)
+        .arg(
+            Arg::new("report")
+                .long("report")
+                .help("print report for specified devices")
+                .action(ArgAction::SetTrue),
+        )
+        .arg(Arg::new("devices").required(true).action(ArgAction::Append));
+
+    for (flag, action) in BLOCKDEV_ACTIONS {
+        let mut arg = Arg::new(flag)
+            .long(flag)
+            .conflicts_with("report")
+            .action(ArgAction::Append)
+            .value_parser(value_parser!(usize));
+
+        match action {
+            BlockdevCommand::SetVerbosity(true) => {
+                arg = arg.short('v').help("verbose mode");
+            }
+            BlockdevCommand::SetVerbosity(false) => {
+                arg = arg.short('q').help("quiet mode");
+            }
+            BlockdevCommand::Ioctl(name, _, _) => {
+                arg = arg.help(name);
+            }
+        }
+
+        match action {
+            BlockdevCommand::Ioctl(_, _, IoctlCommand::SetAttribute) => {
+                arg = arg.num_args(1);
+            }
+            _ => {
+                arg = arg
+                    .num_args(0)
+                    .default_value("0")
+                    .default_missing_value("0");
+            }
+        }
+        cmd = cmd.arg(arg);
+    }
+    cmd
+}
+
+#[cfg(not(target_os = "linux"))]
+#[uucore::main]
+pub fn uumain(args: impl uucore::Args) -> UResult<()> {
+    let _matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?;
+
+    Err(uucore::error::USimpleError::new(
+        1,
+        "`blockdev` is available only on Linux.",
+    ))
+}
diff --git a/src/uu/blockdev/src/main.rs b/src/uu/blockdev/src/main.rs
new file mode 100644
index 0000000..0452069
--- /dev/null
+++ b/src/uu/blockdev/src/main.rs
@@ -0,0 +1 @@
+uucore::bin!(uu_blockdev);
diff --git a/tests/by-util/test_blockdev.rs b/tests/by-util/test_blockdev.rs
new file mode 100644
index 0000000..0fed69a
--- /dev/null
+++ b/tests/by-util/test_blockdev.rs
@@ -0,0 +1,70 @@
+// This file is part of the uutils util-linux package.
+//
+// For the full copyright and license information, please view the LICENSE
+// file that was distributed with this source code.
+
+use crate::common::util::TestScenario;
+
+#[test]
+fn test_invalid_arg() {
+    new_ucmd!().arg("--definitely-invalid").fails().code_is(1);
+}
+
+#[test]
+fn test_report_mutually_exclusive_with_others() {
+    new_ucmd!()
+        .arg("--report")
+        .arg("--getalignoff")
+        .arg("/foo")
+        .fails()
+        .code_is(1)
+        .stderr_contains("the argument '--report' cannot be used with '--getalignoff'");
+}
+
+#[cfg(target_os = "linux")]
+mod linux {
+    use crate::common::util::TestScenario;
+    use regex::Regex;
+
+    #[test]
+    fn test_fails_on_first_error() {
+        new_ucmd!()
+            .arg("-v")
+            .arg("--getalignoff")
+            .arg("--getbsz")
+            .arg("/dev/null")
+            .fails()
+            .code_is(1)
+            .stdout_is("get alignment offset in bytes failed.\n")
+            .stderr_contains("Inappropriate ioctl for device");
+    }
+
+    #[test]
+    fn test_report_continues_on_errors() {
+        new_ucmd!()
+            .arg("--report")
+            .arg("/dev/null")
+            .arg("/non/existing")
+            .fails()
+            .code_is(1)
+            .stderr_matches(
+                &Regex::new("(?ms)Inappropriate ioctl for device.*No such file or directory")
+                    .unwrap(),
+            );
+    }
+}
+
+#[cfg(not(target_os = "linux"))]
+mod non_linux {
+    use crate::common::util::TestScenario;
+
+    #[test]
+    fn test_fails_on_unsupported_platforms() {
+        new_ucmd!()
+            .arg("--report")
+            .arg("/dev/null")
+            .fails()
+            .code_is(1)
+            .stderr_is("blockdev: `blockdev` is available only on Linux.\n");
+    }
+}
diff --git a/tests/tests.rs b/tests/tests.rs
index 824a096..23c3f90 100644
--- a/tests/tests.rs
+++ b/tests/tests.rs
@@ -17,6 +17,10 @@ mod test_lsmem;
 #[path = "by-util/test_mountpoint.rs"]
 mod test_mountpoint;
 
+#[cfg(feature = "blockdev")]
+#[path = "by-util/test_blockdev.rs"]
+mod test_blockdev;
+
 #[cfg(feature = "ctrlaltdel")]
 #[path = "by-util/test_ctrlaltdel.rs"]
 mod test_ctrlaltdel;