diff --git a/src/header/string/mod.rs b/src/header/string/mod.rs
index 285c19cfc59948e2788fd4db1bbebdc9edd9e814..0d16d6d2d127f6e4c501ec8d516ba0823572b8b0 100644
--- a/src/header/string/mod.rs
+++ b/src/header/string/mod.rs
@@ -1,11 +1,12 @@
 //! string implementation for Redox, following http://pubs.opengroup.org/onlinepubs/7908799/xsh/string.h.html
 
-use core::{mem, ptr, slice, usize};
+use core::{iter::once, mem, ptr, slice, usize};
 
 use cbitset::BitSet256;
 
 use crate::{
     header::{errno::*, signal},
+    iter::{NulTerminated, SrcDstPtrIter},
     platform::{self, types::*},
 };
 
@@ -154,17 +155,10 @@ pub unsafe extern "C" fn strcoll(s1: *const c_char, s2: *const c_char) -> c_int
 
 #[no_mangle]
 pub unsafe extern "C" fn strcpy(dst: *mut c_char, src: *const c_char) -> *mut c_char {
-    let mut i = 0;
-
-    loop {
-        let byte = *src.offset(i);
-        *dst.offset(i) = byte;
-
-        if byte == 0 {
-            break;
-        }
-
-        i += 1;
+    let src_iter = unsafe { NulTerminated::new(src) };
+    let src_dest_iter = unsafe { SrcDstPtrIter::new(src_iter.chain(once(&0)), dst) };
+    for (src_item, dst_item) in src_dest_iter {
+        dst_item.write(*src_item);
     }
 
     dst
@@ -262,19 +256,12 @@ pub unsafe extern "C" fn strerror_r(errnum: c_int, buf: *mut c_char, buflen: siz
 
 #[no_mangle]
 pub unsafe extern "C" fn strlen(s: *const c_char) -> size_t {
-    strnlen(s, usize::MAX)
+    unsafe { NulTerminated::new(s) }.count()
 }
 
 #[no_mangle]
 pub unsafe extern "C" fn strnlen(s: *const c_char, size: size_t) -> size_t {
-    let mut i = 0;
-    while i < size {
-        if *s.add(i) == 0 {
-            break;
-        }
-        i += 1;
-    }
-    i as size_t
+    unsafe { NulTerminated::new(s) }.take(size).count()
 }
 
 #[no_mangle]
diff --git a/src/header/wchar/mod.rs b/src/header/wchar/mod.rs
index 0dea96faa84a4c4f4ec9afacfb8f542885514658..67e268e09f5bf5ac32aed7fdd1cba258fedbe51c 100644
--- a/src/header/wchar/mod.rs
+++ b/src/header/wchar/mod.rs
@@ -12,6 +12,7 @@ use crate::{
         time::*,
         wctype::*,
     },
+    iter::NulTerminated,
     platform::{self, types::*, ERRNO},
 };
 
@@ -497,13 +498,7 @@ pub extern "C" fn wcsftime(
 
 #[no_mangle]
 pub unsafe extern "C" fn wcslen(ws: *const wchar_t) -> size_t {
-    let mut i = 0;
-    loop {
-        if *ws.add(i) == 0 {
-            return i;
-        }
-        i += 1;
-    }
+    unsafe { NulTerminated::new(ws) }.count()
 }
 
 #[no_mangle]
diff --git a/src/iter.rs b/src/iter.rs
new file mode 100644
index 0000000000000000000000000000000000000000..5b9f7cb1c90361b09e59ed0913fef84b044e080b
--- /dev/null
+++ b/src/iter.rs
@@ -0,0 +1,140 @@
+//! Utilities to help use Rust iterators on C strings.
+
+use core::{iter::Iterator, marker::PhantomData, mem::MaybeUninit, ptr::NonNull};
+
+use crate::platform::types::*;
+
+/// A minimal alternative to the `Zero` trait from num-traits, for use in
+/// `NulTerminated`.
+///
+/// May be replaced with the one from num-traits at a later time if so
+/// desired.
+pub unsafe trait Zero {
+    fn is_zero(&self) -> bool;
+}
+
+unsafe impl Zero for c_char {
+    fn is_zero(&self) -> bool {
+        self == &0
+    }
+}
+
+unsafe impl Zero for wchar_t {
+    fn is_zero(&self) -> bool {
+        self == &0
+    }
+}
+
+/// An iterator over a nul-terminated buffer.
+///
+/// This is intended to allow safe, ergonomic iteration over C-style byte and
+/// wide strings without first having to read through the string and construct
+/// a slice. Assuming the safety requirements are upheld when constructing the
+/// iterator, it allows for string iteration in safe Rust.
+pub struct NulTerminated<'a, T: Zero> {
+    ptr: NonNull<T>,
+    phantom: PhantomData<&'a T>,
+}
+
+impl<'a, T: Zero> Iterator for NulTerminated<'a, T> {
+    type Item = &'a T;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        // SAFETY: the caller is required to ensure a valid pointer to a
+        // 0-terminated buffer is provided, and the zero-check below ensures
+        // that iteration and pointer increments will stop in time.
+        let val_ref = unsafe { self.ptr.as_ref() };
+        if val_ref.is_zero() {
+            None
+        } else {
+            // SAFETY: the caller is required to provide a 0-terminated
+            // buffer, and this point will only be reached if the next element
+            // is at most the terminating 0.
+            self.ptr = unsafe { self.ptr.add(1) };
+            Some(val_ref)
+        }
+    }
+}
+
+impl<'a, T: Zero> NulTerminated<'a, T> {
+    /// Constructs a new iterator, starting at `ptr`, yielding elements of
+    /// type `&T` up to (but not including) the terminating nul.
+    ///
+    /// The iterator returns `None` after the terminating nul has been
+    /// encountered.
+    ///
+    /// # Safety
+    /// The provided pointer must be a valid pointer to a buffer of contiguous
+    /// elements of type `T`, and the value 0 must be present within the
+    /// buffer at or after `ptr` (not necessarily at the end). The buffer must
+    /// not be written to for the lifetime of the iterator.
+    pub unsafe fn new(ptr: *const T) -> Self {
+        NulTerminated {
+            // NonNull can only wrap only *mut pointers...
+            ptr: NonNull::new(ptr.cast_mut()).unwrap(),
+            phantom: PhantomData,
+        }
+    }
+}
+
+/// A zipped iterator mapping an input iterator to an "out" pointer.
+///
+/// This is intended to allow safe, iterative writing to an "out pointer".
+/// Special care needs to be taken to avoid creating references past the end
+/// of the output buffer, thus the output is zipped with an "input" iterator
+/// to ensure up-front control of the range of memory on which we create
+/// references.
+pub struct SrcDstPtrIter<'a, I: Iterator, U: Copy> {
+    src_iter: I,
+    dst_ptr: *mut U,
+    phantom: PhantomData<&'a mut U>,
+}
+
+impl<'a, I: Iterator, U: Copy> Iterator for SrcDstPtrIter<'a, I, U> {
+    type Item = (I::Item, &'a mut MaybeUninit<U>);
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if let Some(src_item) = self.src_iter.next() {
+            let old_dst_ptr = self.dst_ptr;
+
+            // SAFETY: due to the caller requirements on `I` upon
+            // construction, the new pointer here may be either valid to turn
+            // into a reference or "one past the end". The latter is okay as
+            // long as it is only represented as a raw pointer.
+            self.dst_ptr = unsafe { self.dst_ptr.add(1) };
+
+            // SAFETY: self.dst_ptr may point "one past the end", but the
+            // caller is required upon construction to ensure that `I` does
+            // not over-iterate, and thus old_dst_ptr is always okay to
+            // dereference.
+            let out_mut_ref = unsafe { old_dst_ptr.as_uninit_mut() }.unwrap();
+
+            Some((src_item, out_mut_ref))
+        } else {
+            None
+        }
+    }
+}
+
+impl<'a, I: Iterator, U: Copy> SrcDstPtrIter<'a, I, U> {
+    /// Constructs a new iterator of "zipped" input and output.
+    ///
+    /// The caller must provide an "input" iterator `I` and an "out pointer"
+    /// `ptr`. Assuming `I` has item type `T`, the new iterator will have
+    /// `type Item = (T, &mut MaybeUninit<U>)`.
+    ///
+    /// # Safety
+    /// `ptr` must be a valid pointer to a writable buffer of contiguous (but
+    /// possibly uninitialized) elements of type `U`. The caller must ensure
+    /// that `I` does not return `Some` any more times than there are elements
+    /// in the output buffer. The caller must ensure that the iterator has
+    /// exclusive access to that buffer for the entire lifetime of the
+    /// iterator.
+    pub unsafe fn new(iter: I, ptr: *mut U) -> Self {
+        SrcDstPtrIter {
+            src_iter: iter,
+            dst_ptr: ptr,
+            phantom: PhantomData,
+        }
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 2104fbc851a5c1c0b8081957ee990afbb502475c..8ab4e35145c794f252916c2f2c768e82034d4dd5 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -14,6 +14,7 @@
 #![feature(lang_items)]
 #![feature(let_chains)]
 #![feature(linkage)]
+#![feature(ptr_as_uninit)]
 #![feature(stmt_expr_attributes)]
 #![feature(str_internals)]
 #![feature(sync_unsafe_cell)]
@@ -52,6 +53,7 @@ pub mod error;
 pub mod fs;
 pub mod header;
 pub mod io;
+pub mod iter;
 pub mod ld_so;
 pub mod platform;
 pub mod pthread;