Skip to content

Commit b724448

Browse files
committed
Fix set-gid bit clearing under Linux when effective gid is different from file gid.
1 parent 8b5730a commit b724448

File tree

4 files changed

+201
-7
lines changed

4 files changed

+201
-7
lines changed

src/main/java/com/gitblit/utils/JGitUtils.java

+15-1
Original file line numberDiff line numberDiff line change
@@ -403,9 +403,23 @@ public static int adjustSharedPerm(File path, GitConfigSharedRepository configSh
403403
if (! path.exists()) return -1;
404404

405405
int perm = configShared.getPerm();
406-
int mode = JnaUtils.getFilemode(path);
406+
JnaUtils.Filestat stat = JnaUtils.getFilestat(path);
407+
if (stat == null) return -1;
408+
int mode = stat.mode;
407409
if (mode < 0) return -1;
408410

411+
// Now, here is the kicker: Under Linux, chmod'ing a sgid file whose guid is different from the process'
412+
// effective guid will reset the sgid flag of the file. Since there is no way to get the sgid flag back in
413+
// that case, we decide to rather not touch is and getting the right permissions will have to be achieved
414+
// in a different way, e.g. by using an appropriate umask for the Gitblit process.
415+
if (System.getProperty("os.name").toLowerCase().startsWith("linux")) {
416+
if ( ((mode & (JnaUtils.S_ISGID | JnaUtils.S_ISUID)) != 0)
417+
&& stat.gid != JnaUtils.getegid() ) {
418+
LOGGER.debug("Not adjusting permissions to prevent clearing suid/sgid bits for '" + path + "'" );
419+
return 0;
420+
}
421+
}
422+
409423
// If the owner has no write access, delete it from group and other, too.
410424
if ((mode & JnaUtils.S_IWUSR) == 0) perm &= ~0222;
411425
// If the owner has execute access, set it for all blocks that have read access.

src/main/java/com/gitblit/utils/JnaUtils.java

+84-6
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,28 @@ public static boolean isWindows()
7979

8080
private interface UnixCLibrary extends Library {
8181
public int chmod(String path, int mode);
82+
public int getgid();
83+
public int getegid();
84+
}
85+
86+
87+
public static int getgid()
88+
{
89+
if (isWindows()) {
90+
throw new UnsupportedOperationException("The method JnaUtils.getgid is not supported under Windows.");
91+
}
92+
93+
return getUnixCLibrary().getgid();
94+
}
95+
96+
97+
public static int getegid()
98+
{
99+
if (isWindows()) {
100+
throw new UnsupportedOperationException("The method JnaUtils.getegid is not supported under Windows.");
101+
}
102+
103+
return getUnixCLibrary().getegid();
82104
}
83105

84106

@@ -157,21 +179,77 @@ public static int getFilemode(String path)
157179
throw new UnsupportedOperationException("The method JnaUtils.getFilemode is not supported under Windows.");
158180
}
159181

182+
Filestat stat = getFilestat(path);
183+
if ( stat == null ) return -1;
184+
return stat.mode;
185+
}
186+
187+
188+
/**
189+
* Status information of a file.
190+
*/
191+
public static class Filestat
192+
{
193+
public int mode; // file mode, permissions, type
194+
public int uid; // user Id of owner
195+
public int gid; // group Id of owner
196+
197+
Filestat(int mode, int uid, int gid) {
198+
this.mode = mode; this.uid = uid; this.gid = gid;
199+
}
200+
}
201+
202+
203+
/**
204+
* Get Unix file status information for a file.
205+
*
206+
* This method is only implemented for OSes of the Unix family. It returns file status
207+
* information for a file. Currently this is the file mode, the user id and group id of the owner.
208+
*
209+
* @param path
210+
* File/directory to get the file status from.
211+
* @return Upon successful completion, a Filestat object containing the file information is returned.
212+
* Otherwise, null is returned.
213+
*/
214+
public static Filestat getFilestat(File path)
215+
{
216+
return getFilestat(path.getAbsolutePath());
217+
}
218+
219+
220+
/**
221+
* Get Unix file status information for a file.
222+
*
223+
* This method is only implemented for OSes of the Unix family. It returns file status
224+
* information for a file. Currently this is the file mode, the user id and group id of the owner.
225+
*
226+
* @param path
227+
* Path to a file/directory to get the file status from.
228+
* @return Upon successful completion, a Filestat object containing the file information is returned.
229+
* Otherwise, null is returned.
230+
*/
231+
public static Filestat getFilestat(String path)
232+
{
233+
if (isWindows()) {
234+
throw new UnsupportedOperationException("The method JnaUtils.getFilestat is not supported under Windows.");
235+
}
236+
160237

161238
int mode = 0;
162239

163240
// Use a Runtime, because implementing stat() via JNA is just too much trouble.
241+
// This could be done with the 'stat' command, too. But that may have a shell specific implementation, so we use 'ls' instead.
164242
String lsLine = runProcessLs(path);
165243
if (lsLine == null) {
166244
LOGGER.debug("Could not get file information for path " + path);
167-
return -1;
245+
return null;
168246
}
169247

170-
Pattern p = Pattern.compile("^(([-bcdlsp])([-r][-w][-xSs])([-r][-w][-xSs])([-r][-w][-xTt])) ");
248+
Pattern p = Pattern.compile("^(([-bcdlspCDMnP?])([-r][-w][-xSs])([-r][-w][-xSs])([-r][-w][-xTt]))[@+.]? +[0-9]+ +([0-9]+) +([0-9]+) ");
171249
Matcher m = p.matcher(lsLine);
172250
if ( !m.lookingAt() ) {
173251
LOGGER.debug("Could not parse valid file mode information for path " + path);
174-
return -1;
252+
return null;
175253
}
176254

177255
// Parse mode string to mode bits
@@ -227,20 +305,20 @@ public static int getFilemode(String path)
227305
}
228306
}
229307

230-
return mode;
308+
return new Filestat(mode, Integer.parseInt(m.group(6)), Integer.parseInt(m.group(7)));
231309
}
232310

233311

234312
/**
235-
* Run the unix command 'ls -ldO' on a single file and return the resulting output line.
313+
* Run the unix command 'ls -ldn' on a single file and return the resulting output line.
236314
*
237315
* @param path
238316
* Path to a single file or directory.
239317
* @return The first line of output from the 'ls' command. Null, if an error occurred and no line could be read.
240318
*/
241319
private static String runProcessLs(String path)
242320
{
243-
String cmd = "ls -ld " + path;
321+
String cmd = "ls -ldn " + path;
244322
Runtime rt = Runtime.getRuntime();
245323
Process pr = null;
246324
InputStreamReader ir = null;

src/test/java/com/gitblit/tests/JGitUtilsTest.java

+64
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,70 @@ public void testCreateRepositorySharedCustom() throws Exception {
218218
}
219219
}
220220

221+
@Test
222+
public void testCreateRepositorySharedSgidParent() throws Exception {
223+
if (! JnaUtils.isWindows()) {
224+
String repositoryAll = "NewTestRepositoryAll.git";
225+
String repositoryUmask = "NewTestRepositoryUmask.git";
226+
String sgidParent = "sgid";
227+
228+
File parent = new File(GitBlitSuite.REPOSITORIES, sgidParent);
229+
File folder = null;
230+
boolean parentExisted = parent.exists();
231+
try {
232+
if (!parentExisted) {
233+
assertTrue("Could not create SGID parent folder.", parent.mkdir());
234+
}
235+
int mode = JnaUtils.getFilemode(parent);
236+
assertTrue(mode > 0);
237+
assertEquals(0, JnaUtils.setFilemode(parent, mode | JnaUtils.S_ISGID | JnaUtils.S_IWGRP));
238+
239+
Repository repository = JGitUtils.createRepository(parent, repositoryAll, "all");
240+
folder = FileKey.resolve(new File(parent, repositoryAll), FS.DETECTED);
241+
assertNotNull(repository);
242+
243+
assertEquals("2", repository.getConfig().getString("core", null, "sharedRepository"));
244+
245+
assertTrue(folder.exists());
246+
mode = JnaUtils.getFilemode(folder);
247+
assertEquals(JnaUtils.S_ISGID, mode & JnaUtils.S_ISGID);
248+
249+
mode = JnaUtils.getFilemode(folder.getAbsolutePath() + "/HEAD");
250+
assertEquals(JnaUtils.S_IRGRP | JnaUtils.S_IWGRP, mode & JnaUtils.S_IRWXG);
251+
assertEquals(JnaUtils.S_IROTH, mode & JnaUtils.S_IRWXO);
252+
253+
mode = JnaUtils.getFilemode(folder.getAbsolutePath() + "/config");
254+
assertEquals(JnaUtils.S_IRGRP | JnaUtils.S_IWGRP, mode & JnaUtils.S_IRWXG);
255+
assertEquals(JnaUtils.S_IROTH, mode & JnaUtils.S_IRWXO);
256+
257+
repository.close();
258+
RepositoryCache.close(repository);
259+
260+
261+
262+
repository = JGitUtils.createRepository(parent, repositoryUmask, "umask");
263+
folder = FileKey.resolve(new File(parent, repositoryUmask), FS.DETECTED);
264+
assertNotNull(repository);
265+
266+
assertEquals(null, repository.getConfig().getString("core", null, "sharedRepository"));
267+
268+
assertTrue(folder.exists());
269+
mode = JnaUtils.getFilemode(folder);
270+
assertEquals(JnaUtils.S_ISGID, mode & JnaUtils.S_ISGID);
271+
272+
repository.close();
273+
RepositoryCache.close(repository);
274+
}
275+
finally {
276+
FileUtils.delete(new File(parent, repositoryAll), FileUtils.RECURSIVE | FileUtils.IGNORE_ERRORS);
277+
FileUtils.delete(new File(parent, repositoryUmask), FileUtils.RECURSIVE | FileUtils.IGNORE_ERRORS);
278+
if (!parentExisted) {
279+
FileUtils.delete(parent, FileUtils.RECURSIVE | FileUtils.IGNORE_ERRORS);
280+
}
281+
}
282+
}
283+
}
284+
221285
@Test
222286
public void testRefs() throws Exception {
223287
Repository repository = GitBlitSuite.getJGitRepository();

src/test/java/com/gitblit/tests/JnaUtilsTest.java

+38
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.io.File;
2121
import java.io.IOException;
2222
import static org.junit.Assert.assertEquals;
23+
import static org.junit.Assert.assertNotNull;
2324
import static org.junit.Assert.assertTrue;
2425

2526
import org.apache.commons.io.FileUtils;
@@ -35,6 +36,24 @@
3536
*/
3637
public class JnaUtilsTest {
3738

39+
@Test
40+
public void testGetgid() {
41+
if (JnaUtils.isWindows()) {
42+
try {
43+
JnaUtils.getFilemode(GitBlitSuite.REPOSITORIES);
44+
} catch(UnsupportedOperationException e) {}
45+
}
46+
else {
47+
int gid = JnaUtils.getgid();
48+
assertTrue(gid >= 0);
49+
int egid = JnaUtils.getegid();
50+
assertTrue(egid >= 0);
51+
assertTrue("Really? You're running unit tests as root?!", gid > 0);
52+
System.out.println("gid: " + gid + " egid: " + egid);
53+
}
54+
}
55+
56+
3857
@Test
3958
public void testGetFilemode() throws IOException {
4059
if (JnaUtils.isWindows()) {
@@ -111,4 +130,23 @@ public void testSetFilemode() throws IOException {
111130
FileUtils.deleteDirectory(repository.getDirectory());
112131
}
113132
}
133+
134+
135+
@Test
136+
public void testGetFilestat() {
137+
if (JnaUtils.isWindows()) {
138+
try {
139+
JnaUtils.getFilemode(GitBlitSuite.REPOSITORIES);
140+
} catch(UnsupportedOperationException e) {}
141+
}
142+
else {
143+
JnaUtils.Filestat stat = JnaUtils.getFilestat(GitBlitSuite.REPOSITORIES);
144+
assertNotNull(stat);
145+
assertTrue(stat.mode > 0);
146+
assertTrue(stat.uid > 0);
147+
assertTrue(stat.gid > 0);
148+
}
149+
}
150+
151+
114152
}

0 commit comments

Comments
 (0)