Limit receive commands

Place a configurable upper bound on the amount of command data
received from clients during `git push`.  The limit is applied to the
encoded wire protocol format, not the JGit in-memory representation.
This allows clients to flexibly use the limit; shorter reference names
allow for more commands, longer reference names permit fewer commands
per batch.

Based on data gathered from many repositories at $DAY_JOB, the average
reference name is well under 200 bytes when encoded in UTF-8 (the wire
encoding).  The new 3 MiB default receive.maxCommandBytes allows about
11,155 references in a single `git push` invocation.  A Gerrit Code
Review system with six-digit change numbers could still encode 29,399
references in the 3 MiB maxCommandBytes limit.

Change-Id: I84317d396d25ab1b46820e43ae2b73943646032c
Signed-off-by: David Pursehouse <david.pursehouse@gmail.com>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
This commit is contained in:
Shawn Pearce 2016-07-04 18:04:43 -07:00 committed by Matthias Sohn
parent 67da5635a4
commit 0bff481d45
5 changed files with 156 additions and 26 deletions

View File

@ -173,4 +173,27 @@ public void invalidCommand() throws IOException {
} }
} }
} }
@Test
public void limitCommandBytes() throws IOException {
Map<String, RemoteRefUpdate> updates = new HashMap<>();
for (int i = 0; i < 4; i++) {
RemoteRefUpdate rru = new RemoteRefUpdate(
null, null, obj2, "refs/test/T" + i,
false, null, ObjectId.zeroId());
updates.put(rru.getRemoteName(), rru);
}
server.getConfig().setInt("receive", null, "maxCommandBytes", 170);
try (Transport tn = testProtocol.open(uri, client, "server");
PushConnection connection = tn.openPush()) {
try {
connection.push(NullProgressMonitor.INSTANCE, updates);
fail("server did not abort");
} catch (TransportException e) {
String msg = e.getMessage();
assertEquals("remote: Too many commands", msg);
}
}
}
} }

View File

@ -609,6 +609,7 @@ tagOnRepoWithoutHEADCurrentlyNotSupported=Tag on repository without HEAD current
theFactoryMustNotBeNull=The factory must not be null theFactoryMustNotBeNull=The factory must not be null
timeIsUncertain=Time is uncertain timeIsUncertain=Time is uncertain
timerAlreadyTerminated=Timer already terminated timerAlreadyTerminated=Timer already terminated
tooManyCommands=Too many commands
tooManyIncludeRecursions=Too many recursions; circular includes in config file(s)? tooManyIncludeRecursions=Too many recursions; circular includes in config file(s)?
topologicalSortRequired=Topological sort required. topologicalSortRequired=Topological sort required.
transactionAborted=transaction aborted transactionAborted=transaction aborted

View File

@ -669,6 +669,7 @@ public static JGitText get() {
/***/ public String theFactoryMustNotBeNull; /***/ public String theFactoryMustNotBeNull;
/***/ public String timeIsUncertain; /***/ public String timeIsUncertain;
/***/ public String timerAlreadyTerminated; /***/ public String timerAlreadyTerminated;
/***/ public String tooManyCommands;
/***/ public String tooManyIncludeRecursions; /***/ public String tooManyIncludeRecursions;
/***/ public String topologicalSortRequired; /***/ public String topologicalSortRequired;
/***/ public String transportExceptionBadRef; /***/ public String transportExceptionBadRef;

View File

@ -97,6 +97,7 @@
import org.eclipse.jgit.revwalk.RevSort; import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.PacketLineIn.InputOverLimitIOException;
import org.eclipse.jgit.transport.ReceiveCommand.Result; import org.eclipse.jgit.transport.ReceiveCommand.Result;
import org.eclipse.jgit.util.io.InterruptTimer; import org.eclipse.jgit.util.io.InterruptTimer;
import org.eclipse.jgit.util.io.LimitedInputStream; import org.eclipse.jgit.util.io.LimitedInputStream;
@ -244,6 +245,8 @@ public Set<String> getCapabilities() {
String userAgent; String userAgent;
private Set<ObjectId> clientShallowCommits; private Set<ObjectId> clientShallowCommits;
private List<ReceiveCommand> commands; private List<ReceiveCommand> commands;
private long maxCommandBytes;
private long maxDiscardBytes;
private StringBuilder advertiseError; private StringBuilder advertiseError;
@ -318,6 +321,8 @@ protected BaseReceivePack(final Repository into) {
allowNonFastForwards = rc.allowNonFastForwards; allowNonFastForwards = rc.allowNonFastForwards;
allowOfsDelta = rc.allowOfsDelta; allowOfsDelta = rc.allowOfsDelta;
allowPushOptions = rc.allowPushOptions; allowPushOptions = rc.allowPushOptions;
maxCommandBytes = rc.maxCommandBytes;
maxDiscardBytes = rc.maxDiscardBytes;
advertiseRefsHook = AdvertiseRefsHook.DEFAULT; advertiseRefsHook = AdvertiseRefsHook.DEFAULT;
refFilter = RefFilter.DEFAULT; refFilter = RefFilter.DEFAULT;
advertisedHaves = new HashSet<ObjectId>(); advertisedHaves = new HashSet<ObjectId>();
@ -338,7 +343,8 @@ public ReceiveConfig parse(final Config cfg) {
final boolean allowNonFastForwards; final boolean allowNonFastForwards;
final boolean allowOfsDelta; final boolean allowOfsDelta;
final boolean allowPushOptions; final boolean allowPushOptions;
final long maxCommandBytes;
final long maxDiscardBytes;
final SignedPushConfig signedPush; final SignedPushConfig signedPush;
ReceiveConfig(final Config config) { ReceiveConfig(final Config config) {
@ -350,6 +356,12 @@ public ReceiveConfig parse(final Config cfg) {
true); true);
allowPushOptions = config.getBoolean("receive", "pushoptions", //$NON-NLS-1$ //$NON-NLS-2$ allowPushOptions = config.getBoolean("receive", "pushoptions", //$NON-NLS-1$ //$NON-NLS-2$
false); false);
maxCommandBytes = config.getLong("receive", //$NON-NLS-1$
"maxCommandBytes", //$NON-NLS-1$
3 << 20);
maxDiscardBytes = config.getLong("receive", //$NON-NLS-1$
"maxCommandDiscardBytes", //$NON-NLS-1$
-1);
signedPush = SignedPushConfig.KEY.parse(config); signedPush = SignedPushConfig.KEY.parse(config);
} }
} }
@ -728,6 +740,38 @@ public void setTimeout(final int seconds) {
timeout = seconds; timeout = seconds;
} }
/**
* Set the maximum number of command bytes to read from the client.
*
* @param limit
* command limit in bytes; if 0 there is no limit.
* @since 4.7
*/
public void setMaxCommandBytes(long limit) {
maxCommandBytes = limit;
}
/**
* Set the maximum number of command bytes to discard from the client.
* <p>
* Discarding remaining bytes allows this instance to consume the rest of
* the command block and send a human readable over-limit error via the
* side-band channel. If the client sends an excessive number of bytes this
* limit kicks in and the instance disconnects, resulting in a non-specific
* 'pipe closed', 'end of stream', or similar generic error at the client.
* <p>
* When the limit is set to {@code -1} the implementation will default to
* the larger of {@code 3 * maxCommandBytes} or {@code 3 MiB}.
*
* @param limit
* discard limit in bytes; if 0 there is no limit; if -1 the
* implementation tries to set a reasonable default.
* @since 4.7
*/
public void setMaxCommandDiscardBytes(long limit) {
maxDiscardBytes = limit;
}
/** /**
* Set the maximum allowed Git object size. * Set the maximum allowed Git object size.
* <p> * <p>
@ -741,7 +785,6 @@ public void setMaxObjectSizeLimit(final long limit) {
maxObjectSizeLimit = limit; maxObjectSizeLimit = limit;
} }
/** /**
* Set the maximum allowed pack size. * Set the maximum allowed pack size.
* <p> * <p>
@ -1134,13 +1177,16 @@ public ReceivedPackStatistics getReceivedPackStatistics() {
* @throws IOException * @throws IOException
*/ */
protected void recvCommands() throws IOException { protected void recvCommands() throws IOException {
PacketLineIn pck = maxCommandBytes > 0
? new PacketLineIn(rawIn, maxCommandBytes)
: pckIn;
PushCertificateParser certParser = getPushCertificateParser(); PushCertificateParser certParser = getPushCertificateParser();
boolean firstPkt = true; boolean firstPkt = true;
try { try {
for (;;) { for (;;) {
String line; String line;
try { try {
line = pckIn.readString(); line = pck.readString();
} catch (EOFException eof) { } catch (EOFException eof) {
if (commands.isEmpty()) if (commands.isEmpty())
return; return;
@ -1163,13 +1209,13 @@ protected void recvCommands() throws IOException {
enableCapabilities(); enableCapabilities();
if (line.equals(GitProtocolConstants.OPTION_PUSH_CERT)) { if (line.equals(GitProtocolConstants.OPTION_PUSH_CERT)) {
certParser.receiveHeader(pckIn, !isBiDirectionalPipe()); certParser.receiveHeader(pck, !isBiDirectionalPipe());
continue; continue;
} }
} }
if (line.equals(PushCertificateParser.BEGIN_SIGNATURE)) { if (line.equals(PushCertificateParser.BEGIN_SIGNATURE)) {
certParser.receiveSignature(pckIn); certParser.receiveSignature(pck);
continue; continue;
} }
@ -1186,18 +1232,31 @@ protected void recvCommands() throws IOException {
} }
pushCert = certParser.build(); pushCert = certParser.build();
if (hasCommands()) { if (hasCommands()) {
readPostCommands(pckIn); readPostCommands(pck);
} }
} catch (PackProtocolException e) { } catch (PackProtocolException e) {
if (sideBand) { discardCommands();
try {
pckIn.discardUntilEnd();
} catch (IOException e2) {
// Ignore read failures attempting to discard.
}
}
fatalError(e.getMessage()); fatalError(e.getMessage());
throw e; throw e;
} catch (InputOverLimitIOException e) {
String msg = JGitText.get().tooManyCommands;
discardCommands();
fatalError(msg);
throw new PackProtocolException(msg);
}
}
private void discardCommands() {
if (sideBand) {
long max = maxDiscardBytes;
if (max < 0) {
max = Math.max(3 * maxCommandBytes, 3L << 20);
}
try {
new PacketLineIn(rawIn, max).discardUntilEnd();
} catch (IOException e) {
// Ignore read failures attempting to discard.
}
} }
} }

View File

@ -87,19 +87,32 @@ static enum AckNackResult {
ACK_READY; ACK_READY;
} }
private final byte[] lineBuffer = new byte[SideBandOutputStream.SMALL_BUF];
private final InputStream in; private final InputStream in;
private long limit;
private final byte[] lineBuffer;
/** /**
* Create a new packet line reader. * Create a new packet line reader.
* *
* @param i * @param in
* the input stream to consume. * the input stream to consume.
*/ */
public PacketLineIn(final InputStream i) { public PacketLineIn(InputStream in) {
in = i; this(in, 0);
lineBuffer = new byte[SideBandOutputStream.SMALL_BUF]; }
/**
* Create a new packet line reader.
*
* @param in
* the input stream to consume.
* @param limit
* bytes to read from the input; unlimited if set to 0.
* @since 4.7
*/
public PacketLineIn(InputStream in, long limit) {
this.in = in;
this.limit = limit;
} }
AckNackResult readACK(final MutableObjectId returnedId) throws IOException { AckNackResult readACK(final MutableObjectId returnedId) throws IOException {
@ -210,15 +223,48 @@ void discardUntilEnd() throws IOException {
int readLength() throws IOException { int readLength() throws IOException {
IO.readFully(in, lineBuffer, 0, 4); IO.readFully(in, lineBuffer, 0, 4);
int len;
try { try {
final int len = RawParseUtils.parseHexInt16(lineBuffer, 0); len = RawParseUtils.parseHexInt16(lineBuffer, 0);
if (len != 0 && len < 4)
throw new ArrayIndexOutOfBoundsException();
return len;
} catch (ArrayIndexOutOfBoundsException err) { } catch (ArrayIndexOutOfBoundsException err) {
throw new IOException(MessageFormat.format(JGitText.get().invalidPacketLineHeader, throw invalidHeader();
"" + (char) lineBuffer[0] + (char) lineBuffer[1] //$NON-NLS-1$
+ (char) lineBuffer[2] + (char) lineBuffer[3]));
} }
if (len == 0) {
return 0;
} else if (len < 4) {
throw invalidHeader();
}
if (limit != 0) {
int n = len - 4;
if (limit < n) {
limit = -1;
try {
IO.skipFully(in, n);
} catch (IOException e) {
// Ignore failure discarding packet over limit.
}
throw new InputOverLimitIOException();
}
// if set limit must not be 0 (means unlimited).
limit = n < limit ? limit - n : -1;
}
return len;
}
private IOException invalidHeader() {
return new IOException(MessageFormat.format(JGitText.get().invalidPacketLineHeader,
"" + (char) lineBuffer[0] + (char) lineBuffer[1] //$NON-NLS-1$
+ (char) lineBuffer[2] + (char) lineBuffer[3]));
}
/**
* IOException thrown by read when the configured input limit is exceeded.
*
* @since 4.7
*/
public static class InputOverLimitIOException extends IOException {
private static final long serialVersionUID = 1L;
} }
} }