summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaxim Cournoyer <maxim@guixotic.coop>2025-10-21 23:22:24 +0900
committerMaxim Cournoyer <maxim@guixotic.coop>2025-10-30 16:13:03 +0900
commite1994a021437b3fd73089c08d7e8db876fad698d (patch)
tree4c277554d2167559e9325afc191c53e262733918
parent36a90a1a044e9e141da71f6ff9c7fcf68bcf3016 (diff)
syscalls: Add mmap support.
* guix/build/syscalls.scm (PROT_NONE, PROT_READ, PROT_WRITE, PROT_EXEC) (PROT_SEM, MAP_SHARED, MAP_PRIVATE, MAP_FAILED) (MS_ASYNC, MS_INVALIDATE, MS_SYNC) (%mmap-guardian, %unmapped-bytevectors): New variables. (unmapped-bytevector?, pump-mmap-guardian, %mmap, mmap, %munmap, munmap) (%msync, msync): New procedures. * guix/build/io.scm: New file. * Makefile.am: Register it. * tests/syscalls.scm (strace-output): New variable. ("mmap and munmap", "file->bytevector, reading", "file->bytevector, writing") ("manual munmap does not lead to double free"): New tests. Change-Id: I19ec687899eda635559e91200dd8d98669b0e35f
-rw-r--r--Makefile.am1
-rw-r--r--guix/build/io.scm58
-rw-r--r--guix/build/syscalls.scm112
-rw-r--r--tests/syscalls.scm70
4 files changed, 238 insertions, 3 deletions
diff --git a/Makefile.am b/Makefile.am
index a4e7277d6d4..a6c2e73388f 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -265,6 +265,7 @@ MODULES = \
265 guix/build/kconfig.scm \ 265 guix/build/kconfig.scm \
266 guix/build/linux-module-build-system.scm \ 266 guix/build/linux-module-build-system.scm \
267 guix/build/store-copy.scm \ 267 guix/build/store-copy.scm \
268 guix/build/io.scm \
268 guix/build/json.scm \ 269 guix/build/json.scm \
269 guix/build/pack.scm \ 270 guix/build/pack.scm \
270 guix/build/utils.scm \ 271 guix/build/utils.scm \
diff --git a/guix/build/io.scm b/guix/build/io.scm
new file mode 100644
index 00000000000..1dddbf239cb
--- /dev/null
+++ b/guix/build/io.scm
@@ -0,0 +1,58 @@
1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2025 Maxim Cournoyer <maxim@guixotic.coop>
3;;;
4;;; This file is part of GNU Guix.
5;;;
6;;; GNU Guix is free software; you can redistribute it and/or modify it
7;;; under the terms of the GNU General Public License as published by
8;;; the Free Software Foundation; either version 3 of the License, or (at
9;;; your option) any later version.
10;;;
11;;; GNU Guix is distributed in the hope that it will be useful, but
12;;; WITHOUT ANY WARRANTY; without even the implied warranty of
13;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14;;; GNU General Public License for more details.
15;;;
16;;; You should have received a copy of the GNU General Public License
17;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
18
19(define-module (guix build io)
20 #:use-module (guix build syscalls)
21 #:use-module (ice-9 format)
22 #:use-module (rnrs bytevectors)
23 #:use-module (rnrs io ports)
24 #:use-module (system foreign)
25 #:export (file->bytevector)
26 ;; For convenience.
27 #:re-export (PROT_READ
28 PROT_NONE
29 PROT_READ
30 PROT_WRITE
31 PROT_EXEC
32 PROT_SEM
33 MAP_SHARED
34 MAP_PRIVATE
35 MAP_FAILED
36 munmap))
37
38;;;
39;;; Memory mapped files.
40;;;
41
42(define* (file->bytevector file #:key
43 (protection PROT_READ)
44 (flags (if (logtest PROT_WRITE protection)
45 MAP_SHARED
46 MAP_PRIVATE))
47 (offset 0))
48 "Return a bytevector object that is backed by a memory mapped FILE. This
49avoids eagerly copying the full file contents into memory, instead letting the
50kernel lazily page it in on demand. The underlying memory map is
51automatically unmapped when the bytevector is no longer referenced."
52 (let* ((mode (format #f "rb~:[~;+~]" (and (logtest PROT_WRITE protection)
53 (logtest MAP_SHARED flags))))
54 (port (open-file file mode)))
55 (call-with-port port
56 (lambda (port)
57 (mmap (fileno port) (- (stat:size (stat file)) offset)
58 #:protection protection #:flags flags #:offset offset)))))
diff --git a/guix/build/syscalls.scm b/guix/build/syscalls.scm
index d40b1ae5d93..ef678754706 100644
--- a/guix/build/syscalls.scm
+++ b/guix/build/syscalls.scm
@@ -42,8 +42,23 @@
42 #:use-module (ice-9 regex) 42 #:use-module (ice-9 regex)
43 #:use-module (ice-9 match) 43 #:use-module (ice-9 match)
44 #:use-module (ice-9 ftw) 44 #:use-module (ice-9 ftw)
45 #:use-module (ice-9 threads) 45 #:export (PROT_NONE
46 #:export (MS_RDONLY 46 PROT_READ
47 PROT_WRITE
48 PROT_EXEC
49 PROT_SEM
50 MAP_SHARED
51 MAP_PRIVATE
52 MAP_FAILED
53 mmap
54 munmap
55
56 MS_ASYNC
57 MS_INVALIDATE
58 MS_SYNC
59 msync
60
61 MS_RDONLY
47 MS_NOSUID 62 MS_NOSUID
48 MS_NODEV 63 MS_NODEV
49 MS_NOEXEC 64 MS_NOEXEC
@@ -1108,6 +1123,99 @@ backend device."
1108 1123
1109 1124
1110;;; 1125;;;
1126;;; Memory maps.
1127;;;
1128
1129;;; Constants from <sys/mman.h>
1130(define PROT_NONE #x0) ;page can not be accessed
1131(define PROT_READ #x1) ;page can be read
1132(define PROT_WRITE #x2) ;page can be written
1133(define PROT_EXEC #x4) ;page can be executed
1134(define PROT_SEM #x8) ;page can be used for atomic operations
1135
1136(define MAP_SHARED #x01) ;share changes with other processes
1137(define MAP_PRIVATE #x02) ;private copy-on-write mapping
1138(define MAP_FAILED #xffffffffffffffff) ;mmap failure sentinel
1139
1140(define %mmap
1141 (syscall->procedure '* "mmap" (list '* size_t int int int long)))
1142
1143(define %mmap-guardian
1144 (make-guardian))
1145
1146(define %unmapped-bytevectors
1147 (make-weak-key-hash-table))
1148
1149(define (unmapped-bytevector? bv)
1150 "True if the bytevector BV was already munmap'd."
1151 (hashq-ref %unmapped-bytevectors bv #f))
1152
1153(define (pump-mmap-guardian)
1154 (let ((bv (%mmap-guardian)))
1155 (when bv
1156 (if (unmapped-bytevector? bv)
1157 (hashq-remove! %unmapped-bytevectors bv)
1158 (munmap bv))
1159 (pump-mmap-guardian))))
1160
1161(add-hook! after-gc-hook pump-mmap-guardian)
1162
1163(define* (mmap fd len #:key
1164 (protection PROT_READ)
1165 (flags (if (logtest PROT_WRITE protection)
1166 MAP_SHARED
1167 MAP_PRIVATE))
1168 (offset 0))
1169 "Return a bytevector to a memory-mapped region of length LEN bytes
1170for the open file descriptor FD. The mapping is created with the given memory
1171PROTECTION and FLAGS, biwise-or of PROT_* and MAP_* constants which
1172determine whether updates are visible to other processes and/or carried
1173through to the underlying file. Raise a 'system-error' exception on error.
1174The memory is automatically unmapped with `munmap' when the bytevector object
1175is no longer referenced."
1176 (let-values (((ptr err) (%mmap %null-pointer len protection flags fd offset)))
1177 (when (= MAP_FAILED (pointer-address ptr))
1178 (throw 'system-error "mmap" "mmap ~S with len ~S: ~A"
1179 (list fd len (strerror err))
1180 (list err)))
1181 (let ((bv (pointer->bytevector ptr len)))
1182 (%mmap-guardian bv)
1183 bv)))
1184
1185(define %munmap
1186 (syscall->procedure int "munmap" (list '* size_t)))
1187
1188(define (munmap bv)
1189 "Unmap the memory region described by BV, a bytevector object."
1190 (let*-values (((ptr) (bytevector->pointer bv))
1191 ((len) (bytevector-length bv))
1192 ((ret err) (%munmap ptr len)))
1193 (unless (zero? ret)
1194 (throw 'system-error "munmap" "munmap ~S with len ~S: ~A"
1195 (list ptr len (strerror err))
1196 (list err)))
1197 (hashq-set! %unmapped-bytevectors bv #t)))
1198
1199(define MS_ASYNC 1) ;sync memory asynchronously
1200(define MS_INVALIDATE 2) ;invalidate the caches
1201(define MS_SYNC 4) ;synchronous memory sync
1202
1203(define %msync
1204 (syscall->procedure int "msync" (list '* size_t int)))
1205
1206(define* (msync bv #:key (flags MS_SYNC))
1207 "Flush changes made to the in-core copy of a file that was mapped into memory
1208using `mmap' back to the file system."
1209 (let*-values (((ptr) (bytevector->pointer bv))
1210 ((len) (bytevector-length bv))
1211 ((ret err) (%msync ptr len flags)))
1212 (unless (zero? ret)
1213 (throw 'system-error "msync" "msync ~S with len ~S: ~A"
1214 (list ptr len (strerror err))
1215 (list err)))))
1216
1217
1218;;;
1111;;; Random. 1219;;; Random.
1112;;; 1220;;;
1113 1221
diff --git a/tests/syscalls.scm b/tests/syscalls.scm
index a0483e68f08..1ea49b0acc4 100644
--- a/tests/syscalls.scm
+++ b/tests/syscalls.scm
@@ -22,8 +22,11 @@
22 22
23(define-module (test-syscalls) 23(define-module (test-syscalls)
24 #:use-module (guix utils) 24 #:use-module (guix utils)
25 #:use-module (guix build io)
25 #:use-module (guix build syscalls) 26 #:use-module (guix build syscalls)
27 #:use-module (guix build utils)
26 #:use-module (gnu build linux-container) 28 #:use-module (gnu build linux-container)
29 #:use-module (rnrs bytevectors)
27 #:use-module (srfi srfi-1) 30 #:use-module (srfi srfi-1)
28 #:use-module (srfi srfi-26) 31 #:use-module (srfi srfi-26)
29 #:use-module (srfi srfi-64) 32 #:use-module (srfi srfi-64)
@@ -31,7 +34,7 @@
31 #:use-module (system foreign) 34 #:use-module (system foreign)
32 #:use-module ((ice-9 ftw) #:select (scandir)) 35 #:use-module ((ice-9 ftw) #:select (scandir))
33 #:use-module (ice-9 match) 36 #:use-module (ice-9 match)
34 #:use-module (ice-9 threads)) 37 #:use-module (ice-9 textual-ports))
35 38
36;; Test the (guix build syscalls) module, although there's not much that can 39;; Test the (guix build syscalls) module, although there's not much that can
37;; actually be tested without being root. 40;; actually be tested without being root.
@@ -39,6 +42,9 @@
39(define temp-file 42(define temp-file
40 (string-append "t-utils-" (number->string (getpid)))) 43 (string-append "t-utils-" (number->string (getpid))))
41 44
45(define strace-output
46 (string-append "t-utils-strace" (number->string (getpid))))
47
42 48
43(test-begin "syscalls") 49(test-begin "syscalls")
44 50
@@ -735,6 +741,68 @@
735 (member (system-error-errno args) 741 (member (system-error-errno args)
736 (list EPERM ENOSYS))))) 742 (list EPERM ENOSYS)))))
737 743
744(test-assert "mmap and munmap"
745 (begin
746 (call-with-output-file temp-file
747 (lambda (p)
748 (display "abcdefghij")))
749 (let* ((len 5)
750 (bv (mmap (open-fdes temp-file O_RDONLY) len)))
751 (munmap bv))))
752
753(test-equal "file->bytevector, reading"
754 #\6
755 (begin
756 (call-with-output-file temp-file
757 (lambda (p)
758 (display "0123456789\n" p)))
759 (sync)
760 (integer->char
761 (bytevector-u8-ref (file->bytevector temp-file) 6))))
762
763(test-equal "file->bytevector, writing"
764 "0000000700"
765 (begin
766 (call-with-output-file temp-file
767 (lambda (p)
768 (display "0000000000" p)))
769 (sync)
770 (let ((bv (file->bytevector temp-file
771 #:protection PROT_WRITE)))
772
773 (bytevector-u8-set! bv 7 (char->integer #\7))
774 (msync bv)) ;ensure the file gets written
775 (call-with-input-file temp-file get-string-all)))
776
777(unless (which "strace")
778 (test-skip 1))
779;;; This test currently fails, due to protected items in a guardian being
780;;; dropped from weak hash tables (see:
781;;; <https://codeberg.org/guile/guile/issues/44>).
782(test-expect-fail 1)
783(test-equal "manual munmap does not lead to double free"
784 1 ;single munmap call
785 (begin
786 (call-with-output-file temp-file
787 (lambda (p)
788 (display "something interesting\n" p)))
789 (sync)
790 (gc)
791 (system (string-append "strace -o " strace-output
792 " -p " (number->string (getpid))
793 " -e trace=munmap &"))
794 (sleep 1) ;allow strace to start
795 (let ((bv (file->bytevector temp-file)))
796 (munmap bv))
797 (gc)
798 (sync)
799 (let ((text (call-with-input-file strace-output get-string-all)))
800 ;; The address seen by strace is not the same as the one seen by Guile,
801 ;; so we can't use it in the pattern.
802 (length (filter (cut string-prefix? "munmap(0x" <>)
803 (string-split text #\newline))))))
804
738(test-end) 805(test-end)
739 806
740(false-if-exception (delete-file temp-file)) 807(false-if-exception (delete-file temp-file))
808(false-if-exception (delete-file strace-output))