diff --git a/libos/include/libos_fs_lock.h b/libos/include/libos_fs_lock.h
index 5dc6d489a0..e3be58a930 100644
--- a/libos/include/libos_fs_lock.h
+++ b/libos/include/libos_fs_lock.h
@@ -4,7 +4,8 @@
  */
 
 /*
- * File locks. Currently only POSIX locks are implemented.
+ * File locks. Both POSIX locks (fcntl syscall) and BSD locks (flock syscall) are implemented via
+ * a common struct `posix_lock` (the name is historic). See `man fcntl` and `man flock` for details.
  */
 
 #pragma once
@@ -22,8 +23,8 @@ struct libos_dentry;
 int init_fs_lock(void);
 
 /*
- * File locks. Currently describes only POSIX locks (also known as advisory record locks). See `man
- * fcntl` for details.
+ * File locks. Describes both POSIX locks aka advisory record locks (fcntl syscall) and BSD locks
+ * (flock syscall). See `man fcntl` and `man flock` for details.
  *
  * The current implementation works over IPC and handles all requests in the main process. It has
  * the following caveats:
@@ -34,7 +35,7 @@ int init_fs_lock(void);
  *   local-process-only filesystems (tmpfs).
  * - There is no deadlock detection (EDEADLK).
  * - The lock requests cannot be interrupted (EINTR).
- * - The locks work only on files that have a dentry (no pipes, sockets etc.)
+ * - The locks work only on files that have a dentry (no pipes, sockets etc.).
  */
 
 DEFINE_LISTP(libos_file_lock);
@@ -54,6 +55,9 @@ struct libos_file_lock {
 
     /* List node, used internally */
     LIST_TYPE(libos_file_lock) list;
+
+    /* Unique handle id, works as an identifier for `flock` syscall */
+    uint64_t handle_id;
 };
 
 /*!
@@ -125,3 +129,14 @@ int file_lock_set_from_ipc(const char* path, struct libos_file_lock* file_lock,
  */
 int file_lock_get_from_ipc(const char* path, struct libos_file_lock* file_lock,
                            struct libos_file_lock* out_file_lock);
+
+/*!
+ * \brief Determine whether dentry has flock-typed locks.
+ *
+ * \param dent The dentry for a file.
+ *
+ * Returns true if at least one associated lock is flock-typed. Otherwise returns false. Note that
+ * if a dentry has a mix of fcntl and flock locks, then this function returns true (and the
+ * subsequent behavior is undefined).
+ */
+bool has_flock_locks(struct libos_dentry* dent);
diff --git a/libos/include/libos_handle.h b/libos/include/libos_handle.h
index 056e31aab2..25f1b8b0bd 100644
--- a/libos/include/libos_handle.h
+++ b/libos/include/libos_handle.h
@@ -134,6 +134,10 @@ struct libos_handle {
     enum libos_handle_type type;
     bool is_dir;
 
+    /* Unique id, works as an identifier for `flock` syscall. This field does not change, so
+     * reading it does not require holding any locks. */
+    uint64_t id;
+
     refcount_t ref_count;
 
     struct libos_fs* fs;
diff --git a/libos/include/libos_ipc.h b/libos/include/libos_ipc.h
index 6478f47cd4..9502713905 100644
--- a/libos/include/libos_ipc.h
+++ b/libos/include/libos_ipc.h
@@ -281,6 +281,7 @@ struct libos_ipc_file_lock {
     int type;
     uint64_t start;
     uint64_t end;
+    uint64_t handle_id;
     IDTYPE pid;
 
     bool wait;
@@ -294,6 +295,7 @@ struct libos_ipc_file_lock_resp {
     int type;
     uint64_t start;
     uint64_t end;
+    uint64_t handle_id;
     IDTYPE pid;
 };
 
diff --git a/libos/src/bookkeep/libos_handle.c b/libos/src/bookkeep/libos_handle.c
index 7d73c02490..25bc6ff725 100644
--- a/libos/src/bookkeep/libos_handle.c
+++ b/libos/src/bookkeep/libos_handle.c
@@ -292,7 +292,7 @@ static struct libos_handle* __detach_fd_handle(struct libos_fd_handle* fd, int*
 }
 
 static int clear_posix_locks(struct libos_handle* handle) {
-    if (handle && handle->dentry) {
+    if (handle && handle->dentry && (!has_flock_locks(handle->dentry))) {
         /* Clear file (POSIX) locks for a file. We are required to do that every time a FD is
          * closed, even if the process holds other handles for that file, or duplicated FDs for the
          * same handle. */
@@ -301,6 +301,7 @@ static int clear_posix_locks(struct libos_handle* handle) {
             .start = 0,
             .end = FS_LOCK_EOF,
             .pid = g_process.pid,
+            .handle_id = 0,
         };
         int ret = file_lock_set(handle->dentry, &file_lock, /*block=*/false);
         if (ret < 0) {
@@ -350,6 +351,10 @@ struct libos_handle* get_new_handle(void) {
     }
     INIT_LISTP(&new_handle->epoll_items);
     new_handle->epoll_items_count = 0;
+
+    static uint64_t local_counter = 0;
+    new_handle->id = ((uint64_t)g_process.pid << 32)
+                      | __atomic_add_fetch(&local_counter, 1, __ATOMIC_RELAXED);
     return new_handle;
 }
 
@@ -475,6 +480,26 @@ static void destroy_handle(struct libos_handle* hdl) {
     free_mem_obj_to_mgr(handle_mgr, hdl);
 }
 
+static int clear_flock_locks(struct libos_handle* hdl) {
+    /* Clear flock (BSD) locks for a file. We are required to do that when the handle is closed. */
+    if (hdl && hdl->dentry && has_flock_locks(hdl->dentry)) {
+        assert(hdl->ref_count == 0);
+        struct libos_file_lock file_lock = {
+            .type = F_UNLCK,
+            .start = 0,
+            .end = FS_LOCK_EOF,
+            .pid = g_process.pid,
+            .handle_id = hdl->id,
+        };
+        int ret = file_lock_set(hdl->dentry, &file_lock, /*block=*/false);
+        if (ret < 0) {
+            log_warning("error releasing locks: %s", unix_strerror(ret));
+            return ret;
+        }
+    }
+    return 0;
+}
+
 void put_handle(struct libos_handle* hdl) {
     refcount_t ref_count = refcount_dec(&hdl->ref_count);
 
@@ -496,8 +521,10 @@ void put_handle(struct libos_handle* hdl) {
             hdl->pal_handle = NULL;
         }
 
-        if (hdl->dentry)
+        if (hdl->dentry) {
+            (void)clear_flock_locks(hdl);
             put_dentry(hdl->dentry);
+        }
 
         if (hdl->inode)
             put_inode(hdl->inode);
diff --git a/libos/src/fs/libos_fs_lock.c b/libos/src/fs/libos_fs_lock.c
index 30948ce067..3119d2fb99 100644
--- a/libos/src/fs/libos_fs_lock.c
+++ b/libos/src/fs/libos_fs_lock.c
@@ -44,14 +44,22 @@ struct file_lock_request {
     LIST_TYPE(file_lock_request) list;
 };
 
-/* Describes file locks' details for a given dentry. Currently holds only POSIX locks. */
+/* Describes file locks' details for a given dentry. Holds both POSIX (fcntl) and BSD (flock)
+ * locks. */
 DEFINE_LISTP(dent_file_locks);
 DEFINE_LIST(dent_file_locks);
 struct dent_file_locks {
     struct libos_dentry* dent;
 
-    /* Currently only POSIX locks, sorted by PID and then by start position (so that we are able to
-     * merge and split locks). The ranges do not overlap within a given PID. */
+    /*
+     * POSIX (fcntl) and BSD (flock) locks for a given dentry.
+     *
+     * For POSIX locks:
+     *   - sorted by PID and then by start position (so that we are able to merge and split locks),
+     *   - the ranges do not overlap within a given PID.
+     *
+     * BSD locks do not have ranges, thus the above properties do not apply.
+     */
     LISTP_TYPE(libos_file_lock) file_locks;
 
     /* Pending requests. */
@@ -104,11 +112,18 @@ static void file_locks_dump(struct dent_file_locks* dent_file_locks) {
 
     struct libos_file_lock* file_lock;
     LISTP_FOR_EACH_ENTRY(file_lock, &dent_file_locks->file_locks, list) {
-        if (file_lock->pid != pid) {
+        if (file_lock->handle_id == 0) {
+            if (file_lock->pid != pid) {
+                if (pid != 0)
+                    buf_flush(&buf);
+                pid = file_lock->pid;
+                buf_printf(&buf, "fcntl (POSIX): pid=%d:", pid);
+            }
+        } else {
             if (pid != 0)
                 buf_flush(&buf);
-            pid = file_lock->pid;
-            buf_printf(&buf, "%d:", pid);
+            pid = 123; /* dummy pid to force a flush in the line above */
+            buf_printf(&buf, " flock (BSD): handle id=%lu:", file_lock->handle_id);
         }
 
         char c;
@@ -147,8 +162,10 @@ static void dent_file_locks_gc(struct dent_file_locks* dent_file_locks) {
 }
 
 /*
- * Find first lock that conflicts with `file_lock`. Two locks conflict if they have different PIDs,
- * their ranges overlap, and at least one of them is a write lock.
+ * Find first lock that conflicts with `file_lock`. For fcntl locks (file_lock->handle_id == 0), two
+ * locks conflict if they have different PIDs, their ranges overlap, and at least one of them is a
+ * write lock. For flock locks, two locks conflict if they have different handle IDs and at least
+ * one of them is a write lock.
  */
 static struct libos_file_lock* file_lock_find_conflict(struct dent_file_locks* dent_file_locks,
                                                        struct libos_file_lock* file_lock) {
@@ -156,11 +173,21 @@ static struct libos_file_lock* file_lock_find_conflict(struct dent_file_locks* d
     assert(file_lock->type != F_UNLCK);
 
     struct libos_file_lock* cur;
-    LISTP_FOR_EACH_ENTRY(cur, &dent_file_locks->file_locks, list) {
-        if (cur->pid != file_lock->pid && file_lock->start <= cur->end
-               && cur->start <= file_lock->end
-               && (cur->type == F_WRLCK || file_lock->type == F_WRLCK))
-            return cur;
+    /* Gramine doesn't support mixing POSIX and flock types of locks, and assumes that the
+     * application won't ever mix them, otherwise it may exhibit unexpected behavior. */
+    if (file_lock->handle_id == 0) {
+        LISTP_FOR_EACH_ENTRY(cur, &dent_file_locks->file_locks, list) {
+            if (cur->pid != file_lock->pid && file_lock->start <= cur->end
+                    && cur->start <= file_lock->end
+                    && (cur->type == F_WRLCK || file_lock->type == F_WRLCK))
+                return cur;
+        }
+    } else {
+        LISTP_FOR_EACH_ENTRY(cur, &dent_file_locks->file_locks, list) {
+            if (cur->handle_id != file_lock->handle_id
+                    && (cur->type == F_WRLCK || file_lock->type == F_WRLCK))
+                return cur;
+        }
     }
     return NULL;
 }
@@ -184,9 +211,29 @@ static int file_lock_add_request(struct dent_file_locks* dent_file_locks,
     return 0;
 }
 
+bool has_flock_locks(struct libos_dentry* dent) {
+    lock(&g_fs_lock_lock);
+    if (!dent->file_locks) {
+        unlock(&g_fs_lock_lock);
+        return false;
+    }
+
+    bool has_flock = false;
+    struct dent_file_locks* dent_file_locks = dent->file_locks;
+    struct libos_file_lock* cur;
+    LISTP_FOR_EACH_ENTRY(cur, &dent_file_locks->file_locks, list) {
+        if (cur->handle_id != 0) {
+            has_flock = true;
+            break;
+        }
+    }
+    unlock(&g_fs_lock_lock);
+    return has_flock;
+}
+
 /*
- * Main part of `file_lock_set`. Adds/removes a lock (depending on `file_lock->type`), assumes we
- * already verified there are no conflicts. Replaces existing locks for a given PID, and merges
+ * Main part of `file_lock_set`. Adds/removes a POSIX lock (depending on `file_lock->type`), assumes
+ * we already verified there are no conflicts. Replaces existing locks for a given PID, and merges
  * adjacent locks if possible.
  *
  * See also Linux sources (`fs/locks.c`) for a similar implementation.
@@ -194,6 +241,7 @@ static int file_lock_add_request(struct dent_file_locks* dent_file_locks,
 static int _posix_lock_set(struct dent_file_locks* dent_file_locks,
                            struct libos_file_lock* file_lock) {
     assert(locked(&g_fs_lock_lock));
+    assert(file_lock->handle_id == 0);
 
     /* Preallocate new locks first, so that we don't fail after modifying something. */
 
@@ -289,6 +337,7 @@ static int _posix_lock_set(struct dent_file_locks* dent_file_locks,
                 extra->start = end + 1;
                 extra->end = cur->end;
                 extra->pid = cur->pid;
+                extra->handle_id = 0;
                 cur->end = start - 1;
                 LISTP_ADD_AFTER(extra, cur, &dent_file_locks->file_locks, list);
                 extra = NULL;
@@ -324,11 +373,11 @@ static int _posix_lock_set(struct dent_file_locks* dent_file_locks,
     if (new) {
         assert(file_lock->type != F_UNLCK);
 
-
         new->type = file_lock->type;
         new->start = start;
         new->end = end;
         new->pid = file_lock->pid;
+        new->handle_id = 0;
 
 #ifdef DEBUG
         /* Assert that list order is preserved */
@@ -354,6 +403,49 @@ static int _posix_lock_set(struct dent_file_locks* dent_file_locks,
     return 0;
 }
 
+/*
+ * Main part of `file_lock_set`. Adds/removes a BSD lock (depending on `file_lock->type`), assumes
+ * we already verified there are no conflicts. Replaces existing locks for a given handle ID.
+ */
+static int _flock_lock_set(struct dent_file_locks* dent_file_locks,
+                           struct libos_file_lock* file_lock) {
+    assert(locked(&g_fs_lock_lock));
+    assert(file_lock->handle_id);
+
+    /* Lock to be added. Not necessary for F_UNLCK, because we're only removing existing locks. */
+    struct libos_file_lock* new = NULL;
+    if (file_lock->type != F_UNLCK) {
+        new = malloc(sizeof(*new));
+        if (!new)
+            return -ENOMEM;
+    }
+
+    struct libos_file_lock* cur;
+    struct libos_file_lock* tmp;
+    LISTP_FOR_EACH_ENTRY_SAFE(cur, tmp, &dent_file_locks->file_locks, list) {
+        if (cur->handle_id == file_lock->handle_id) {
+            LISTP_DEL(cur, &dent_file_locks->file_locks, list);
+            free(cur);
+            break;
+        }
+    }
+
+    if (new) {
+        assert(file_lock->type != F_UNLCK);
+        new->type = file_lock->type;
+        /* Lock the whole file; start, end and pid fields are set only for sanity
+         * (not used by flock). */
+        new->start = 0;
+        new->end = FS_LOCK_EOF;
+        new->pid = file_lock->pid;
+        new->handle_id = file_lock->handle_id;
+
+        LISTP_ADD(new, &dent_file_locks->file_locks, list);
+    }
+
+    return 0;
+}
+
 /*
  * Process pending requests. This function should be called after any modification to the list of
  * locks, since we might have unblocked a request.
@@ -373,7 +465,9 @@ static void file_lock_process_requests(struct dent_file_locks* dent_file_locks)
             struct libos_file_lock* conflict = file_lock_find_conflict(dent_file_locks,
                                                                        &req->file_lock);
             if (!conflict) {
-                int result = _posix_lock_set(dent_file_locks, &req->file_lock);
+                int result = req->file_lock.handle_id == 0
+                                 ? _posix_lock_set(dent_file_locks, &req->file_lock)
+                                 : _flock_lock_set(dent_file_locks, &req->file_lock);
                 LISTP_DEL(req, &dent_file_locks->file_lock_requests, list);
 
                 /* Notify the waiter that we processed their request. Note that the result might
@@ -434,7 +528,8 @@ static int file_lock_set_or_add_request(struct libos_dentry* dent,
 
         *out_req = req;
     } else {
-        ret = _posix_lock_set(dent_file_locks, file_lock);
+        ret = file_lock->handle_id == 0 ? _posix_lock_set(dent_file_locks, file_lock)
+                                        : _flock_lock_set(dent_file_locks, file_lock);
         if (ret < 0)
             goto out;
         file_lock_process_requests(dent_file_locks);
@@ -581,6 +676,7 @@ int file_lock_get(struct libos_dentry* dent, struct libos_file_lock* file_lock,
         out_file_lock->start = conflict->start;
         out_file_lock->end = conflict->end;
         out_file_lock->pid = conflict->pid;
+        out_file_lock->handle_id = conflict->handle_id;
     } else {
         out_file_lock->type = F_UNLCK;
     }
@@ -613,7 +709,7 @@ int file_lock_get_from_ipc(const char* path, struct libos_file_lock* file_lock,
     return ret;
 }
 
-/* Removes all locks and lock requests for a given PID and dentry. */
+/* Removes all POSIX locks and lock requests for a given PID and dentry. */
 static int file_lock_clear_pid_from_dentry(struct libos_dentry* dent, IDTYPE pid) {
     assert(locked(&g_fs_lock_lock));
 
@@ -631,7 +727,7 @@ static int file_lock_clear_pid_from_dentry(struct libos_dentry* dent, IDTYPE pid
     struct libos_file_lock* file_lock;
     struct libos_file_lock* file_lock_tmp;
     LISTP_FOR_EACH_ENTRY_SAFE(file_lock, file_lock_tmp, &dent_file_locks->file_locks, list) {
-        if (file_lock->pid == pid) {
+        if (file_lock->handle_id == 0 && file_lock->pid == pid) {
             LISTP_DEL(file_lock, &dent_file_locks->file_locks, list);
             free(file_lock);
             changed = true;
@@ -641,7 +737,7 @@ static int file_lock_clear_pid_from_dentry(struct libos_dentry* dent, IDTYPE pid
     struct file_lock_request* req;
     struct file_lock_request* req_tmp;
     LISTP_FOR_EACH_ENTRY_SAFE(req, req_tmp, &dent_file_locks->file_lock_requests, list) {
-        if (req->file_lock.pid == pid) {
+        if (req->file_lock.handle_id == 0 && req->file_lock.pid == pid) {
             assert(!req->notify.event);
             LISTP_DEL(req, &dent_file_locks->file_lock_requests, list);
             free(req);
diff --git a/libos/src/ipc/libos_ipc_fs_lock.c b/libos/src/ipc/libos_ipc_fs_lock.c
index 3b0e96a23d..f43a8a0f37 100644
--- a/libos/src/ipc/libos_ipc_fs_lock.c
+++ b/libos/src/ipc/libos_ipc_fs_lock.c
@@ -17,6 +17,7 @@ int ipc_file_lock_set(const char* path, struct libos_file_lock* file_lock, bool
         .type = file_lock->type,
         .start = file_lock->start,
         .end = file_lock->end,
+        .handle_id = file_lock->handle_id,
         .pid = file_lock->pid,
 
         .wait = wait,
@@ -60,6 +61,7 @@ int ipc_file_lock_get(const char* path, struct libos_file_lock* file_lock,
         .type = file_lock->type,
         .start = file_lock->start,
         .end = file_lock->end,
+        .handle_id = file_lock->handle_id,
         .pid = file_lock->pid,
     };
 
@@ -85,6 +87,7 @@ int ipc_file_lock_get(const char* path, struct libos_file_lock* file_lock,
         out_file_lock->type = resp->type;
         out_file_lock->start = resp->start;
         out_file_lock->end = resp->end;
+        out_file_lock->handle_id = resp->handle_id;
         out_file_lock->pid = resp->pid;
     }
     free(data);
@@ -115,6 +118,7 @@ int ipc_file_lock_set_callback(IDTYPE src, void* data, unsigned long seq) {
         .start = msgin->start,
         .end = msgin->end,
         .pid = msgin->pid,
+        .handle_id = msgin->handle_id,
     };
 
     return file_lock_set_from_ipc(msgin->path, &file_lock, msgin->wait, src, seq);
@@ -127,6 +131,7 @@ int ipc_file_lock_get_callback(IDTYPE src, void* data, unsigned long seq) {
         .start = msgin->start,
         .end = msgin->end,
         .pid = msgin->pid,
+        .handle_id = msgin->handle_id,
     };
 
     struct libos_file_lock file_lock2 = {0};
@@ -136,6 +141,7 @@ int ipc_file_lock_get_callback(IDTYPE src, void* data, unsigned long seq) {
         .type = file_lock2.type,
         .start = file_lock2.start,
         .end = file_lock2.end,
+        .handle_id = file_lock2.handle_id,
         .pid = file_lock2.pid,
     };
 
diff --git a/libos/src/sys/libos_fcntl.c b/libos/src/sys/libos_fcntl.c
index e17ae657ac..d373c50369 100644
--- a/libos/src/sys/libos_fcntl.c
+++ b/libos/src/sys/libos_fcntl.c
@@ -5,7 +5,9 @@
  */
 
 /*
- * Implementation of system call "fcntl":
+ * Implementation of system calls "fcntl" and "flock".
+ *
+ * The "fcntl" syscall supports:
  *
  * - F_DUPFD, F_DUPFD_CLOEXEC (duplicate a file descriptor)
  * - F_GETFD, F_SETFD (file descriptor flags)
@@ -58,7 +60,7 @@ int set_handle_nonblocking(struct libos_handle* handle, bool on) {
  * We need to return -EINVAL for underflow (positions before start of file), and -EOVERFLOW for
  * positive overflow.
  */
-static int flock_to_file_lock(struct flock* fl, struct libos_handle* hdl,
+static int flock_to_file_lock(struct flock* fl, struct libos_handle* hdl, uint64_t handle_id,
                               struct libos_file_lock* file_lock) {
     if (!(fl->l_type == F_RDLCK || fl->l_type == F_WRLCK || fl->l_type == F_UNLCK))
         return -EINVAL;
@@ -128,6 +130,7 @@ static int flock_to_file_lock(struct flock* fl, struct libos_handle* hdl,
     file_lock->start = start;
     file_lock->end = end;
     file_lock->pid = g_process.pid;
+    file_lock->handle_id = handle_id;
     return 0;
 }
 
@@ -212,7 +215,7 @@ long libos_syscall_fcntl(int fd, int cmd, unsigned long arg) {
             }
 
             struct libos_file_lock file_lock;
-            ret = flock_to_file_lock(fl, hdl, &file_lock);
+            ret = flock_to_file_lock(fl, hdl, /*handle_id=*/0, &file_lock);
             if (ret < 0)
                 break;
 
@@ -235,7 +238,7 @@ long libos_syscall_fcntl(int fd, int cmd, unsigned long arg) {
             }
 
             struct libos_file_lock file_lock;
-            ret = flock_to_file_lock(fl, hdl, &file_lock);
+            ret = flock_to_file_lock(fl, hdl, /*handle_id=*/0, &file_lock);
             if (ret < 0)
                 break;
 
@@ -294,18 +297,29 @@ long libos_syscall_flock(unsigned int fd, unsigned int cmd) {
     if (!hdl)
         return -EBADF;
 
+    struct flock fl = { .l_whence = SEEK_SET };
+
     switch (cmd & ~LOCK_NB) {
         case LOCK_EX:
+            fl.l_type = F_WRLCK;
+            break;
         case LOCK_SH:
+            fl.l_type = F_RDLCK;
+            break;
         case LOCK_UN:
+            fl.l_type = F_UNLCK;
             break;
         default:
             ret = -EINVAL;
             goto out;
     }
 
-    /* TODO: add implementation */
-    ret = -ENOSYS;
+    struct libos_file_lock file_lock;
+    ret = flock_to_file_lock(&fl, hdl, hdl->id, &file_lock);
+    if (ret < 0)
+        goto out;
+
+    ret = file_lock_set(hdl->dentry, &file_lock, !(cmd & LOCK_NB));
 out:
     put_handle(hdl);
     return ret;
diff --git a/libos/test/ltp/ltp.cfg b/libos/test/ltp/ltp.cfg
index 290fb043eb..e40899a1b9 100644
--- a/libos/test/ltp/ltp.cfg
+++ b/libos/test/ltp/ltp.cfg
@@ -547,8 +547,8 @@ skip = yes
 [flistxattr*]
 skip = yes
 
-# no flock()
-[flock*]
+# uses futexes on memory shared between processes
+[flock03]
 skip = yes
 
 # %fs test, i386 only
diff --git a/libos/test/regression/flock_lock.c b/libos/test/regression/flock_lock.c
new file mode 100644
index 0000000000..3adbff4cc8
--- /dev/null
+++ b/libos/test/regression/flock_lock.c
@@ -0,0 +1,191 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* Copyright (C) 2023 Intel Corporation
+ *                    Liang Ma <liang3.ma@intel.com>
+ */
+
+/*
+ * Test for `flock` syscall (`flock(LOCK_EX/LOCK_SH/LOCK_UN`). We assert that the calls succeed (or
+ * taking a lock fails), and log all details for debugging purposes.
+ *
+ * The tests involve multithreaded, dup and file-backed mmap cases. We don't add multi-process cases
+ * here because they are already covered by LTP tests `flock03` and `flock04`.
+ */
+
+#define _GNU_SOURCE
+#include <assert.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/file.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include "common.h"
+
+#define TEST_FILE "tmp/flock_file"
+#define FILE_SIZE 1024
+
+struct thread_args {
+    int pipes[2][2];
+};
+
+static const char* str_type(int type) {
+    switch (type) {
+        case LOCK_SH: return "LOCK_SH";
+        case LOCK_EX: return "LOCK_EX";
+        case LOCK_UN: return "LOCK_UN";
+        case LOCK_SH | LOCK_NB: return "LOCK_SH | LOCK_NB";
+        case LOCK_EX | LOCK_NB: return "LOCK_EX | LOCK_NB";
+        default: return "???";
+    }
+}
+
+static void try_flock(int fd, int operation, int expect_ret) {
+    int ret = flock(fd, operation);
+    if (ret != expect_ret) {
+        errx(1, "flock(%d, %s) error with return value = %d, expected value = %d",
+             fd, str_type(operation), ret, expect_ret);
+    }
+}
+
+static void open_pipes(int pipes[2][2]) {
+    for (unsigned int i = 0; i < 2; i++) {
+        CHECK(pipe(pipes[i]));
+    }
+}
+
+static void close_pipes(int pipes[2][2]) {
+    for (unsigned int i = 0; i < 2; i++) {
+        for (unsigned int j = 0; j < 2; j++) {
+            CHECK(close(pipes[i][j]));
+        }
+    }
+}
+
+static void write_pipe(int pipe[2]) {
+    char c = 0;
+    int ret;
+    do {
+        ret = write(pipe[1], &c, sizeof(c));
+    } while (ret == -1 && errno == EINTR);
+    if (ret == -1)
+        err(1, "write");
+}
+
+static void read_pipe(int pipe[2]) {
+    char c;
+    int ret;
+    do {
+        ret = read(pipe[0], &c, sizeof(c));
+    } while (ret == -1 && errno == EINTR);
+    if (ret == -1)
+        err(1, "read");
+    if (ret == 0)
+        err(1, "pipe closed");
+}
+
+static void test_flock_dup_open(void) {
+    printf("testing locks with the dup and open...\n");
+    int fd = CHECK(open(TEST_FILE, O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC, 0600));
+    try_flock(fd, LOCK_EX, 0);
+
+    int fd2 = CHECK(dup(fd));
+    try_flock(fd2, LOCK_EX, 0);
+    try_flock(fd, LOCK_UN, 0);
+    try_flock(fd2, LOCK_EX, 0);
+
+    int fd3 = CHECK(open(TEST_FILE, O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC, 0600));
+    CHECK(close(fd));
+    try_flock(fd3, LOCK_EX | LOCK_NB, -1);
+    CHECK(close(fd2));
+    try_flock(fd3, LOCK_EX | LOCK_NB, 0);
+    CHECK(close(fd3));
+}
+
+static void test_mmap_flock_close_unmap(void) {
+    printf("testing locks with the mmap and flock...\n");
+    int fd1, fd2;
+    void* file_data;
+
+    fd1 = CHECK(open(TEST_FILE, O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC, 0600));
+    file_data = mmap(NULL, FILE_SIZE, PROT_READ, MAP_SHARED, fd1, 0);
+    if (file_data == MAP_FAILED) {
+        err(1, "mmap");
+    }
+    try_flock(fd1, LOCK_EX, 0);
+    CHECK(close(fd1));
+    fd2 = CHECK(open(TEST_FILE, O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC, 0600));
+    try_flock(fd2, LOCK_EX | LOCK_NB, -1);
+    CHECK(munmap(file_data, FILE_SIZE));
+    try_flock(fd2, LOCK_EX, 0);
+    CHECK(close(fd2));
+}
+
+static void* thread_flock_first(void* arg) {
+    struct thread_args* args = (struct thread_args*)arg;
+
+    int fd = CHECK(open(TEST_FILE, O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC, 0600));
+    try_flock(fd, LOCK_EX | LOCK_NB, 0);
+    write_pipe(args->pipes[0]);
+    read_pipe(args->pipes[1]);
+    try_flock(fd, LOCK_UN, 0);
+    write_pipe(args->pipes[0]);
+    read_pipe(args->pipes[1]);
+    try_flock(fd, LOCK_SH | LOCK_NB, 0);
+    CHECK(close(fd));
+
+    return arg;
+}
+
+static void* thread_flock_second(void* arg) {
+    struct thread_args* args = (struct thread_args*)arg;
+
+    int fd = CHECK(open(TEST_FILE, O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC, 0600));
+    read_pipe(args->pipes[0]);
+    try_flock(fd, LOCK_EX | LOCK_NB, -1);
+    write_pipe(args->pipes[1]);
+    read_pipe(args->pipes[0]);
+    try_flock(fd, LOCK_SH | LOCK_NB, 0);
+    write_pipe(args->pipes[1]);
+    CHECK(close(fd));
+
+    return arg;
+}
+
+static void test_flock_multithread(void) {
+    printf("testing flock with multithread...\n");
+    int ret;
+    pthread_t threads[2];
+    struct thread_args args = {0};
+    open_pipes(args.pipes);
+
+    ret = pthread_create(&threads[0], NULL, thread_flock_first, (void*)&args);
+    if (ret != 0)
+        errx(1, "pthread_create");
+
+    ret = pthread_create(&threads[1], NULL, thread_flock_second, (void*)&args);
+    if (ret != 0)
+        errx(1, "pthread_create");
+
+    for (int i = 0; i < 2; i++) {
+        if ((ret = pthread_join(threads[i], NULL)) != 0) {
+            errx(1, "pthread_join");
+        }
+    }
+    close_pipes(args.pipes);
+}
+
+int main(void) {
+    setbuf(stdout, NULL);
+
+    test_flock_dup_open();
+    test_mmap_flock_close_unmap();
+    test_flock_multithread();
+
+    CHECK(unlink(TEST_FILE));
+    printf("TEST OK\n");
+    return 0;
+}
diff --git a/libos/test/regression/meson.build b/libos/test/regression/meson.build
index 9ef611b8a7..15cd4a39dd 100644
--- a/libos/test/regression/meson.build
+++ b/libos/test/regression/meson.build
@@ -33,6 +33,7 @@ tests = {
     'fdleak': {},
     'file_check_policy': {},
     'file_size': {},
+    'flock_lock': {},
     'fopen_cornercases': {},
     'fork_and_exec': {},
     'fp_multithread': {
diff --git a/libos/test/regression/test_libos.py b/libos/test/regression/test_libos.py
index fb5fbc3f73..3d3fbf039a 100644
--- a/libos/test/regression/test_libos.py
+++ b/libos/test/regression/test_libos.py
@@ -969,6 +969,14 @@ def test_130_sid(self):
         stdout, _ = self.run_binary(['sid'])
         self.assertIn("TEST OK", stdout)
 
+    def test_140_flock_lock(self):
+        try:
+            stdout, _ = self.run_binary(['flock_lock'])
+        finally:
+            if os.path.exists('tmp/flock_file'):
+                os.remove('tmp/flock_file')
+        self.assertIn('TEST OK', stdout)
+
 class TC_31_Syscall(RegressionTestCase):
     def test_000_syscall_redirect(self):
         stdout, _ = self.run_binary(['syscall'])
diff --git a/libos/test/regression/tests.toml b/libos/test/regression/tests.toml
index 498ab28d9d..09d9dbca90 100644
--- a/libos/test/regression/tests.toml
+++ b/libos/test/regression/tests.toml
@@ -36,6 +36,7 @@ manifests = [
   "file_check_policy_allow_all_but_log",
   "file_check_policy_strict",
   "file_size",
+  "flock_lock",
   "fopen_cornercases",
   "fork_and_exec",
   "fork_disallowed",
diff --git a/libos/test/regression/tests_musl.toml b/libos/test/regression/tests_musl.toml
index b747158852..7ea4655c2d 100644
--- a/libos/test/regression/tests_musl.toml
+++ b/libos/test/regression/tests_musl.toml
@@ -38,6 +38,7 @@ manifests = [
   "file_check_policy_allow_all_but_log",
   "file_check_policy_strict",
   "file_size",
+  "flock_lock",
   "fopen_cornercases",
   "fork_and_exec",
   "fork_disallowed",