1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
|
Origin: https://github.com/git/git/commit/6e7ad1e4c22e7038975ba37c7413374fe566b064
Origin: https://github.com/git/git/commit/bdc77d1d685be9c10b88abb281a42bc620548595
Origin: https://github.com/git/git/commit/8959555cee7ec045958f9b6dd62e541affb7e7d9
Origin: https://github.com/git/git/commit/cb95038137e9e66fc6a6b4a0e8db62bcc521b709
Origin: https://github.com/git/git/commit/e47363e5a8bdf5144059d664c45c0975243ef05b
Origin: https://github.com/git/git/commit/bb50ec3cc300eeff3aba7a2bea145aabdb477d31
Origin: https://github.com/git/git/commit/0f85c4a30b072a26d74af8bbf63cc8f6a5dfc1b8
Reviewed-by: Aron Xu <aron@debian.org>
Last-Updated: 2023-01-26
Comment: With the above patches, we are effectively updating the package to v2.30.3
plus some more changes. The symlink change to RelNotes has been dropped.
diff --git a/Documentation/RelNotes/2.30.3.txt b/Documentation/RelNotes/2.30.3.txt
new file mode 100644
index 0000000..31b2a4d
--- /dev/null
+++ b/Documentation/RelNotes/2.30.3.txt
@@ -0,0 +1,24 @@
+Git v2.30.2 Release Notes
+=========================
+
+This release addresses the security issue CVE-2022-24765.
+
+Fixes since v2.30.2
+-------------------
+
+ * Build fix on Windows.
+
+ * Fix `GIT_CEILING_DIRECTORIES` with Windows-style root directories.
+
+ * CVE-2022-24765:
+ On multi-user machines, Git users might find themselves
+ unexpectedly in a Git worktree, e.g. when another user created a
+ repository in `C:\.git`, in a mounted network drive or in a
+ scratch space. Merely having a Git-aware prompt that runs `git
+ status` (or `git diff`) and navigating to a directory which is
+ supposedly not a Git worktree, or opening such a directory in an
+ editor or IDE such as VS Code or Atom, will potentially run
+ commands defined by that other user.
+
+Credit for finding this vulnerability goes to 俞晨东; The fix was
+authored by Johannes Schindelin.
diff --git a/Documentation/config.txt b/Documentation/config.txt
index 6ba50b1..34e6d47 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -438,6 +438,8 @@ include::config/rerere.txt[]
include::config/reset.txt[]
+include::config/safe.txt[]
+
include::config/sendemail.txt[]
include::config/sequencer.txt[]
diff --git a/Documentation/config/safe.txt b/Documentation/config/safe.txt
new file mode 100644
index 0000000..6d764fe
--- /dev/null
+++ b/Documentation/config/safe.txt
@@ -0,0 +1,28 @@
+safe.directory::
+ These config entries specify Git-tracked directories that are
+ considered safe even if they are owned by someone other than the
+ current user. By default, Git will refuse to even parse a Git
+ config of a repository owned by someone else, let alone run its
+ hooks, and this config setting allows users to specify exceptions,
+ e.g. for intentionally shared repositories (see the `--shared`
+ option in linkgit:git-init[1]).
++
+This is a multi-valued setting, i.e. you can add more than one directory
+via `git config --add`. To reset the list of safe directories (e.g. to
+override any such directories specified in the system config), add a
+`safe.directory` entry with an empty value.
++
+This config setting is only respected when specified in a system or global
+config, not when it is specified in a repository config or via the command
+line option `-c safe.directory=<path>`.
++
+The value of this setting is interpolated, i.e. `~/<path>` expands to a
+path relative to the home directory and `%(prefix)/<path>` expands to a
+path relative to Git's (runtime) prefix.
++
+To completely opt-out of this security check, set `safe.directory` to the
+string `*`. This will allow all repositories to be treated as if their
+directory was listed in the `safe.directory` list. If `safe.directory=*`
+is set in system config and you want to re-enable this protection, then
+initialize your list with an empty value before listing the repositories
+that you deem safe.
diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN
index 9d789e0..7cf68be 100755
--- a/GIT-VERSION-GEN
+++ b/GIT-VERSION-GEN
@@ -1,7 +1,7 @@
#!/bin/sh
GVF=GIT-VERSION-FILE
-DEF_VER=v2.30.2
+DEF_VER=v2.30.3
LF='
'
diff --git a/compat/mingw.c b/compat/mingw.c
index a435998..38ac359 100644
--- a/compat/mingw.c
+++ b/compat/mingw.c
@@ -1,5 +1,6 @@
#include "../git-compat-util.h"
#include "win32.h"
+#include <aclapi.h>
#include <conio.h>
#include <wchar.h>
#include "../strbuf.h"
@@ -1060,6 +1061,7 @@ int pipe(int filedes[2])
return 0;
}
+#ifndef __MINGW64__
struct tm *gmtime_r(const time_t *timep, struct tm *result)
{
if (gmtime_s(result, timep) == 0)
@@ -1073,6 +1075,7 @@ struct tm *localtime_r(const time_t *timep, struct tm *result)
return result;
return NULL;
}
+#endif
char *mingw_getcwd(char *pointer, int len)
{
@@ -2599,6 +2602,92 @@ static void setup_windows_environment(void)
}
}
+static PSID get_current_user_sid(void)
+{
+ HANDLE token;
+ DWORD len = 0;
+ PSID result = NULL;
+
+ if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token))
+ return NULL;
+
+ if (!GetTokenInformation(token, TokenUser, NULL, 0, &len)) {
+ TOKEN_USER *info = xmalloc((size_t)len);
+ if (GetTokenInformation(token, TokenUser, info, len, &len)) {
+ len = GetLengthSid(info->User.Sid);
+ result = xmalloc(len);
+ if (!CopySid(len, result, info->User.Sid)) {
+ error(_("failed to copy SID (%ld)"),
+ GetLastError());
+ FREE_AND_NULL(result);
+ }
+ }
+ FREE_AND_NULL(info);
+ }
+ CloseHandle(token);
+
+ return result;
+}
+
+int is_path_owned_by_current_sid(const char *path)
+{
+ WCHAR wpath[MAX_PATH];
+ PSID sid = NULL;
+ PSECURITY_DESCRIPTOR descriptor = NULL;
+ DWORD err;
+
+ static wchar_t home[MAX_PATH];
+
+ int result = 0;
+
+ if (xutftowcs_path(wpath, path) < 0)
+ return 0;
+
+ /*
+ * On Windows, the home directory is owned by the administrator, but for
+ * all practical purposes, it belongs to the user. Do pretend that it is
+ * owned by the user.
+ */
+ if (!*home) {
+ DWORD size = ARRAY_SIZE(home);
+ DWORD len = GetEnvironmentVariableW(L"HOME", home, size);
+ if (!len || len > size)
+ wcscpy(home, L"::N/A::");
+ }
+ if (!wcsicmp(wpath, home))
+ return 1;
+
+ /* Get the owner SID */
+ err = GetNamedSecurityInfoW(wpath, SE_FILE_OBJECT,
+ OWNER_SECURITY_INFORMATION |
+ DACL_SECURITY_INFORMATION,
+ &sid, NULL, NULL, NULL, &descriptor);
+
+ if (err != ERROR_SUCCESS)
+ error(_("failed to get owner for '%s' (%ld)"), path, err);
+ else if (sid && IsValidSid(sid)) {
+ /* Now, verify that the SID matches the current user's */
+ static PSID current_user_sid;
+
+ if (!current_user_sid)
+ current_user_sid = get_current_user_sid();
+
+ if (current_user_sid &&
+ IsValidSid(current_user_sid) &&
+ EqualSid(sid, current_user_sid))
+ result = 1;
+ }
+
+ /*
+ * We can release the security descriptor struct only now because `sid`
+ * actually points into this struct.
+ */
+ if (descriptor)
+ LocalFree(descriptor);
+
+ return result;
+}
+
int is_valid_win32_path(const char *path, int allow_literal_nul)
{
const char *p = path;
diff --git a/compat/mingw.h b/compat/mingw.h
index af8eddd..f6bab54 100644
--- a/compat/mingw.h
+++ b/compat/mingw.h
@@ -452,6 +452,13 @@ char *mingw_query_user_email(void);
#include <inttypes.h>
#endif
+/**
+ * Verifies that the specified path is owned by the user running the
+ * current process.
+ */
+int is_path_owned_by_current_sid(const char *path);
+#define is_path_owned_by_current_user is_path_owned_by_current_sid
+
/**
* Verifies that the given path is a valid one on Windows.
*
diff --git a/git-compat-util.h b/git-compat-util.h
index 7d3db43..63ba89d 100644
--- a/git-compat-util.h
+++ b/git-compat-util.h
@@ -127,7 +127,9 @@
/* Approximation of the length of the decimal representation of this type. */
#define decimal_length(x) ((int)(sizeof(x) * 2.56 + 0.5) + 1)
-#if defined(__sun__)
+#ifdef __MINGW64__
+#define _POSIX_C_SOURCE 1
+#elif defined(__sun__)
/*
* On Solaris, when _XOPEN_EXTENDED is set, its header file
* forces the programs to be XPG4v2, defeating any _XOPEN_SOURCE
@@ -390,6 +392,18 @@ static inline int git_offset_1st_component(const char *path)
#define is_valid_path(path) 1
#endif
+#ifndef is_path_owned_by_current_user
+static inline int is_path_owned_by_current_uid(const char *path)
+{
+ struct stat st;
+ if (lstat(path, &st))
+ return 0;
+ return st.st_uid == geteuid();
+}
+
+#define is_path_owned_by_current_user is_path_owned_by_current_uid
+#endif
+
#ifndef find_last_dir_sep
static inline char *git_find_last_dir_sep(const char *path)
{
diff --git a/path.c b/path.c
index 7b385e5..853e716 100644
--- a/path.c
+++ b/path.c
@@ -1218,11 +1218,15 @@ int longest_ancestor_length(const char *path, struct string_list *prefixes)
const char *ceil = prefixes->items[i].string;
int len = strlen(ceil);
- if (len == 1 && ceil[0] == '/')
- len = 0; /* root matches anything, with length 0 */
- else if (!strncmp(path, ceil, len) && path[len] == '/')
- ; /* match of length len */
- else
+ /*
+ * For root directories (`/`, `C:/`, `//server/share/`)
+ * adjust the length to exclude the trailing slash.
+ */
+ if (len > 0 && ceil[len - 1] == '/')
+ len--;
+
+ if (strncmp(path, ceil, len) ||
+ path[len] != '/' || !path[len + 1])
continue; /* no match */
if (len > max_len)
diff --git a/setup.c b/setup.c
index c04cd25..aad9ace 100644
--- a/setup.c
+++ b/setup.c
@@ -5,6 +5,7 @@
#include "string-list.h"
#include "chdir-notify.h"
#include "promisor-remote.h"
+#include "quote.h"
static int inside_git_dir = -1;
static int inside_work_tree = -1;
@@ -1024,6 +1025,48 @@ static int canonicalize_ceiling_entry(struct string_list_item *item,
}
}
+struct safe_directory_data {
+ const char *path;
+ int is_safe;
+};
+
+static int safe_directory_cb(const char *key, const char *value, void *d)
+{
+ struct safe_directory_data *data = d;
+
+ if (strcmp(key, "safe.directory"))
+ return 0;
+
+ if (!value || !*value) {
+ data->is_safe = 0;
+ } else if (!strcmp(value, "*")) {
+ data->is_safe = 1;
+ } else {
+ const char *interpolated = NULL;
+
+ if (!git_config_pathname(&interpolated, key, value) &&
+ !fspathcmp(data->path, interpolated ? interpolated : value))
+ data->is_safe = 1;
+
+ free((char *)interpolated);
+ }
+
+ return 0;
+}
+
+static int ensure_valid_ownership(const char *path)
+{
+ struct safe_directory_data data = { .path = path };
+
+ if (!git_env_bool("GIT_TEST_ASSUME_DIFFERENT_OWNER", 0) &&
+ is_path_owned_by_current_user(path))
+ return 1;
+
+ read_very_early_config(safe_directory_cb, &data);
+
+ return data.is_safe;
+}
+
enum discovery_result {
GIT_DIR_NONE = 0,
GIT_DIR_EXPLICIT,
@@ -1032,7 +1075,8 @@ enum discovery_result {
/* these are errors */
GIT_DIR_HIT_CEILING = -1,
GIT_DIR_HIT_MOUNT_POINT = -2,
- GIT_DIR_INVALID_GITFILE = -3
+ GIT_DIR_INVALID_GITFILE = -3,
+ GIT_DIR_INVALID_OWNERSHIP = -4
};
/*
@@ -1122,11 +1166,15 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,
}
strbuf_setlen(dir, offset);
if (gitdirenv) {
+ if (!ensure_valid_ownership(dir->buf))
+ return GIT_DIR_INVALID_OWNERSHIP;
strbuf_addstr(gitdir, gitdirenv);
return GIT_DIR_DISCOVERED;
}
if (is_git_directory(dir->buf)) {
+ if (!ensure_valid_ownership(dir->buf))
+ return GIT_DIR_INVALID_OWNERSHIP;
strbuf_addstr(gitdir, ".");
return GIT_DIR_BARE;
}
@@ -1253,6 +1301,19 @@ const char *setup_git_directory_gently(int *nongit_ok)
dir.buf);
*nongit_ok = 1;
break;
+ case GIT_DIR_INVALID_OWNERSHIP:
+ if (!nongit_ok) {
+ struct strbuf quoted = STRBUF_INIT;
+
+ sq_quote_buf_pretty("ed, dir.buf);
+ die(_("unsafe repository ('%s' is owned by someone else)\n"
+ "To add an exception for this directory, call:\n"
+ "\n"
+ "\tgit config --global --add safe.directory %s"),
+ dir.buf, quoted.buf);
+ }
+ *nongit_ok = 1;
+ break;
case GIT_DIR_NONE:
/*
* As a safeguard against setup_git_directory_gently_1 returning
diff --git a/t/t0033-safe-directory.sh b/t/t0033-safe-directory.sh
new file mode 100755
index 0000000..239d93f
--- /dev/null
+++ b/t/t0033-safe-directory.sh
@@ -0,0 +1,49 @@
+#!/bin/sh
+
+test_description='verify safe.directory checks'
+
+. ./test-lib.sh
+
+GIT_TEST_ASSUME_DIFFERENT_OWNER=1
+export GIT_TEST_ASSUME_DIFFERENT_OWNER
+
+expect_rejected_dir () {
+ test_must_fail git status 2>err &&
+ grep "safe.directory" err
+}
+
+test_expect_success 'safe.directory is not set' '
+ expect_rejected_dir
+'
+
+test_expect_success 'safe.directory does not match' '
+ git config --global safe.directory bogus &&
+ expect_rejected_dir
+'
+
+test_expect_success 'path exist as different key' '
+ git config --global foo.bar "$(pwd)" &&
+ expect_rejected_dir
+'
+
+test_expect_success 'safe.directory matches' '
+ git config --global --add safe.directory "$(pwd)" &&
+ git status
+'
+
+test_expect_success 'safe.directory matches, but is reset' '
+ git config --global --add safe.directory "" &&
+ expect_rejected_dir
+'
+
+test_expect_success 'safe.directory=*' '
+ git config --global --add safe.directory "*" &&
+ git status
+'
+
+test_expect_success 'safe.directory=*, but is reset' '
+ git config --global --add safe.directory "" &&
+ expect_rejected_dir
+'
+
+test_done
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index 56db5c8..f538264 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh
@@ -55,12 +55,15 @@ fi
ancestor() {
# We do some math with the expected ancestor length.
expected=$3
- if test -n "$rootoff" && test "x$expected" != x-1; then
- expected=$(($expected-$rootslash))
- test $expected -lt 0 ||
- expected=$(($expected+$rootoff))
- fi
- test_expect_success "longest ancestor: $1 $2 => $expected" \
+ case "$rootoff,$expected,$2" in
+ *,*,//*) ;; # leave UNC paths alone
+ [0-9]*,[0-9]*,/*)
+ # On Windows, expect MSYS2 pseudo root translation for
+ # Unix-style absolute paths
+ expected=$(($expected-$rootslash+$rootoff))
+ ;;
+ esac
+ test_expect_success $4 "longest ancestor: $1 $2 => $expected" \
"actual=\$(test-tool path-utils longest_ancestor_length '$1' '$2') &&
test \"\$actual\" = '$expected'"
}
@@ -156,6 +159,11 @@ ancestor /foo/bar /foo 4
ancestor /foo/bar /foo:/bar 4
ancestor /foo/bar /bar -1
+# Windows-specific: DOS drives, network shares
+ancestor C:/Users/me C:/ 2 MINGW
+ancestor D:/Users/me C:/ -1 MINGW
+ancestor //server/share/my-directory //server/share/ 14 MINGW
+
test_expect_success 'strip_path_suffix' '
test c:/msysgit = $(test-tool path-utils strip_path_suffix \
c:/msysgit/libexec//git-core libexec/git-core)
|