diff --git a/libc/build.mak b/libc/build.mak
index 9277a1a87c7bf1ee8eb317ec945522438aa38dff..07a1ba6560b58eca842e6b4bbcd5df413d47827e 100644
--- a/libc/build.mak
+++ b/libc/build.mak
@@ -353,3 +353,4 @@ libc += time.o
 libc += signal.o
 libc += mman.o
 libc += qsort.o
+libc += sem.o
diff --git a/libc/sem.cc b/libc/sem.cc
new file mode 100644
index 0000000000000000000000000000000000000000..4373dbcd0adb4923c613014db2dfd101a8c552e6
--- /dev/null
+++ b/libc/sem.cc
@@ -0,0 +1,80 @@
+#include <semaphore.h>
+#include "sched.hh"
+#include "mutex.hh"
+
+// FIXME: smp safety
+
+class semaphore {
+public:
+    explicit semaphore(unsigned val);
+    void post();
+    void wait();
+private:
+    unsigned _val;
+    mutex _mtx;
+    struct wait_record {
+        sched::thread* owner;
+    };
+    std::list<wait_record*> _waiters;
+};
+
+semaphore::semaphore(unsigned val)
+    : _val(val)
+{
+}
+
+void semaphore::post()
+{
+    auto wr = with_lock(_mtx, [this] () -> wait_record* {
+        if (_waiters.empty()) {
+            ++_val;
+            return nullptr;
+        }
+        auto wr = _waiters.front();
+        _waiters.pop_front();
+        return wr;
+    });
+    if (wr) {
+        auto t = wr->owner;
+        wr->owner = nullptr;
+        t->wake();
+    }
+}
+
+void semaphore::wait()
+{
+    wait_record wr;
+    wr.owner = nullptr;
+    with_lock(_mtx, [&] {
+        if (_val > 0) {
+            --_val;
+        } else {
+            wr.owner = sched::thread::current();
+            _waiters.push_back(&wr);
+        }
+    });
+    sched::thread::wait_until([&] { return !wr.owner; });
+}
+
+semaphore* from_libc(sem_t* p)
+{
+    return reinterpret_cast<semaphore*>(p);
+}
+
+int sem_init(sem_t* s, int pshared, unsigned val)
+{
+    new (s) semaphore(val);
+    return 0;
+}
+
+int sem_post(sem_t* s)
+{
+    from_libc(s)->post();
+    return 0;
+}
+
+int sem_wait(sem_t* s)
+{
+    from_libc(s)->wait();
+    return 0;
+}