Skip to content

Commit 99fe7ed

Browse files
authored
Merge pull request #80 from Max042004/fix-fcntl-flock-ltype
Translate fcntl flock l_type between Linux and macOS
2 parents 43f4763 + 63e7a93 commit 99fe7ed

3 files changed

Lines changed: 137 additions & 2 deletions

File tree

src/syscall/fs.c

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -768,11 +768,33 @@ int64_t sys_fcntl(guest_t *g, int fd, int cmd, uint64_t arg)
768768
memcpy(&l_start, lflock + 8, 8); /* offset 8 due to padding */
769769
memcpy(&l_len, lflock + 16, 8);
770770

771+
/* l_type constants differ between Linux and macOS/BSD:
772+
* Linux: F_RDLCK=0, F_WRLCK=1, F_UNLCK=2
773+
* macOS: F_RDLCK=1, F_UNLCK=2, F_WRLCK=3
774+
* Passing the Linux value straight through makes a Linux F_RDLCK (0)
775+
* an invalid type on macOS, which fcntl() rejects with EINVAL. This is
776+
* the lock POSIX databases (e.g. SQLite) take first, so it must map. */
777+
short mac_type;
778+
switch (l_type) {
779+
case 0: /* LINUX_F_RDLCK */
780+
mac_type = F_RDLCK;
781+
break;
782+
case 1: /* LINUX_F_WRLCK */
783+
mac_type = F_WRLCK;
784+
break;
785+
case 2: /* LINUX_F_UNLCK */
786+
mac_type = F_UNLCK;
787+
break;
788+
default:
789+
host_fd_ref_close(&host_ref);
790+
return -LINUX_EINVAL;
791+
}
792+
771793
struct flock mac_fl = {
772794
.l_start = l_start,
773795
.l_len = l_len,
774796
.l_pid = 0,
775-
.l_type = l_type, /* F_RDLCK=0, F_WRLCK=1, F_UNLCK=2 same on both */
797+
.l_type = mac_type,
776798
.l_whence = l_whence, /* SEEK_SET=0, SEEK_CUR=1, SEEK_END=2 same */
777799
};
778800

@@ -784,7 +806,20 @@ int64_t sys_fcntl(guest_t *g, int fd, int cmd, uint64_t arg)
784806

785807
/* For F_GETLK, write back the result */
786808
if (cmd == 5) {
787-
int16_t rt = mac_fl.l_type, rw = mac_fl.l_whence;
809+
/* Map macOS l_type back to Linux constants (see above). */
810+
int16_t rt;
811+
switch (mac_fl.l_type) {
812+
case F_RDLCK:
813+
rt = 0; /* LINUX_F_RDLCK */
814+
break;
815+
case F_WRLCK:
816+
rt = 1; /* LINUX_F_WRLCK */
817+
break;
818+
default:
819+
rt = 2; /* LINUX_F_UNLCK */
820+
break;
821+
}
822+
int16_t rw = mac_fl.l_whence;
788823
int64_t rs = mac_fl.l_start, rl = mac_fl.l_len;
789824
int32_t rp = mac_fl.l_pid;
790825
memset(lflock, 0, sizeof(lflock));

tests/manifest.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ test-socket
4242

4343
[section] Syscall coverage tests
4444
test-file-ops
45+
test-flock
4546
test-sysinfo
4647
test-io-opt
4748
test-syscall-smoke

tests/test-flock.c

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/* Test POSIX advisory record locking via fcntl(F_SETLK/F_GETLK/F_SETLKW)
2+
*
3+
* Copyright 2026 elfuse contributors
4+
* Copyright 2025 Moritz Angermann, zw3rk pte. ltd.
5+
* SPDX-License-Identifier: Apache-2.0
6+
*
7+
* Regression coverage for the Linux<->macOS struct flock translation. The
8+
* l_type constants differ between the two ABIs (Linux F_RDLCK=0/F_WRLCK=1,
9+
* macOS F_RDLCK=1/F_WRLCK=3), so passing the guest value straight through to
10+
* the host made the very first lock SQLite takes (a shared F_RDLCK) fail with
11+
* EINVAL and surface as "disk I/O error". The byte offsets below mirror the
12+
* ones SQLite locks around its 1GiB "pending byte".
13+
*/
14+
15+
#include <errno.h>
16+
#include <fcntl.h>
17+
#include <stdlib.h>
18+
#include <string.h>
19+
#include <unistd.h>
20+
21+
#include "test-harness.h"
22+
23+
#define PENDING_BYTE 0x40000000L
24+
#define RESERVED_BYTE (PENDING_BYTE + 1)
25+
#define SHARED_FIRST (PENDING_BYTE + 2)
26+
#define SHARED_SIZE 510
27+
28+
static int set_lock(int fd, short type, off_t start, off_t len)
29+
{
30+
struct flock fl = {
31+
.l_type = type,
32+
.l_whence = SEEK_SET,
33+
.l_start = start,
34+
.l_len = len,
35+
};
36+
return fcntl(fd, F_SETLK, &fl);
37+
}
38+
39+
int main(void)
40+
{
41+
int passes = 0, fails = 0;
42+
const char *path = "/tmp/elfuse-test-flock.db";
43+
44+
int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644);
45+
if (fd < 0) {
46+
perror("open");
47+
return 1;
48+
}
49+
50+
/* Shared read lock -- this is the call that regressed to EINVAL. */
51+
TEST("F_SETLK F_RDLCK (shared)");
52+
EXPECT_EQ(set_lock(fd, F_RDLCK, SHARED_FIRST, SHARED_SIZE), 0,
53+
"shared read lock rejected");
54+
55+
/* Promote the pending byte to a write lock, then drop it. */
56+
TEST("F_SETLK F_WRLCK (pending)");
57+
EXPECT_EQ(set_lock(fd, F_WRLCK, PENDING_BYTE, 1), 0,
58+
"pending write lock rejected");
59+
60+
TEST("F_SETLK F_WRLCK (reserved)");
61+
EXPECT_EQ(set_lock(fd, F_WRLCK, RESERVED_BYTE, 1), 0,
62+
"reserved write lock rejected");
63+
64+
TEST("F_SETLK F_UNLCK (release shared)");
65+
EXPECT_EQ(set_lock(fd, F_UNLCK, SHARED_FIRST, SHARED_SIZE), 0,
66+
"unlock rejected");
67+
68+
/* Blocking variant must take the same translation path. */
69+
TEST("F_SETLKW F_WRLCK");
70+
struct flock wfl = {
71+
.l_type = F_WRLCK,
72+
.l_whence = SEEK_SET,
73+
.l_start = 0,
74+
.l_len = 16,
75+
};
76+
EXPECT_EQ(fcntl(fd, F_SETLKW, &wfl), 0, "F_SETLKW rejected");
77+
78+
/* F_GETLK on a region this process already write-locks must report back a
79+
* Linux l_type. Linux reports F_UNLCK for locks held by the *same* owner,
80+
* so the only thing we can assert portably is that the type round-trips to
81+
* a valid Linux constant and the call succeeds. */
82+
TEST("F_GETLK round-trips l_type");
83+
struct flock gfl = {
84+
.l_type = F_WRLCK,
85+
.l_whence = SEEK_SET,
86+
.l_start = 0,
87+
.l_len = 16,
88+
};
89+
int gr = fcntl(fd, F_GETLK, &gfl);
90+
EXPECT_TRUE(gr == 0 && (gfl.l_type == F_UNLCK || gfl.l_type == F_RDLCK ||
91+
gfl.l_type == F_WRLCK),
92+
"F_GETLK returned an invalid l_type");
93+
94+
close(fd);
95+
unlink(path);
96+
97+
SUMMARY("test-flock");
98+
return fails == 0 ? 0 : 1;
99+
}

0 commit comments

Comments
 (0)